Compare commits

..

226 commits

Author SHA1 Message Date
f55ef1d7ef Version 0.10.0-rc.7 2021-03-19 11:46:46 -04:00
14bc9f0946
Merge pull request #1500 from LemmyNet/jwt_revocation_dess
Jwt revocation dess
2021-03-19 15:43:47 +00:00
493598c1ba A few suggestion fixes. 2021-03-19 10:02:58 -04:00
96efe302ce
Merge pull request #1499 from LemmyNet/strictly_type_db_ids
Strictly type db ids
2021-03-19 13:41:22 +00:00
e25bcb35d7
Merge pull request #1428 from LemmyNet/split_user_table
Split user table
2021-03-19 13:20:23 +00:00
05b485b678 Merge branch 'Mart-Bogdan-1462-jwt-revocation-on-pwd-change' into jwt_revocation_dess 2021-03-19 00:31:49 -04:00
360d4ea8d1 Merge branch '1462-jwt-revocation-on-pwd-change' of https://github.com/Mart-Bogdan/lemmy into Mart-Bogdan-1462-jwt-revocation-on-pwd-change 2021-03-18 21:41:00 -04:00
c06d612432 Merge branch 'split_user_table' into strictly_type_db_ids 2021-03-18 19:37:54 -04:00
c88722983e Merge branch 'main' into split_user_table 2021-03-18 19:36:48 -04:00
33a326854a
Set CARGO_HOME for CI so deps arent redownloaded (#1497)
* Set CARGO_HOME for CI so deps arent redownloaded

* run find on x86

* fix path

* cleanup

* try again

* use mv
2021-03-18 16:35:04 -04:00
9930c7288a Merge branch 'split_user_table' into strictly_type_db_ids 2021-03-18 16:30:42 -04:00
8d9fab0389 Merge branch 'main' into split_user_table 2021-03-18 16:30:29 -04:00
c3efb9f7cf Strictly typing DB id fields. Fixes #1498 2021-03-18 16:25:21 -04:00
5899b89ef2 Adding some comments to notifs. 2021-03-18 10:59:17 -04:00
99e5a4d1c3 Moving send email check inside function. 2021-03-18 10:52:25 -04:00
dessalines
db4fe8031c Merge pull request 'Insert announced activities into DB for fetching (fixes #1494)' (#187) from insert-local-announce into main
Reviewed-on: https://yerbamate.ml/LemmyNet/lemmy/pulls/187
2021-03-16 13:47:32 +00:00
270ce539bf Removing some TODOS. 2021-03-15 18:18:50 -04:00
b9f483bc27 Version 0.10.0-rc.5 2021-03-15 14:50:50 -04:00
8ee624a542 Some changes
- Changing claim name to local_user_id to facilitate logout.
- Changing AddAdmin back to using person_id
2021-03-15 14:02:27 -04:00
621355b6ef Insert announced activities into DB for fetching (fixes #1494) 2021-03-15 13:58:54 +01:00
72b5e0cab5
Merge pull request #1491 from LemmyNet/upgrade_pictrs_3
Upgrading pictrs.
2021-03-15 12:14:31 +00:00
Bogdan Mart
74272ed754 more correct tests 2021-03-13 22:36:40 +02:00
Bogdan Mart
4426c3176d fix timestamp condition #1462 2021-03-13 22:18:26 +02:00
Bogdan Mart
7b0a09e84e Merge remote-tracking branch 'origin/main' into 1462-jwt-revocation-on-pwd-change
* origin/main:
  revert Compose file version from 3.3 to 2.2
  Adding more mem limits
  bump memory limit of iframely
  Remove extra category_id s . Fixes #1429
  Fixing wrong user_ and community icon and banner urls.
  Remove category from activitypub context
  Adding a password length check to other API actions. (#1474)
  Update test script
  Use URL type in most outstanding struct fields (#1468)
  Forbid usage of unwrap
  Upgrade Rust version
  Rewrite settings implementation. Fixes #1270 (#1433)
  Rename `lemmy_structs` to `lemmy_api_structs`

# Conflicts:
#	crates/db_schema/src/source/user.rs
2021-03-13 20:19:55 +02:00
Bogdan Mart
ab947f1f08 User token revocation upon password change
Added DB column validator_time and chedking that is is less then token's "Issuead at time"
Wip on #1462
2021-03-13 20:16:35 +02:00
5998c83b2a Only sending private message if its a local user. 2021-03-12 15:18:03 -05:00
434fb53dd1 Trying to fix API tests. 2021-03-12 14:09:03 -05:00
75a95acf04 Change joinuser, sendusermessage to use local_user_id 2021-03-12 10:54:47 -05:00
dessalines
931a132161 Merge pull request 'Add memory limit for pictrs' (#186) from nutomic-patch-1 into main
Reviewed-on: https://yerbamate.ml/LemmyNet/lemmy/pulls/186
2021-03-12 15:16:49 +00:00
0a7271a185 Upgrading pictrs. 2021-03-12 10:13:20 -05:00
7c039340ed 2nd pass. JWT now uses local_user_id 2021-03-11 17:47:44 -05:00
7d04f371a5 fixing tests 3 2021-03-11 12:08:30 -05:00
5d8ccbafe4 Fixing some tests 2 2021-03-11 11:54:03 -05:00
1a4c8c08ee Fixing some tests 2021-03-11 11:41:04 -05:00
9cb4dad4b4 A first pass. 2021-03-10 23:43:11 -05:00
ddf4a667b1 ~80% done 2021-03-10 17:33:55 -05:00
nutomic
fc74bfeb23 Add memory limit for pictrs 2021-03-10 18:38:00 +00:00
8f6b8895f4
Merge pull request #1485 from PatMulligan/fix-docker-compose-yaml
revert Compose file version from 3.3 to 2.2
2021-03-08 15:41:17 +00:00
Patrick Mulligan
a650312858 revert Compose file version from 3.3 to 2.2 2021-03-08 09:23:50 -06:00
ff2c71a74a Adding more mem limits 2021-03-04 22:41:08 -05:00
Avery Pierce
126c6a23bb bump memory limit of iframely 2021-03-04 08:58:03 -06:00
e0c61c1334
Merge pull request #1478 from LemmyNet/fix_wrong_urls
Fixing wrong user_ and community icon and banner urls.
2021-03-04 12:18:30 +00:00
f7aa97d45e
Merge pull request #1479 from LemmyNet/fix_extra_categories
Remove extra category_id s . Fixes #1429
2021-03-04 12:14:25 +00:00
a1c7584875 Remove extra category_id s . Fixes #1429 2021-03-03 23:44:07 -05:00
817b4ff08e Fixing wrong user_ and community icon and banner urls.
- Fixes #1477
2021-03-03 23:40:00 -05:00
ca3c1269f5 Merge branch 'main' of https://github.com/lemmynet/lemmy 2021-03-02 11:52:46 -05:00
dessalines
0a52396706 Merge pull request 'Forbid usage of unwrap' (#179) from clippy-unwrap into main
Reviewed-on: https://yerbamate.ml/LemmyNet/lemmy/pulls/179
2021-03-02 16:49:19 +00:00
dessalines
7c4969c92b Merge pull request 'Remove category from activitypub context' (#183) from context-remove-category into main
Reviewed-on: https://yerbamate.ml/LemmyNet/lemmy/pulls/183
2021-03-02 16:48:24 +00:00
dessalines
45a94203f2 Merge pull request 'Update test script' (#182) from update-test-script into main
Reviewed-on: https://yerbamate.ml/LemmyNet/lemmy/pulls/182
2021-03-02 16:47:58 +00:00
7189328f80 Remove category from activitypub context 2021-03-02 17:12:45 +01:00
Dessalines
134fece36d
Adding a password length check to other API actions. (#1474)
* Adding a password length check to other API actions.

- Fixes #1473

* Fixing comment.
2021-03-02 10:36:10 -05:00
985dbcaada Update test script 2021-03-02 13:57:06 +01:00
Andrew Yoon
e78ba38e94
Use URL type in most outstanding struct fields (#1468)
* Use URL type in most outstanding struct fields

This fixes all known remaining cases where url fields are stored as
plain strings, with the exception of form fields where empty strings
are used as sentinels (see `diesel_option_overwrite_to_url`).

Tested for regressions in the federated docker setup attempting to
exercise all changed fields, including through apub federation.

Fixes #1385

* Add migration to fix blank-string post.url values to be null

This also then fixes #602

* Address review feedback

- Fixed some unwraps and err message formatting
- Bumped the `url` library to 2.2.1 to fix a bug with serde error
  messages
- Add unit tests for the two diesel option override functions
- Fix migration teardown by adding a no-op

* Rename lemmy_db_queries::Url to lemmy_db_queries::DbUrl

* fix compile error

* box PostOrComment variants
2021-03-02 12:41:48 +00:00
7f56281c26 Forbid usage of unwrap 2021-03-01 19:24:34 +01:00
dessalines
45e05dac30 Merge pull request 'Upgrade Rust version' (#181) from upgrade-rust into main
Reviewed-on: https://yerbamate.ml/LemmyNet/lemmy/pulls/181
2021-03-01 18:02:39 +00:00
66946117e1 Upgrade Rust version 2021-03-01 18:46:56 +01:00
Dessalines
462c4a2954
Rewrite settings implementation. Fixes #1270 (#1433)
* A first attempt at using deser-hjson. Fixes #1270

* Trying to fix tests, try 1

* Trying to fix tests, try 2

* A few fixes to deser_hjson

- Removing unwrap_or_defaults, using impl functions.
- Reorganized settings

* Make clippy happy

* hjson list strings must be quoted.

* Adding support for env vars.

* Moving to structs and defaults file.

* Moving settings default and struct.
2021-03-01 17:24:11 +00:00
dessalines
5ce8adcb13 Merge pull request 'Rename lemmy_structs to lemmy_api_structs' (#180) from rename-structs into main
Reviewed-on: https://yerbamate.ml/LemmyNet/lemmy/pulls/180
2021-03-01 15:50:45 +00:00
3bdd78f341 Rename lemmy_structs to lemmy_api_structs 2021-03-01 14:08:41 +01:00
Dessalines
b5aa4cf41a
Merge pull request #1465 from arjenpdevries/patch-2
Update README.md
2021-02-27 18:08:58 -05:00
Arjen P. de Vries
a5e2463097
Update README.md
Federation is probably more complete than suggested here.

In response to:
https://github.com/LemmyNet/lemmy/issues/647#issuecomment-787068478
2021-02-27 20:26:30 +01:00
a869a2823b Still continuing on.... 2021-02-26 08:49:58 -05:00
dessalines
ff3e26452a Merge pull request 'Remove federation backward compatibility code (ref #1220)' (#164) from remove-backwards-compatibility into main
Reviewed-on: https://yerbamate.ml/LemmyNet/lemmy/pulls/164
2021-02-26 13:23:46 +00:00
dessalines
da5b27ecc6 Merge pull request 'Remove code for apub compatibility with Lemmy v0.8.9 and older' (#178) from remove-apub-compat-code into main
Reviewed-on: https://yerbamate.ml/LemmyNet/lemmy/pulls/178
2021-02-26 13:23:07 +00:00
c618b4efaa Remove federation backward compatibility code (ref #1220) 2021-02-26 14:06:26 +01:00
4cc341e4aa Remove code for apub compatibility with Lemmy v0.8.9 and older 2021-02-26 14:03:49 +01:00
82d97cf6de
Merge pull request #1455 from ajyoon/fix-flaky-db-tests
Support plain `cargo test` and disable unused doctests for speed
2021-02-25 22:54:40 +00:00
Andrew Yoon
600ae662a5 Support plain cargo test and disable unused doctests for speed
Since DB tests execute diesel migrations automatically, concurrent
execution causes flaky failures from simultaneous migrations. This can
be worked around using `cargo test --workspace -- --test-threads=1`,
which is what the CI config does, but this is not intuitive for
newcomer developers and unnecessarily slows down the test suite for
the majority of tests which are safe to run concurrently. This fixes
this issue by integrating with the small test crate `serial_test` and
using it to explicitly mark DB tests to run sequentially while
allowing all other tests to run in parallel.

Additionally, this greatly improves the speed of `cargo test` by
disabling doc-tests in all crates, since these are aren't currently
used and cargo's doc-test pass, even when no doc-tests exist, has
significant overhead. On my machine, this change significantly
improves test suite times by about 85%, making it much more practical
to develop with tools like `cargo watch` auto-running tests.
2021-02-25 15:44:30 -05:00
efc9047f87 Done with user->person migrations, now to code. 2021-02-25 14:04:12 -05:00
aba32917bd Merge branch 'main' into split_user_table 2021-02-25 12:34:00 -05:00
ea3c0e1772 Merge branch 'main' into remove-integration-tests 2021-02-25 11:37:54 -05:00
058052b46c Merge remote-tracking branch 'yerba/main' 2021-02-25 11:36:08 -05:00
dessalines
7c87da012e Merge pull request 'Remove categories (fixes #1429)' (#176) from remove-categories into main
Reviewed-on: https://yerbamate.ml/LemmyNet/lemmy/pulls/176
2021-02-25 16:35:07 +00:00
bf1e859e72 Remove broken actix_rt test 2021-02-25 17:25:31 +01:00
289cef3101 Remove integration tests (fixes #1449) 2021-02-25 16:31:41 +01:00
085c307b8b
Merge pull request #1451 from LemmyNet/update_cargo_chef
Use more recent version of cargo chef.
2021-02-25 15:19:29 +00:00
72783edb17 In remove categories down migration, add default for category 2021-02-25 16:16:02 +01:00
3141ad31de Remove categories (fixes #1429) 2021-02-25 13:22:37 +01:00
dessalines
40ceec9737 Merge pull request 'Better type safety for activity parsing' (#175) from apub-type-safety into main
Reviewed-on: https://yerbamate.ml/LemmyNet/lemmy/pulls/175
2021-02-24 22:20:15 +00:00
723ec65ac6 Use more recent version of cargo chef. 2021-02-24 17:10:28 -05:00
3ae62573b7 Better type safety for activity parsing 2021-02-24 20:37:27 +01:00
dessalines
6499709221 Merge pull request 'Dont include community in comment to field (fixes #1446)' (#174) from no-community-in-comment-to into main
Reviewed-on: https://yerbamate.ml/LemmyNet/lemmy/pulls/174
2021-02-24 01:03:07 +00:00
813fcabe13 Fix lemmy_dev ansible playbook 2021-02-23 19:35:09 +01:00
92ea9b97dd Dont include community in comment to field (fixes #1446) 2021-02-23 19:00:47 +01:00
dessalines
0c9b109bf7 Merge pull request 'Order outbox by published, not id' (#171) from outbox-order-published into main
Reviewed-on: https://yerbamate.ml/LemmyNet/lemmy/pulls/171
2021-02-22 20:37:06 +00:00
dessalines
2cbd158a11 Merge pull request 'Use name field for post titles instead of summary (ref #1220)' (#173) from apub-post-name into main
Reviewed-on: https://yerbamate.ml/LemmyNet/lemmy/pulls/173
2021-02-22 20:36:22 +00:00
dessalines
7dc3ff4544 Merge pull request 'Fix clippy error upper_case_acronyms' (#172) from fix-clippy into main
Reviewed-on: https://yerbamate.ml/LemmyNet/lemmy/pulls/172
2021-02-22 20:34:22 +00:00
8d5e9f865c Use name field for post titles instead of summary (ref #1220) 2021-02-22 19:34:41 +01:00
8096765f0e Fix clippy error upper_case_acronyms 2021-02-22 19:04:32 +01:00
8eb81bb153 Updating release version. 2021-02-19 13:11:16 -05:00
c81435c994 Version 0.9.9 2021-02-19 13:10:04 -05:00
7548b44d1b
Merge pull request #1442 from LemmyNet/fix_deploy_version_1
Fixing deploy version.
2021-02-19 18:09:27 +00:00
bcc8dae16b Fixing deploy version. 2021-02-19 13:05:42 -05:00
b014ac44c8 Adding 0.9.8 release notes. 2021-02-19 12:41:51 -05:00
a806493bc2 Version 0.9.8 2021-02-19 11:38:24 -05:00
b593047fb1 Order outbox by published, not id 2021-02-19 15:59:06 +01:00
9845366a36 Merge remote-tracking branch 'yerba/main' 2021-02-18 16:19:39 -05:00
dessalines
15d0f54a88 Merge pull request 'Specify order for activities query (fixes #1436)' (#169) from outbox-order into main
Reviewed-on: https://yerbamate.ml/LemmyNet/lemmy/pulls/169
2021-02-18 21:18:26 +00:00
Dessalines
8088055d38
Fix aggregates time columns 2 (#1427)
* Adding a new comment sort. Fixes #1294

* Fixing a migration comment.

* Adding a comment for newest_comment_time_necro

* Make sure federated items set correct aggregates fields in trigger.

- Fixes #1402
2021-02-18 10:53:04 -05:00
Dessalines
0c4b57a6d0
Adding a new comment sort for posts. Fixes #1294 (#1425)
* Adding a new comment sort. Fixes #1294

* Fixing a migration comment.

* Adding a comment for newest_comment_time_necro
2021-02-18 10:38:25 -05:00
d3707ad4ef Fix new compiler warning 2021-02-18 16:16:18 +01:00
13a949d9ec Specify order for activities query (fixes #1436) 2021-02-18 16:15:52 +01:00
6f683682c3
Merge pull request #1435 from LemmyNet/fix_lemmy_prod_open_port
Closing open lemmy-ui prod port. Fixes #1430
2021-02-17 16:42:32 +00:00
a920bf768e Closing open lemmy-ui prod port. Fixes #1430 2021-02-17 11:26:03 -05:00
a183815870 Adding a few more tables. 2021-02-15 14:34:10 -05:00
d0bd02eea0 Starting on user_ to person migration. 2021-02-14 13:46:16 -05:00
37ea778776 Merge remote-tracking branch 'origin/main' 2021-02-11 10:31:10 -05:00
dessalines
f37fd0ecfd Merge pull request 'Hide followed communities, except for own user (fixes #1303)' (#168) from hide-followed into main
Reviewed-on: https://yerbamate.ml/LemmyNet/lemmy/pulls/168
2021-02-11 15:31:04 +00:00
3d400ca21d Hide followed communities, except for own user (fixes #1303) 2021-02-11 14:53:17 +01:00
71aa8f3670
Merge pull request #1426 from LemmyNet/rss_link_post
Change RSS feeds to use lemmy URL for the rss link. Fixes #1378
2021-02-11 14:06:29 +01:00
37ad9e9a09 Change RSS feeds to use lemmy URL for the rss link. Fixes #1378 2021-02-10 15:43:03 -05:00
1af906c224 Merge remote-tracking branch 'origin/main' 2021-02-10 15:13:22 -05:00
dessalines
f899831ed3 Merge pull request 'Explicitly mark posts and comments as public (ref #1220)' (#167) from comments-posts-public into main
Reviewed-on: https://yerbamate.ml/LemmyNet/lemmy/pulls/167
2021-02-10 19:45:36 +00:00
08900b5b94
Merge pull request #1422 from LemmyNet/display_name_limit
Display name limit
2021-02-10 16:13:26 +00:00
5a33fce8bd Listing columns. 2021-02-10 11:05:12 -05:00
acadf0289e Fixing reason lengths to char counts. 2021-02-10 10:36:22 -05:00
2e5ccaf7fe Fixing display name limit. Fixes #1421 2021-02-10 10:27:48 -05:00
63d9c0ee46 Explicitly mark posts and comments as public (ref #1220) 2021-02-10 14:01:02 +01:00
dessalines
68edda7bf5 Merge pull request 'Move routes into separate crate to speed up compilation' (#166) from move-routes into main
Reviewed-on: https://yerbamate.ml/LemmyNet/lemmy/pulls/166
2021-02-09 18:44:45 +00:00
999d9f4d6c Move routes into separate crate to speed up compilation 2021-02-09 19:34:36 +01:00
ce00677880 Adding v0.9.7 release notes. 2021-02-08 15:24:51 -05:00
5656db3e3d Version 0.9.7 2021-02-08 15:17:56 -05:00
c213edf7ee Adding 0.9.6 release notes. 2021-02-06 11:50:31 -05:00
a7540b4947 Fixing build / drone badge. 2021-02-05 23:19:23 -05:00
5f112aad44 Fixing image links. #1355 2021-02-05 23:15:02 -05:00
f198f281cf Version 0.9.6 2021-02-05 13:01:29 -05:00
bf751dc7ab
Merge pull request #1415 from LemmyNet/fed_inbox_url_fix
Fixing inbox url code migration. Fixes #1414
2021-02-05 17:59:56 +00:00
dessalines
6f364e60fa Merge pull request 'Include object id when logging apub errors' (#165) from log-ids into main
Reviewed-on: https://yerbamate.ml/LemmyNet/lemmy/pulls/165
2021-02-05 17:52:51 +00:00
2b6df63aee Merge remote-tracking branch 'yerba/main' 2021-02-05 12:50:31 -05:00
dessalines
def8af7d8a Merge pull request 'Make apub extension fields optional (ref #1220)' (#163) from optional-apub-extensions into main
Reviewed-on: https://yerbamate.ml/LemmyNet/lemmy/pulls/163
2021-02-05 17:50:42 +00:00
14465b91b1 Fixing inbox url code migration. Fixes #1414 2021-02-05 12:06:32 -05:00
d09df9c02b
Merge pull request #1412 from LemmyNet/fix_search_communities_subscribed
Fixing community search not using auth. Fixes #1411
2021-02-05 16:33:06 +00:00
897263e5a3 Include object id when logging apub errors 2021-02-05 17:15:29 +01:00
4864f80656 Fixing community search not using auth. Fixes #1411 2021-02-05 10:36:28 -05:00
105dfc93f1 Make apub extension fields optional (ref #1220) 2021-02-05 14:23:57 +01:00
Dessalines
d5d99fa3b9
Moving docs to join.lemmy.ml . Fixes #1396 (#1410)
* Moving docs to join.lemmy.ml . Fixes #1396

* Removing submodule fetch from drone.
2021-02-05 12:30:49 +00:00
8a7e50381f Version 0.9.5 2021-02-04 22:48:21 -05:00
f45f2ec202 Removing old lemmy schema. 2021-02-04 11:35:20 -05:00
nutomic
1a4e35eb50 Store activitypub endpoints in database (#162)
Address review comments

Store Activitypub urls in database (fixes #808)

Co-authored-by: Felix Ableitner <me@nutomic.com>
Reviewed-on: https://yerbamate.ml/LemmyNet/lemmy/pulls/162
Co-Authored-By: nutomic <nutomic@noreply.yerbamate.ml>
Co-Committed-By: nutomic <nutomic@noreply.yerbamate.ml>
2021-02-04 16:34:58 +00:00
ed8a12f96f Some updates to 0.9.4 release. 2021-02-02 16:01:08 -05:00
525fdcf73c Some updates to 0.9.4 release. 2021-02-02 15:57:45 -05:00
840ab7cba4 Adding pre-release notes. 2021-02-02 15:45:02 -05:00
9415bec557 Version 0.9.4 2021-02-02 15:29:38 -05:00
712d497a2d Merge branch 'main' of https://github.com/lemmynet/lemmy 2021-02-02 11:13:31 -05:00
dbc94af51d
Merge pull request #1406 from LemmyNet/upgrade_deps
Trying to upgrade lemmys deps.
2021-02-02 15:17:11 +00:00
86d8c9b18e Adding awesome-humane-tech to readme. #1394 2021-02-02 09:35:56 -05:00
1857f02af8 Moving back tokio and reqwest. 2021-02-01 21:54:23 -05:00
10f0b3b877 Trying to upgrade lemmys deps. 2021-02-01 15:56:37 -05:00
Dessalines
0be9b5bddb
Add allowed and blocked instances to the federated_instances response. (#1398)
- Fixes #1315
2021-02-01 13:11:37 -05:00
Dessalines
6bb4f0b41f
Adding forum sort for post_aggregates. Fixes #1312 (#1400)
* Adding forum sort for post_aggregates. Fixes #1312

* Changing sort name from forum to MostComments.
2021-02-01 11:53:44 -05:00
Dessalines
f4d33389a5
Merge pull request #1401 from LemmyNet/non_null_post_view_vote
Post and comment vote views now return 0 instead of null.
2021-02-01 10:43:34 -05:00
c8254dc0a8
Merge pull request #1399 from LemmyNet/dont_let_banned_users_follow
Make sure banned users cant subscribe, and the ban unsubs them. Fixes…
2021-02-01 12:19:41 +00:00
6b36bf772e
Merge pull request #1397 from LemmyNet/parent_comment_check
Add check for parent comment. Fixes #1390
2021-02-01 12:13:18 +00:00
d2ba2960dd Post and comment vote views now return 0 instead of null.
- Fixes #1389
2021-01-31 10:29:21 -05:00
cd08fdf76f Make sure banned users cant subscribe, and the ban unsubs them. Fixes #1324 2021-01-30 23:55:14 -05:00
aecb2411d8 Add check for parent comment. Fixes #1390 2021-01-30 23:10:16 -05:00
9609bd99bb
Add readme link to translation instructions for documentation (#1392)
* Add readme link to translation instructions for documentation

* Fix contributing link in readme
2021-01-30 12:00:33 -05:00
Anton Kuzmin
5102cdddc1
rename lemmer to remmel (#1391) 2021-01-30 11:51:38 -05:00
3a05817b41 Version 0.9.3 2021-01-29 14:25:20 -05:00
51465bc0d7 Nodeinfo devs think halfyear is one word. 2021-01-29 14:24:10 -05:00
2322534648 Version 0.9.2 2021-01-29 13:49:43 -05:00
3f23e0e6b9 Adding camelCase to node-info users. 2021-01-29 13:48:59 -05:00
e6a16f08a3 Version 0.9.1 2021-01-29 11:43:16 -05:00
Dessalines
0fd0279543
Adding some recurring lemmy tasks. (#1386)
* Adding some recurring lemmy tasks.

- Add active users by day, week, month, and half year to site and
  community. Fixes #1195
- Periodically re-index the aggregates tables that use hot_rank.
  Fixes #1384
- Clear out old activities (> 6 months). Fixes #1133

* Some cleanup, recalculating actives every hour.
2021-01-29 11:38:27 -05:00
dessalines
f5e58c8bf5 Merge pull request 'Increase MAX_REQUEST_NUMBER for fetches to 25' (#161) from fetch-limit into main
Reviewed-on: https://yerbamate.ml/LemmyNet/lemmy/pulls/161
2021-01-29 15:52:23 +00:00
5f59d7ba5f Increase MAX_REQUEST_NUMBER for fetches to 25 2021-01-29 16:45:28 +01:00
62a145d8b3 Merge remote-tracking branch 'yerba/outbox-activities' 2021-01-29 09:17:14 -05:00
c09c462a6e Serve activities in community outbox (fixes #1216) 2021-01-29 14:48:42 +01:00
3d578f9df2
Use Url type for ap_id fields in database (fixes #1364) (#1371) 2021-01-27 11:42:23 -05:00
363ceea5c7 Fixing some readme links. Fixes #1382 2021-01-26 14:49:15 -05:00
c51f750831 Use Url type for ap_id fields in database (fixes #1364) 2021-01-26 18:52:18 +01:00
Dessalines
cf911c023d
Add listing type to list communities (#1380)
* Adding listing type to ListCommunities. Fixes #1379

* Upgrading lemmy-js-client.
2021-01-26 12:18:01 -05:00
ea59cf16e8
Merge pull request #1381 from LemmyNet/fix_mod_bans_and_adds
Fixing modlog not showing bans and adds. Fixes #1376
2021-01-26 16:57:49 +00:00
91d210ce79 Fixing modlog not showing bans and adds. Fixes #1376 2021-01-26 11:45:36 -05:00
f0dcc3a104 Updating releases.md 2021-01-25 09:44:33 -05:00
b2d6b554e8 Fixing release docs location. 2021-01-25 00:10:27 -05:00
1addbe361a Version 0.9.0 2021-01-24 22:43:52 -05:00
Dessalines
97617d699d
Docker manifest arm amd64 deploy (#1367)
* A first try at docker manifest. 1.

* Fixing api version location

* Version 0.9.0-rc.13

* Test docker.

* Test docker 2.

* Test docker 3.

* Test docker 4.

* Test docker 5.

* Test docker 6.

* Test docker 7.

* Test docker 8.

* Test docker 9.

* Test docker 10.

* Test docker 11.

* Test docker 12.

* Version 0.9.0-rc.14

* Test docker 13.

* Test docker 14.

* Version 0.9.0-rc.15

* Test docker 15.

* Version 0.9.0-rc.16

* Test docker 16.

* Version 0.9.0-rc.17
2021-01-24 22:44:35 -05:00
ac969dc737 Adding 0.9.0 Release notes. 2021-01-24 22:40:39 -05:00
c014bef84d Updating docs locations. 2021-01-23 14:56:41 -05:00
f2f9f5776c Merge branch 'uuttff8-main' 2021-01-22 13:35:12 -05:00
cb9c354c28 Updating to add lemmer. 2021-01-22 13:34:48 -05:00
Anton Kuzmin
b1fb90ff6d add lemmer 2021-01-22 21:32:00 +03:00
Dessalines
ee03cf8ae9
Explicit error http status codes (#1362)
* Trying to type specific errors.

* Using @asonix 's downcast method.
2021-01-21 16:32:19 +00:00
dessalines
a01af67948 Merge pull request 'Move most code into crates/ subfolder' (#159) from crates-folder into main
Reviewed-on: https://yerbamate.ml/LemmyNet/lemmy/pulls/159
2021-01-20 15:40:03 +00:00
3b64c58198 Move most code into crates/ subfolder 2021-01-20 16:21:27 +01:00
Dessalines
88284a999e
Merge pull request #1328 from LemmyNet/move_views_to_diesel
Move SQL views to diesel
2021-01-20 10:01:53 -05:00
856802ef35 Version 0.9.0-rc.12 2021-01-19 09:37:26 -05:00
Dessalines
24c78de5f0
Merge pull request #1358 from LemmyNet/v2_api_additions_1
A few API v2 changes based on nutomic's suggestions.
2021-01-19 09:35:43 -05:00
1de8a4606a A few API v2 changes based on nutomic's suggestions.
- Changed `edit_id` s to their type (comment_id)
- Moved websocket actions to their own file in structs and api.
- Got rid of UserViewDangerous, added UserSafeSettings.
  - GetSite now returns UserSafeSettings for `my_user`.
- Got rid of `admin` field in `Register`.
2021-01-18 16:57:31 -05:00
672d4507b2 Removing check documentation build from drone, now that's in lemmy-docs. 2021-01-18 11:08:53 -05:00
Dessalines
25dd1a21e2
Try arm fix (#1356)
* Trying to fix arm build.

* Version 0.9.0-rc.8

* Trying to fix arm build 2.

* Version 0.9.0-rc.9

* Checking time when removing lto.

* Version 0.9.0-rc.10

* Adding back in arm tests.

* Version 0.9.0-rc.11
2021-01-18 13:04:32 +00:00
6f2954dffd Version 0.9.0-rc.7 2021-01-15 13:32:10 -05:00
8cfee9ca7d Trying to fix arm build. 2021-01-15 13:31:10 -05:00
b124a29e05 Version 0.9.0-rc.6 2021-01-15 12:44:34 -05:00
Dessalines
b3163f99f4
Merge pull request #1344 from LemmyNet/remove_travis_and_federation_docker
Removing docker/federation and docker/travis folders.
2021-01-15 12:40:34 -05:00
fe4b516bd9 Adding back in federation docker-compose lemmy-ui writing 2021-01-15 12:38:44 -05:00
f0d928a433 Removing tag generation from drone. 2021-01-15 12:28:49 -05:00
edf0fd4381 Merge branch 'move_views_to_diesel' into remove_travis_and_federation_docker 2021-01-15 11:28:21 -06:00
Dessalines
1de737b7a3
Merge pull request #1352 from LemmyNet/ci-on-arm
Add drone CI for arm
2021-01-15 12:10:37 -05:00
Dessalines
29ec5d4855
Merge pull request #1353 from LemmyNet/move-coc
Code of conduct moved to documentation
2021-01-15 12:09:26 -05:00
Dessalines
381fd82c13
Merge pull request #1351 from LemmyNet/shorten-slur-filter
Shorten slur filter to avoid false positives (fixes #535)
2021-01-15 12:03:53 -05:00
8f61a148f6 Fixing comment count necro-bump issue. 2021-01-15 11:58:56 -05:00
a4f6ca0c9c Code of conduct moved to documentation 2021-01-15 17:49:23 +01:00
15710a0595 Shorten slur filter to avoid false positives (fixes #535) 2021-01-15 17:31:14 +01:00
f06b71d961 Add drone CI for arm 2021-01-15 15:24:48 +01:00
ccd2b9eb75 Fixing a stackoverflow error with url searching. 2021-01-14 23:56:53 -05:00
c8a8670aec Merge branch 'move_views_to_diesel' of https://github.com/lemmynet/lemmy into move_views_to_diesel 2021-01-14 19:16:02 -06:00
dessalines
110167f085 Ensure that comments are always delivered to the parent creator (fixes #1325) (#156)
Don't send activities to ourselves

Ensure that comments are always delivered to the parent creator (fixes #1325)

Co-authored-by: Felix Ableitner <me@nutomic.com>
Reviewed-on: https://yerbamate.ml/LemmyNet/lemmy/pulls/156
Co-Authored-By: dessalines <dessalines@noreply.yerbamate.ml>
Co-Committed-By: dessalines <dessalines@noreply.yerbamate.ml>
2021-01-15 01:18:18 +00:00
Dessalines
66102fb2d4
Merge pull request #1349 from LemmyNet/site_counts_local
Report only local counts in site_view.
2021-01-14 16:10:26 -05:00
4fdcb57753 Report only local counts in site_view.
- Move open_registrations under top level.
- Fixes #1340
2021-01-14 15:22:07 -05:00
dessalines
8b4a16a3f3 Merge pull request 'Add compilation benchmark, move scripts into subfolder' (#158) from compilation-benchmark into move_views_to_diesel
Reviewed-on: https://yerbamate.ml/LemmyNet/lemmy/pulls/158
2021-01-14 18:14:09 +00:00
5c198ea85d Add compilation benchmark, move scripts into subfolder 2021-01-14 18:04:01 +01:00
15c5e5c502 Merge branch 'move_views_to_diesel' into remove_travis_and_federation_docker 2021-01-13 14:20:21 -05:00
116d908002 Restoring docker-compose and nginx in federation folder. 2021-01-13 14:18:26 -05:00
0c932e3ace Merge remote-tracking branch 'yerba/move_views_to_diesel' into move_views_to_diesel 2021-01-13 13:16:25 -05:00
dessalines
6a04aaca55 Merge pull request 'Set debug=0 in cargo.toml to speed up builds' (#157) from debug-false into move_views_to_diesel
Reviewed-on: https://yerbamate.ml/LemmyNet/lemmy/pulls/157
2021-01-13 18:17:04 +00:00
cd19a72c41 Version 0.9.0-rc.4 2021-01-13 12:05:56 -05:00
36976acb2f Another notifs fix. 2021-01-13 12:04:00 -05:00
4677d3d782 Updating docs. 2021-01-13 12:03:26 -05:00
82227846af Fixing top level replies, and notifs. 2021-01-13 12:01:42 -05:00
d4e800175f Merge branch 'move_views_to_diesel' into remove_travis_and_federation_docker 2021-01-12 11:56:24 -05:00
3d4cc32525 Adding back start-local-instances. 2021-01-12 10:42:34 -05:00
7db754e94c Revert "Revert "Removing docker/federation and docker/travis folders.""
This reverts commit e483b6b51f.
2021-01-12 10:40:38 -05:00
e483b6b51f Revert "Removing docker/federation and docker/travis folders."
This reverts commit 689f5c1306.
2021-01-12 10:39:15 -05:00
689f5c1306 Removing docker/federation and docker/travis folders. 2021-01-11 20:41:10 -05:00
260 changed files with 9609 additions and 7037 deletions

View file

@ -1,49 +1,52 @@
---
kind: pipeline
name: default
name: amd64
platform:
os: linux
arch: amd64
steps:
- name: fetch git submodules
image: node:15-alpine3.12
commands:
- apk add git
- git submodule update --init --recursive --remote
- find docs/
- name: chown repo
image: ekidd/rust-musl-builder:1.47.0
image: ekidd/rust-musl-builder:1.50.0
user: root
commands:
- chown 1000:1000 . -R
- name: check documentation build
image: ekidd/rust-musl-builder:1.47.0
commands:
- cargo install mdbook --git https://github.com/Ruin0x11/mdBook.git --branch localization --rev d06249b
- mdbook build docs/
- name: check formatting
image: rustdocker/rust:nightly
commands:
- /root/.cargo/bin/cargo fmt -- --check
- name: cargo clippy
image: ekidd/rust-musl-builder:1.47.0
image: ekidd/rust-musl-builder:1.50.0
environment:
CARGO_HOME: /drone/src/.cargo
commands:
- whoami
- ls -la ~/.cargo
- mv ~/.cargo .
- ls -la .cargo
- cargo clippy --workspace --tests --all-targets --all-features -- -D warnings -D deprecated -D clippy::perf -D clippy::complexity -D clippy::dbg_macro
- cargo clippy --workspace -- -D clippy::unwrap_used
- name: cargo test
image: ekidd/rust-musl-builder:1.47.0
image: ekidd/rust-musl-builder:1.50.0
environment:
LEMMY_DATABASE_URL: postgres://lemmy:password@database:5432/lemmy
RUST_BACKTRACE: 1
RUST_TEST_THREADS: 1
CARGO_HOME: /drone/src/.cargo
commands:
- sudo apt-get update
- sudo apt-get -y install --no-install-recommends espeak postgresql-client
- cargo test --workspace --no-fail-fast
- name: cargo build
image: ekidd/rust-musl-builder:1.47.0
image: ekidd/rust-musl-builder:1.50.0
environment:
CARGO_HOME: /drone/src/.cargo
commands:
- cargo build
- mv target/x86_64-unknown-linux-musl/debug/lemmy_server target/lemmy_server
@ -54,21 +57,12 @@ steps:
LEMMY_DATABASE_URL: postgres://lemmy:password@database:5432
DO_WRITE_HOSTS_FILE: 1
commands:
- ls -la target/lemmy_server
- apk add bash curl postgresql-client
- bash api_tests/prepare-drone-federation-test.sh
- cd api_tests/
- yarn
- yarn api-test
- name: create docker tags
image: ekidd/rust-musl-builder:1.47.0
commands:
- echo "$(git describe),latest" > .tags
when:
ref:
- refs/tags/*
- name: make release build and push to docker hub
image: plugins/docker
settings:
@ -79,6 +73,24 @@ steps:
from_secret: docker_password
repo: dessalines/lemmy
auto_tag: true
auto_tag_suffix: linux-amd64
when:
ref:
- refs/tags/*
- name: push to docker manifest
image: plugins/manifest
settings:
username:
from_secret: docker_username
password:
from_secret: docker_password
target: "dessalines/lemmy:${DRONE_TAG}"
template: "dessalines/lemmy:${DRONE_TAG}-OS-ARCH"
platforms:
- linux/amd64
- linux/arm64
ignore_missing: true
when:
ref:
- refs/tags/*
@ -90,6 +102,89 @@ services:
POSTGRES_USER: lemmy
POSTGRES_PASSWORD: password
volumes:
- name: dieselcli
temp: {}
---
kind: pipeline
name: arm64
platform:
os: linux
arch: arm64
steps:
- name: cargo test
image: rust:1.50-slim-buster
environment:
LEMMY_DATABASE_URL: postgres://lemmy:password@database:5432/lemmy
RUST_BACKTRACE: 1
RUST_TEST_THREADS: 1
CARGO_HOME: /drone/src/.cargo
commands:
- apt-get update
- apt-get -y install --no-install-recommends espeak postgresql-client libssl-dev pkg-config libpq-dev
- cargo test --workspace --no-fail-fast
- cargo build
# Using Debian here because there seems to be no official Alpine-based Rust docker image for ARM.
- name: cargo build
image: rust:1.50-slim-buster
environment:
CARGO_HOME: /drone/src/.cargo
commands:
- apt-get update
- apt-get -y install --no-install-recommends libssl-dev pkg-config libpq-dev
- cargo build
- mv target/debug/lemmy_server target/lemmy_server
- name: run federation tests
image: node:15-buster-slim
environment:
LEMMY_DATABASE_URL: postgres://lemmy:password@database:5432
DO_WRITE_HOSTS_FILE: 1
commands:
- mkdir -p /usr/share/man/man1 /usr/share/man/man7
- apt-get update
- apt-get -y install --no-install-recommends bash curl libssl-dev pkg-config libpq-dev postgresql-client libc6-dev
- bash api_tests/prepare-drone-federation-test.sh
- cd api_tests/
- yarn
- yarn api-test
- name: make release build and push to docker hub
image: plugins/docker
settings:
dockerfile: docker/prod/Dockerfile.arm
username:
from_secret: docker_username
password:
from_secret: docker_password
repo: dessalines/lemmy
auto_tag: true
auto_tag_suffix: linux-arm64
when:
ref:
- refs/tags/*
- name: push to docker manifest
image: plugins/manifest
settings:
username:
from_secret: docker_username
password:
from_secret: docker_password
target: "dessalines/lemmy:${DRONE_TAG}"
template: "dessalines/lemmy:${DRONE_TAG}-OS-ARCH"
platforms:
- linux/amd64
- linux/arm64
ignore_missing: true
when:
ref:
- refs/tags/*
services:
- name: database
image: postgres:12-alpine
environment:
POSTGRES_USER: lemmy
POSTGRES_PASSWORD: password

4
.gitmodules vendored
View file

@ -1,4 +0,0 @@
[submodule "docs"]
path = docs
url = https://github.com/LemmyNet/lemmy-docs
branch = main

1
.rgignore Normal file
View file

@ -0,0 +1 @@
*.sqldump

View file

@ -1,5 +1,5 @@
tab_spaces = 2
edition="2018"
imports_layout="HorizontalVertical"
merge_imports=true
imports_granularity="Crate"
reorder_imports=true

View file

@ -1,35 +0,0 @@
# Code of Conduct
- We are committed to providing a friendly, safe and welcoming environment for all, regardless of level of experience, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, ethnicity, age, religion, nationality, or other similar characteristic.
- Please avoid using overtly sexual aliases or other nicknames that might detract from a friendly, safe and welcoming environment for all.
- Please be kind and courteous. Theres no need to be mean or rude.
- Respect that people have differences of opinion and that every design or implementation choice carries a trade-off and numerous costs. There is seldom a right answer.
- Please keep unstructured critique to a minimum. If you have solid ideas you want to experiment with, make a fork and see how it works.
- We will exclude you from interaction if you insult, demean or harass anyone. That is not welcome behavior. We interpret the term “harassment” as including the definition in the Citizen Code of Conduct; if you have any lack of clarity about what might be included in that concept, please read their definition. In particular, we dont tolerate behavior that excludes people in socially marginalized groups.
- Private harassment is also unacceptable. No matter who you are, if you feel you have been or are being harassed or made uncomfortable by a community member, please contact one of the channel ops or any of the Lemmy moderation team immediately. Whether youre a regular contributor or a newcomer, we care about making this community a safe place for you and weve got your back.
- Likewise any spamming, trolling, flaming, baiting or other attention-stealing behavior is not welcome.
[**Message the Moderation Team on Mastodon**](https://mastodon.social/@LemmyDev)
[**Email The Moderation Team**](mailto:contact@lemmy.ml)
## Moderation
These are the policies for upholding our communitys standards of conduct. If you feel that a thread needs moderation, please contact the Lemmy moderation team .
1. Remarks that violate the Lemmy standards of conduct, including hateful, hurtful, oppressive, or exclusionary remarks, are not allowed. (Cursing is allowed, but never targeting another user, and never in a hateful manner.)
2. Remarks that moderators find inappropriate, whether listed in the code of conduct or not, are also not allowed.
3. Moderators will first respond to such remarks with a warning, at the same time the offending content will likely be removed whenever possible.
4. If the warning is unheeded, the user will be “kicked,” i.e., kicked out of the communication channel to cool off.
5. If the user comes back and continues to make trouble, they will be banned, i.e., indefinitely excluded.
6. Moderators may choose at their discretion to un-ban the user if it was a first offense and they offer the offended party a genuine apology.
7. If a moderator bans someone and you think it was unjustified, please take it up with that moderator, or with a different moderator, in private. Complaints about bans in-channel are not allowed.
8. Moderators are held to a higher standard than other community members. If a moderator creates an inappropriate situation, they should expect less leeway than others.
In the Lemmy community we strive to go the extra step to look out for each other. Dont just aim to be technically unimpeachable, try to be your best self. In particular, avoid flirting with offensive or sensitive issues, particularly if theyre off-topic; this all too often leads to unnecessary fights, hurt feelings, and damaged trust; worse, it can drive people away from the community entirely.
And if someone takes issue with something you said or did, resist the urge to be defensive. Just stop doing what it was they complained about and apologize. Even if you feel you were misinterpreted or unfairly accused, chances are good there was something you couldve communicated better — remember that its your responsibility to make others comfortable. Everyone wants to get along and we are all here first and foremost because we want to talk about cool technology. You will find that people will be eager to assume good intent and forgive as long as you earn their trust.
The enforcement policies listed above apply to all official Lemmy venues; including git repositories under [github.com/LemmyNet/lemmy](https://github.com/LemmyNet/lemmy) and [yerbamate.ml/LemmyNet/lemmy](https://yerbamate.ml/LemmyNet/lemmy), the [Matrix channel](https://matrix.to/#/!BZVTUuEiNmRcbFeLeI:matrix.org?via=matrix.org&via=privacytools.io&via=permaweb.io); and all instances under lemmy.ml. For other projects adopting the Rust Code of Conduct, please contact the maintainers of those projects for enforcement. If you wish to use this code of conduct for your own project, consider explicitly mentioning your moderation policy or making a copy with your own moderation policy so as to avoid confusion.
Adapted from the [Rust Code of Conduct](https://www.rust-lang.org/policies/code-of-conduct), which is based on the [Node.js Policy on Trolling](http://blog.izs.me/post/30036893703/policy-on-trolling) as well as the [Contributor Covenant v1.3.0](https://www.contributor-covenant.org/version/1/3/0/).

View file

@ -1,4 +1,4 @@
# Contributing
See [here](https://lemmy.ml/docs/contributing.html) for contributing Instructions.
See [here](https://join.lemmy.ml/docs/en/contributing/contributing.html) for contributing Instructions.

1080
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -3,61 +3,58 @@ name = "lemmy_server"
version = "0.0.1"
edition = "2018"
[lib]
doctest = false
[profile.dev]
debug = 0
[profile.release]
lto = true
[workspace]
members = [
"lemmy_api",
"lemmy_apub",
"lemmy_utils",
"lemmy_db_queries",
"lemmy_db_schema",
"lemmy_db_views",
"lemmy_db_views_actor",
"lemmy_db_views_actor",
"lemmy_structs",
"lemmy_websocket",
"crates/api",
"crates/apub",
"crates/utils",
"crates/db_queries",
"crates/db_schema",
"crates/db_views",
"crates/db_views_actor",
"crates/db_views_actor",
"crates/api_structs",
"crates/websocket",
"crates/routes"
]
[dependencies]
lemmy_api = { path = "./lemmy_api" }
lemmy_apub = { path = "./lemmy_apub" }
lemmy_utils = { path = "./lemmy_utils" }
lemmy_db_schema = { path = "./lemmy_db_schema" }
lemmy_db_queries = { path = "lemmy_db_queries" }
lemmy_db_views = { path = "./lemmy_db_views" }
lemmy_db_views_moderator = { path = "./lemmy_db_views_moderator" }
lemmy_db_views_actor = { path = "lemmy_db_views_actor" }
lemmy_structs = { path = "./lemmy_structs" }
lemmy_websocket = { path = "./lemmy_websocket" }
lemmy_api = { path = "./crates/api" }
lemmy_apub = { path = "./crates/apub" }
lemmy_utils = { path = "./crates/utils" }
lemmy_db_schema = { path = "./crates/db_schema" }
lemmy_db_queries = { path = "./crates/db_queries" }
lemmy_db_views = { path = "./crates/db_views" }
lemmy_db_views_moderator = { path = "./crates/db_views_moderator" }
lemmy_db_views_actor = { path = "./crates/db_views_actor" }
lemmy_api_structs = { path = "crates/api_structs" }
lemmy_websocket = { path = "./crates/websocket" }
lemmy_routes = { path = "./crates/routes" }
diesel = "1.4.5"
diesel_migrations = "1.4.0"
chrono = { version = "0.4.19", features = ["serde"] }
serde = { version = "1.0.118", features = ["derive"] }
serde = { version = "1.0.123", features = ["derive"] }
actix = "0.10.0"
actix-web = { version = "3.3.2", default-features = false, features = ["rustls"] }
actix-files = { version = "0.4.1", default-features = false }
actix-web-actors = { version = "3.0.0", default-features = false }
awc = { version = "2.0.3", default-features = false }
log = "0.4.11"
log = "0.4.14"
env_logger = "0.8.2"
strum = "0.20.0"
lazy_static = "1.4.0"
rss = "1.9.0"
url = { version = "2.2.0", features = ["serde"] }
openssl = "0.10.31"
url = { version = "2.2.1", features = ["serde"] }
openssl = "0.10.32"
http-signature-normalization-actix = { version = "0.4.1", default-features = false, features = ["sha-2"] }
tokio = "0.3.6"
sha2 = "0.9.2"
anyhow = "1.0.36"
anyhow = "1.0.38"
reqwest = { version = "0.10.10", features = ["json"] }
activitystreams = "0.7.0-alpha.8"
activitystreams = "0.7.0-alpha.10"
actix-rt = { version = "1.1.1", default-features = false }
serde_json = { version = "1.0.60", features = ["preserve_order"] }
serde_json = { version = "1.0.61", features = ["preserve_order"] }
clokwerk = "0.3.4"
[dev-dependencies.cargo-husky]
version = "1.5.0"

View file

@ -1,12 +1,13 @@
<div align="center">
![GitHub tag (latest SemVer)](https://img.shields.io/github/tag/LemmyNet/lemmy.svg)
[![Build Status](https://travis-ci.org/LemmyNet/lemmy.svg?branch=main)](https://travis-ci.org/LemmyNet/lemmy)
[![Build Status](https://cloud.drone.io/api/badges/LemmyNet/lemmy/status.svg)](https://cloud.drone.io/LemmyNet/lemmy/)
[![GitHub issues](https://img.shields.io/github/issues-raw/LemmyNet/lemmy.svg)](https://github.com/LemmyNet/lemmy/issues)
[![Docker Pulls](https://img.shields.io/docker/pulls/dessalines/lemmy.svg)](https://cloud.docker.com/repository/docker/dessalines/lemmy/)
[![Translation status](http://weblate.yerbamate.ml/widgets/lemmy/-/lemmy/svg-badge.svg)](http://weblate.yerbamate.ml/engage/lemmy/)
[![License](https://img.shields.io/github/license/LemmyNet/lemmy.svg)](LICENSE)
![GitHub stars](https://img.shields.io/github/stars/LemmyNet/lemmy?style=social)
[![Awesome Humane Tech](https://raw.githubusercontent.com/humanetech-community/awesome-humane-tech/main/humane-tech-badge.svg?sanitize=true)](https://github.com/humanetech-community/awesome-humane-tech)
</div>
<p align="center">
@ -20,21 +21,23 @@
<br />
<a href="https://join.lemmy.ml">Join Lemmy</a>
·
<a href="https://lemmy.ml/docs/index.html">Documentation</a>
<a href="https://join.lemmy.ml/docs/en/index.html">Documentation</a>
·
<a href="https://github.com/LemmyNet/lemmy/issues">Report Bug</a>
·
<a href="https://github.com/LemmyNet/lemmy/issues">Request Feature</a>
·
<a href="https://github.com/LemmyNet/lemmy/blob/main/RELEASES.md">Releases</a>
·
<a href="https://join.lemmy.ml/docs/en/code_of_conduct.html">Code of Conduct</a>
</p>
</p>
## About The Project
Front Page|Post
Desktop|Mobile
---|---
![main screen](https://raw.githubusercontent.com/LemmyNet/lemmy/main/docs/img/main_screen.png)|![chat screen](https://raw.githubusercontent.com/LemmyNet/lemmy/main/docs/img/chat_screen.png)
![desktop](https://raw.githubusercontent.com/LemmyNet/joinlemmy-site/main/static/images/main_img.webp)|![mobile](https://raw.githubusercontent.com/LemmyNet/joinlemmy-site/main/static/images/mobile_pic.webp)
[Lemmy](https://github.com/LemmyNet/lemmy) is similar to sites like [Reddit](https://reddit.com), [Lobste.rs](https://lobste.rs), or [Hacker News](https://news.ycombinator.com/): you subscribe to forums you're interested in, post links and discussions, then vote, and comment on them. Behind the scenes, it is very different; anyone can easily run a server, and all these servers are federated (think email), and connected to the same universe, called the [Fediverse](https://en.wikipedia.org/wiki/Fediverse).
@ -44,7 +47,7 @@ The overall goal is to create an easily self-hostable, decentralized alternative
Each Lemmy server can set its own moderation policy; appointing site-wide admins, and community moderators to keep out the trolls, and foster a healthy, non-toxic environment where all can feel comfortable contributing.
*Note: Federation is still in active development and the WebSocket, as well as, HTTP API are currently unstable*
*Note: The WebSocket and HTTP APIs are currently unstable*
### Why's it called Lemmy?
@ -65,7 +68,7 @@ Each Lemmy server can set its own moderation policy; appointing site-wide admins
- Open source, [AGPL License](/LICENSE).
- Self hostable, easy to deploy.
- Comes with [Docker](https://lemmy.ml/docs/administration_install_docker.html) and [Ansible](https://lemmy.ml/docs/administration_install_ansible.html).
- Comes with [Docker](https://join.lemmy.ml/docs/en/administration/install_docker.html) and [Ansible](https://join.lemmy.ml/docs/en/administration/install_ansible.html).
- Clean, mobile-friendly interface.
- Only a minimum of a username and password is required to sign up!
- User avatar support.
@ -100,16 +103,16 @@ Each Lemmy server can set its own moderation policy; appointing site-wide admins
## Installation
- [Docker](https://lemmy.ml/docs/administration_install_docker.html)
- [Ansible](https://lemmy.ml/docs/administration_install_ansible.html)
- [Docker](https://join.lemmy.ml/docs/en/administration/install_docker.html)
- [Ansible](https://join.lemmy.ml/docs/en/administration/install_ansible.html)
## Lemmy Projects
### Apps
- [lemmy-ui - The official web app for lemmy](https://github.com/LemmyNet/lemmy-ui)
- [Lemmur - A flutter lemmy app ( under development )](https://github.com/krawieck/lemmur)
- [Lemmy-mobile (Android / IOS) - React native ( under development )](https://github.com/koredefashokun/lemmy-mobile)
- [Lemmur - A mobile client for Lemmy (Android, Linux, Windows)](https://github.com/krawieck/lemmur)
- [Remmel - A native iOS app](https://github.com/uuttff8/Lemmy-iOS)
### Libraries
@ -134,13 +137,13 @@ Lemmy is free, open-source software, meaning no advertising, monetizing, or vent
## Contributing
- [Contributing instructions](https://lemmy.ml/docs/contributing.html)
- [Docker Development](https://lemmy.ml/docs/contributing_docker_development.html)
- [Local Development](https://lemmy.ml/docs/contributing_local_development.html)
- [Contributing instructions](https://join.lemmy.ml/docs/en/contributing/contributing.html)
- [Docker Development](https://join.lemmy.ml/docs/en/contributing/docker_development.html)
- [Local Development](https://join.lemmy.ml/docs/en/contributing/local_development.html)
### Translations
If you want to help with translating, take a look at [Weblate](https://weblate.yerbamate.ml/projects/lemmy/).
If you want to help with translating, take a look at [Weblate](https://weblate.yerbamate.ml/projects/lemmy/). You can also help by [translating the documentation](https://github.com/LemmyNet/lemmy-docs#adding-a-new-language).
## Contact

View file

@ -1,3 +1,126 @@
# Lemmy v0.9.9 Release (2021-02-19)
## Changes
### Lemmy backend
- Added an federated activity query sorting order.
- Explicitly marking posts and comments as public.
- Added a `NewComment` / forum sort for posts.
- Fixed an issue with not setting correct published time for fetched posts.
- Fixed an issue with an open docker port on lemmy-ui.
- Using lemmy post link for RSS link.
- Fixed reason and display name lengths to use char counts instead.
### Lemmy-ui
- Updated translations.
- Made websocket host configurable.
- Added some accessibility features.
- Always showing password reset link.
# Lemmy v0.9.7 Release (2021-02-08)
## Changes
- Posts and comments are no longer live-sorted (meaning most content should stay in place).
- Fixed an issue with the create post title field not expanding when copied from iframely suggestion.
- Fixed broken federated community paging / sorting.
- Added aria attributes for accessibility, thx to @Mitch Lillie.
- Updated translations and added croatian.
- No changes to lemmy back-end.
# Lemmy v0.9.6 Release (2021-02-05)
## Changes
- Fixed inbox_urls not being correctly set, which broke federation in `v0.9.5`. Added some logging to catch these.
- Fixing community search not using auth.
- Moved docs to https://join.lemmy.ml
- Fixed an issue w/ lemmy-ui with forms being cleared out.
# Lemmy v0.9.4 Pre-Release (2021-02-02)
## Changes
### Lemmy
- Fixed a critical bug with votes and comment unlike responses not being `0` for your user.
- Fixed a critical bug with comment creation not checking if its parent comment is in the post.
- Serving proper activities for community outbox.
- Added some active user counts, including `users_active_day`, `users_active_week`, `users_active_month`, `users_active_half_year` to `SiteAggregates` and `CommunityAggregates`. (Also added to lemmy-ui)
- Made sure banned users can't follow.
- Added `FederatedInstances` to `SiteResponse`, to show allowed and blocked instances. (Also added to lemmy-ui)
- Added a `MostComments` sort for posts. (Also added to lemmy-ui)
### Lemmy-UI
- Added a scroll position restore to lemmy-ui.
- Reworked the combined inbox so incoming comments don't wipe out your current form.
- Fixed an updated bug on the user page.
- Fixed cross-post titles and body getting clipped.
- Fixing the post creation title height.
- Squashed some other smaller bugs.
# Lemmy v0.9.0 Release (2021-01-25)
## Changes
Since our last release in October of last year, and we've had [~450](https://github.com/LemmyNet/lemmy/compare/v0.8.0...main) commits.
The biggest changes, as we'll outline below, are a re-work of Lemmy's database structure, a `v2` of Lemmy's API, and activitypub compliance fixes. The new re-worked DB is much faster, easier to maintain, and [now supports hierarchical rather than flat objects in the new API](https://github.com/LemmyNet/lemmy/issues/1275).
We've also seen the first release of [Lemmur](https://github.com/krawieck/lemmur/releases/tag/v0.1.1), an android / iOS (soon) / windows / linux client, as well as [Lemmer](https://github.com/uuttff8/Lemmy-iOS), a native iOS client. Much thanks to @krawieck, @shilangyu, and @uuttff8 for making these great clients. If you can, please contribute to their [patreon](https://www.patreon.com/lemmur) to help fund lemmur development.
## LemmyNet projects
### Lemmy Server
- [Moved views from SQL to Diesel](https://github.com/LemmyNet/lemmy/issues/1275). This was a spinal replacement for much of lemmy.
- Removed all the old fast_tables and triggers, and created new aggregates tables.
- Added a `v2` of the API to support the hierarchical objects created from the above changes.
- Moved continuous integration to [drone](https://cloud.drone.io/LemmyNet/lemmy/), now includes formatting, clippy, and cargo build checks, unit testing, and federation testing. [Drone also deploys both amd64 and arm64 images to dockerhub.](https://hub.docker.com/r/dessalines/lemmy)
- Split out documentation into git submodule.
- Shortened slur filter to avoid false positives.
- Added query performance testing and comparisons. Added indexes to make sure every query is `< 30 ms`.
- Added compilation time testing.
### Federation
This release includes some bug fixes for federation, and some changes to get us closer to compliance with the ActivityPub standard.
- [Community bans now federating](https://github.com/LemmyNet/lemmy/issues/1287).
- [Local posts sometimes got marked as remote](https://github.com/LemmyNet/lemmy/issues/1302).
- [Creator of post/comment was not notified about new child comments](https://github.com/LemmyNet/lemmy/issues/1325).
- [Community deletion now federated](https://github.com/LemmyNet/lemmy/issues/1256).
None of these are breaking changes, so federation between 0.9.0 and 0.8.11 will work without problems.
### Lemmy javascript / typescript client
- Updated the [lemmy-js-client](https://github.com/LemmyNet/lemmy-js-client) to use the new `v2` API. Our API docs now reference this project's files, to show what the http / websocket forms and responses should look like.
- Drone now handles publishing its [npm packages.](https://www.npmjs.com/package/lemmy-js-client)
### Lemmy-UI
- Updated it to use the `v2` API via `lemmy-js-client`, required changing nearly every component.
- Added a live comment count.
- Added drone deploying, and builds for ARM.
- Fixed community link wrapping.
- Various other bug fixes.
### Lemmy Docs
- We moved documentation into a separate git repository, and support translation for the docs now!
- Moved our code of conduct into the documentation.
## Upgrading
If you'd like to make a DB backup before upgrading, follow [this guide](https://join.lemmy.ml/docs/en/administration/backup_and_restore.html).
- [Upgrade with manual Docker installation](https://join.lemmy.ml/docs/en/administration/install_docker.html#updating)
- [Upgrade with Ansible installation](https://join.lemmy.ml/docs/en/administration/install_ansible.html)
# Lemmy v0.8.0 Release (2020-10-16)
## Changes
@ -20,7 +143,7 @@ Here are some of the bigger changes:
- The first **federation public beta release**, woohoo :fireworks:
- All Lemmy functionality now works over ActivityPub (except turning remote users into mods/admins)
- Instance allowlist and blocklist
- Documentation for [admins](https://lemmy.ml/docs/administration_federation.html) and [devs](https://lemmy.ml/docs/contributing_federation_overview.html) on how federation works
- Documentation for [admins](https://join.lemmy.ml/docs/administration_federation.html) and [devs](https://join.lemmy.ml/docs/contributing_federation_overview.html) on how federation works
- Upgraded to newest versions of @asonix activitypub libraries
- Full local federation setup for manual testing
- Automated testing for nearly every federation action
@ -58,8 +181,8 @@ We'd also like to thank both the [NLnet foundation](https://nlnet.nl/) for their
## Upgrading
- [with manual Docker installation](https://lemmy.ml/docs/administration_install_docker.html#updating)
- [with Ansible installation](https://lemmy.ml/docs/administration_install_ansible.html)
- [with manual Docker installation](https://join.lemmy.ml/docs/administration_install_docker.html#updating)
- [with Ansible installation](https://join.lemmy.ml/docs/administration_install_ansible.html)
## Testing Federation
@ -147,7 +270,7 @@ Overall, since our last major release in January (v0.6.0), we have closed over
Before starting the upgrade, make sure that you have a working backup of your
database and image files. See our
[documentation](https://lemmy.ml/docs/administration_backup_and_restore.html)
[documentation](https://join.lemmy.ml/docs/administration_backup_and_restore.html)
for backup instructions.
**With Ansible:**

View file

@ -1 +1 @@
v0.9.0-rc.2
0.10.0-rc.7

View file

@ -64,6 +64,14 @@
- src: '../docker/iframely.config.local.js'
dest: '{{lemmy_base_dir}}/iframely.config.local.js'
mode: '0600'
vars:
lemmy_docker_image: "dessalines/lemmy:dev"
lemmy_docker_ui_image: "dessalines/lemmy-ui:{{ lookup('file', 'VERSION') }}"
lemmy_port: "8536"
lemmy_ui_port: "1235"
pictshare_port: "8537"
iframely_port: "8538"
postgres_password: "{{ lookup('password', 'passwords/{{ inventory_hostname }}/postgres chars=ascii_letters,digits') }}"
- name: add config file (only during initial setup)
template:

View file

@ -1,6 +1,6 @@
{
# for more info about the config, check out the documentation
# https://lemmy.ml/docs/administration_configuration.html
# https://join.lemmy.ml/docs/en/administration/configuration.html
# settings related to the postgresql database
database: {
@ -26,11 +26,12 @@
# whether to enable activitypub federation.
enabled: false
# Allows and blocks are described here:
# https://lemmy.ml/docs/administration_federation.html#instance-allowlist-and-blocklist
# https://join.lemmy.ml/docs/en/federation/administration.html#instance-allowlist-and-blocklist
#
# comma separated list of instances with which federation is allowed
# allowed_instances: ""
# Only one of these blocks should be uncommented
# allowed_instances: ["instance1.tld","instance2.tld"]
# comma separated list of instances which are blocked from federating
# blocked_instances: ""
# blocked_instances: []
}
}

View file

@ -1,4 +1,4 @@
version: '3.3'
version: '2.2'
services:
lemmy:
@ -38,13 +38,14 @@ services:
restart: always
pictrs:
image: asonix/pictrs:v0.2.5-r0
image: asonix/pictrs:v0.2.6-r1
user: 991:991
ports:
- "127.0.0.1:8537:8080"
volumes:
- ./volumes/pictrs:/mnt
restart: always
mem_limit: 200m
iframely:
image: dogbin/iframely:latest
@ -53,6 +54,7 @@ services:
volumes:
- ./iframely.config.local.js:/iframely/config.local.js:ro
restart: always
mem_limit: 200m
postfix:
image: mwader/postfix-relay

View file

@ -78,7 +78,7 @@ server {
}
# backend
location ~ ^/(api|docs|pictrs|feeds|nodeinfo|.well-known) {
location ~ ^/(api|pictrs|feeds|nodeinfo|.well-known) {
proxy_pass http://0.0.0.0:{{ lemmy_port }};
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;

View file

@ -12,14 +12,14 @@
"api-test": "jest src/ -i --verbose"
},
"devDependencies": {
"@types/jest": "^26.0.19",
"jest": "^26.6.3",
"lemmy-js-client": "1.0.17-beta6",
"node-fetch": "^2.6.1",
"ts-jest": "^26.4.4",
"prettier": "^2.1.2",
"eslint": "^7.10.0",
"@types/jest": "^26.0.20",
"eslint": "^7.18.0",
"eslint-plugin-jane": "^9.0.3",
"jest": "^26.6.3",
"lemmy-js-client": "0.10.0-rc.4",
"node-fetch": "^2.6.1",
"prettier": "^2.1.2",
"ts-jest": "^26.4.4",
"typescript": "^4.1.3"
}
}

View file

@ -1,13 +1,6 @@
#!/bin/bash
set -e
export LEMMY_JWT_SECRET=changeme
export LEMMY_FEDERATION__ENABLED=true
export LEMMY_TLS_ENABLED=false
export LEMMY_SETUP__ADMIN_PASSWORD=lemmy
export LEMMY_RATE_LIMIT__POST=99999
export LEMMY_RATE_LIMIT__REGISTER=99999
export LEMMY_CAPTCHA__ENABLED=false
export LEMMY_TEST_SEND_SYNC=1
export RUST_BACKTRACE=1
@ -35,52 +28,40 @@ fi
killall lemmy_server || true
echo "$PWD"
echo "start alpha"
LEMMY_HOSTNAME=lemmy-alpha:8541 \
LEMMY_PORT=8541 \
LEMMY_CONFIG_LOCATION=./docker/federation/lemmy_alpha.hjson \
LEMMY_DATABASE_URL="${LEMMY_DATABASE_URL}/lemmy_alpha" \
LEMMY_FEDERATION__ALLOWED_INSTANCES=lemmy-beta,lemmy-gamma,lemmy-delta,lemmy-epsilon \
LEMMY_SETUP__ADMIN_USERNAME=lemmy_alpha \
LEMMY_SETUP__SITE_NAME=lemmy-alpha \
target/lemmy_server >/dev/null 2>&1 &
LEMMY_HOSTNAME="lemmy-alpha:8541" \
target/lemmy_server >/tmp/lemmy_alpha.out 2>&1 &
echo "start beta"
LEMMY_HOSTNAME=lemmy-beta:8551 \
LEMMY_PORT=8551 \
LEMMY_CONFIG_LOCATION=./docker/federation/lemmy_beta.hjson \
LEMMY_DATABASE_URL="${LEMMY_DATABASE_URL}/lemmy_beta" \
LEMMY_FEDERATION__ALLOWED_INSTANCES=lemmy-alpha,lemmy-gamma,lemmy-delta,lemmy-epsilon \
LEMMY_SETUP__ADMIN_USERNAME=lemmy_beta \
LEMMY_SETUP__SITE_NAME=lemmy-beta \
target/lemmy_server >/dev/null 2>&1 &
target/lemmy_server >/tmp/lemmy_beta.out 2>&1 &
echo "start gamma"
LEMMY_HOSTNAME=lemmy-gamma:8561 \
LEMMY_PORT=8561 \
LEMMY_CONFIG_LOCATION=./docker/federation/lemmy_gamma.hjson \
LEMMY_DATABASE_URL="${LEMMY_DATABASE_URL}/lemmy_gamma" \
LEMMY_FEDERATION__ALLOWED_INSTANCES=lemmy-alpha,lemmy-beta,lemmy-delta,lemmy-epsilon \
LEMMY_SETUP__ADMIN_USERNAME=lemmy_gamma \
LEMMY_SETUP__SITE_NAME=lemmy-gamma \
target/lemmy_server >/dev/null 2>&1 &
target/lemmy_server >/tmp/lemmy_gamma.out 2>&1 &
echo "start delta"
# An instance with only an allowlist for beta
LEMMY_HOSTNAME=lemmy-delta:8571 \
LEMMY_PORT=8571 \
LEMMY_CONFIG_LOCATION=./docker/federation/lemmy_delta.hjson \
LEMMY_DATABASE_URL="${LEMMY_DATABASE_URL}/lemmy_delta" \
LEMMY_FEDERATION__ALLOWED_INSTANCES=lemmy-beta \
LEMMY_SETUP__ADMIN_USERNAME=lemmy_delta \
LEMMY_SETUP__SITE_NAME=lemmy-delta \
target/lemmy_server >/dev/null 2>&1 &
target/lemmy_server >/tmp/lemmy_delta.out 2>&1 &
echo "start epsilon"
# An instance who has a blocklist, with lemmy-alpha blocked
LEMMY_HOSTNAME=lemmy-epsilon:8581 \
LEMMY_PORT=8581 \
LEMMY_CONFIG_LOCATION=./docker/federation/lemmy_epsilon.hjson \
LEMMY_DATABASE_URL="${LEMMY_DATABASE_URL}/lemmy_epsilon" \
LEMMY_FEDERATION__BLOCKED_INSTANCES=lemmy-alpha \
LEMMY_SETUP__ADMIN_USERNAME=lemmy_epsilon \
LEMMY_SETUP__SITE_NAME=lemmy-epsilon \
target/lemmy_server >/dev/null 2>&1 &
target/lemmy_server >/tmp/lemmy_epsilon.out 2>&1 &
echo "wait for all instances to start"
while [[ "$(curl -s -o /dev/null -w '%{http_code}' 'localhost:8541/api/v2/site')" != "200" ]]; do sleep 1; done

View file

@ -4,7 +4,7 @@ set -e
export LEMMY_DATABASE_URL=postgres://lemmy:password@localhost:5432
pushd ..
cargo +1.47.0 build
cargo build
rm target/lemmy_server || true
cp target/debug/lemmy_server target/lemmy_server
./api_tests/prepare-drone-federation-test.sh

View file

@ -33,9 +33,6 @@ function assertCommunityFederation(
);
expect(communityOne.creator.actor_id).toBe(communityTwo.creator.actor_id);
expect(communityOne.community.nsfw).toBe(communityTwo.community.nsfw);
expect(communityOne.community.category_id).toBe(
communityTwo.community.category_id
);
expect(communityOne.community.removed).toBe(communityTwo.community.removed);
expect(communityOne.community.deleted).toBe(communityTwo.community.deleted);
}

View file

@ -20,9 +20,9 @@ import {
getPost,
unfollowRemotes,
searchForUser,
banUserFromSite,
banPersonFromSite,
searchPostLocal,
banUserFromCommunity,
banPersonFromCommunity,
} from './shared';
import { PostView, CommunityView } from 'lemmy-js-client';
@ -305,7 +305,7 @@ test('Enforce site ban for federated user', async () => {
expect(alphaUser).toBeDefined();
// ban alpha from beta site
let banAlpha = await banUserFromSite(beta, alphaUser.user.id, true);
let banAlpha = await banPersonFromSite(beta, alphaUser.person.id, true);
expect(banAlpha.banned).toBe(true);
// Alpha makes post on beta
@ -321,7 +321,7 @@ test('Enforce site ban for federated user', async () => {
expect(betaPost).toBeUndefined();
// Unban alpha
let unBanAlpha = await banUserFromSite(beta, alphaUser.user.id, false);
let unBanAlpha = await banPersonFromSite(beta, alphaUser.person.id, false);
expect(unBanAlpha.banned).toBe(false);
});
@ -332,8 +332,8 @@ test('Enforce community ban for federated user', async () => {
expect(alphaUser).toBeDefined();
// ban alpha from beta site
await banUserFromCommunity(beta, alphaUser.user.id, 2, false);
let banAlpha = await banUserFromCommunity(beta, alphaUser.user.id, 2, true);
await banPersonFromCommunity(beta, alphaUser.person.id, 2, false);
let banAlpha = await banPersonFromCommunity(beta, alphaUser.person.id, 2, true);
expect(banAlpha.banned).toBe(true);
// Alpha makes post on beta
@ -349,9 +349,9 @@ test('Enforce community ban for federated user', async () => {
expect(betaPost).toBeUndefined();
// Unban alpha
let unBanAlpha = await banUserFromCommunity(
let unBanAlpha = await banPersonFromCommunity(
beta,
alphaUser.user.id,
alphaUser.person.id,
2,
false
);

View file

@ -25,7 +25,7 @@ import {
CreateCommunity,
DeleteCommunity,
RemoveCommunity,
GetUserMentions,
GetPersonMentions,
CreateCommentLike,
CreatePostLike,
EditPrivateMessage,
@ -36,15 +36,15 @@ import {
GetPost,
PrivateMessageResponse,
PrivateMessagesResponse,
GetUserMentionsResponse,
GetPersonMentionsResponse,
SaveUserSettings,
SortType,
ListingType,
GetSiteResponse,
SearchType,
LemmyHttp,
BanUserResponse,
BanUser,
BanPersonResponse,
BanPerson,
BanFromCommunity,
BanFromCommunityResponse,
Post,
@ -144,7 +144,7 @@ export async function editPost(api: API, post: Post): Promise<PostResponse> {
let name = 'A jest test federated post, updated';
let form: EditPost = {
name,
edit_id: post.id,
post_id: post.id,
auth: api.auth,
nsfw: false,
};
@ -157,7 +157,7 @@ export async function deletePost(
post: Post
): Promise<PostResponse> {
let form: DeletePost = {
edit_id: post.id,
post_id: post.id,
deleted: deleted,
auth: api.auth,
};
@ -170,7 +170,7 @@ export async function removePost(
post: Post
): Promise<PostResponse> {
let form: RemovePost = {
edit_id: post.id,
post_id: post.id,
removed,
auth: api.auth,
};
@ -183,7 +183,7 @@ export async function stickyPost(
post: Post
): Promise<PostResponse> {
let form: StickyPost = {
edit_id: post.id,
post_id: post.id,
stickied,
auth: api.auth,
};
@ -196,7 +196,7 @@ export async function lockPost(
post: Post
): Promise<PostResponse> {
let form: LockPost = {
edit_id: post.id,
post_id: post.id,
locked,
auth: api.auth,
};
@ -289,32 +289,32 @@ export async function searchForUser(
return api.client.search(form);
}
export async function banUserFromSite(
export async function banPersonFromSite(
api: API,
user_id: number,
person_id: number,
ban: boolean
): Promise<BanUserResponse> {
): Promise<BanPersonResponse> {
// Make sure lemmy-beta/c/main is cached on lemmy_alpha
// Use short-hand search url
let form: BanUser = {
user_id,
let form: BanPerson = {
person_id,
ban,
remove_data: false,
auth: api.auth,
};
return api.client.banUser(form);
return api.client.banPerson(form);
}
export async function banUserFromCommunity(
export async function banPersonFromCommunity(
api: API,
user_id: number,
person_id: number,
community_id: number,
ban: boolean
): Promise<BanFromCommunityResponse> {
// Make sure lemmy-beta/c/main is cached on lemmy_alpha
// Use short-hand search url
let form: BanFromCommunity = {
user_id,
person_id,
community_id,
remove_data: false,
ban,
@ -376,12 +376,12 @@ export async function createComment(
export async function editComment(
api: API,
edit_id: number,
comment_id: number,
content = 'A jest test federated comment update'
): Promise<CommentResponse> {
let form: EditComment = {
content,
edit_id,
comment_id,
auth: api.auth,
};
return api.client.editComment(form);
@ -390,10 +390,10 @@ export async function editComment(
export async function deleteComment(
api: API,
deleted: boolean,
edit_id: number
comment_id: number
): Promise<CommentResponse> {
let form: DeleteComment = {
edit_id,
comment_id,
deleted,
auth: api.auth,
};
@ -403,23 +403,23 @@ export async function deleteComment(
export async function removeComment(
api: API,
removed: boolean,
edit_id: number
comment_id: number
): Promise<CommentResponse> {
let form: RemoveComment = {
edit_id,
comment_id,
removed,
auth: api.auth,
};
return api.client.removeComment(form);
}
export async function getMentions(api: API): Promise<GetUserMentionsResponse> {
let form: GetUserMentions = {
export async function getMentions(api: API): Promise<GetPersonMentionsResponse> {
let form: GetPersonMentions = {
sort: SortType.New,
unread_only: false,
auth: api.auth,
};
return api.client.getUserMentions(form);
return api.client.getPersonMentions(form);
}
export async function likeComment(
@ -448,7 +448,6 @@ export async function createCommunity(
description,
icon,
banner,
category_id: 1,
nsfw: false,
auth: api.auth,
};
@ -468,10 +467,10 @@ export async function getCommunity(
export async function deleteCommunity(
api: API,
deleted: boolean,
edit_id: number
community_id: number
): Promise<CommunityResponse> {
let form: DeleteCommunity = {
edit_id,
community_id,
deleted,
auth: api.auth,
};
@ -481,10 +480,10 @@ export async function deleteCommunity(
export async function removeCommunity(
api: API,
removed: boolean,
edit_id: number
community_id: number
): Promise<CommunityResponse> {
let form: RemoveCommunity = {
edit_id,
community_id,
removed,
auth: api.auth,
};
@ -506,12 +505,12 @@ export async function createPrivateMessage(
export async function editPrivateMessage(
api: API,
edit_id: number
private_message_id: number
): Promise<PrivateMessageResponse> {
let updatedContent = 'A jest test federated private message edited';
let form: EditPrivateMessage = {
content: updatedContent,
edit_id,
private_message_id,
auth: api.auth,
};
return api.client.editPrivateMessage(form);
@ -520,11 +519,11 @@ export async function editPrivateMessage(
export async function deletePrivateMessage(
api: API,
deleted: boolean,
edit_id: number
private_message_id: number
): Promise<PrivateMessageResponse> {
let form: DeletePrivateMessage = {
deleted,
edit_id,
private_message_id,
auth: api.auth,
};
return api.client.deletePrivateMessage(form);
@ -538,7 +537,6 @@ export async function registerUser(
username,
password: 'test',
password_verify: 'test',
admin: false,
show_nsfw: true,
};
return api.client.register(form);

View file

@ -8,7 +8,7 @@ import {
getSite,
} from './shared';
import {
UserViewSafe,
PersonViewSafe,
SaveUserSettings,
SortType,
ListingType,
@ -17,14 +17,14 @@ import {
let auth: string;
let apShortname: string;
function assertUserFederation(userOne: UserViewSafe, userTwo: UserViewSafe) {
expect(userOne.user.name).toBe(userTwo.user.name);
expect(userOne.user.preferred_username).toBe(userTwo.user.preferred_username);
expect(userOne.user.bio).toBe(userTwo.user.bio);
expect(userOne.user.actor_id).toBe(userTwo.user.actor_id);
expect(userOne.user.avatar).toBe(userTwo.user.avatar);
expect(userOne.user.banner).toBe(userTwo.user.banner);
expect(userOne.user.published).toBe(userTwo.user.published);
function assertUserFederation(userOne: PersonViewSafe, userTwo: PersonViewSafe) {
expect(userOne.person.name).toBe(userTwo.person.name);
expect(userOne.person.preferred_username).toBe(userTwo.person.preferred_username);
expect(userOne.person.bio).toBe(userTwo.person.bio);
expect(userOne.person.actor_id).toBe(userTwo.person.actor_id);
expect(userOne.person.avatar).toBe(userTwo.person.avatar);
expect(userOne.person.banner).toBe(userTwo.person.banner);
expect(userOne.person.published).toBe(userTwo.person.published);
}
test('Create user', async () => {
@ -34,7 +34,7 @@ test('Create user', async () => {
let site = await getSite(alpha, auth);
expect(site.my_user).toBeDefined();
apShortname = `@${site.my_user.name}@lemmy-alpha:8541`;
apShortname = `@${site.my_user.person.name}@lemmy-alpha:8541`;
});
test('Set some user settings, check that they are federated', async () => {

View file

@ -293,10 +293,10 @@
exec-sh "^0.3.2"
minimist "^1.2.0"
"@eslint/eslintrc@^0.2.2":
version "0.2.2"
resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-0.2.2.tgz#d01fc791e2fc33e88a29d6f3dc7e93d0cd784b76"
integrity sha512-EfB5OHNYp1F4px/LI/FEnGylop7nOqkQ1LRzCM0KccA2U8tvV8w01KBv37LbO7nW4H+YhKyo2LcJhRwjjV17QQ==
"@eslint/eslintrc@^0.3.0":
version "0.3.0"
resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-0.3.0.tgz#d736d6963d7003b6514e6324bec9c602ac340318"
integrity sha512-1JTKgrOKAHVivSvOYw+sJOunkBjUOvjqWk1DPja7ZFhIS2mX/4EgTT8M7eTK9jrKhL/FvXXEbQwIs3pg1xp3dg==
dependencies:
ajv "^6.12.4"
debug "^4.1.1"
@ -305,7 +305,7 @@
ignore "^4.0.6"
import-fresh "^3.2.1"
js-yaml "^3.13.1"
lodash "^4.17.19"
lodash "^4.17.20"
minimatch "^3.0.4"
strip-json-comments "^3.1.1"
@ -590,7 +590,7 @@
dependencies:
"@types/istanbul-lib-report" "*"
"@types/jest@26.x", "@types/jest@^26.0.19":
"@types/jest@26.x":
version "26.0.19"
resolved "https://registry.yarnpkg.com/@types/jest/-/jest-26.0.19.tgz#e6fa1e3def5842ec85045bd5210e9bb8289de790"
integrity sha512-jqHoirTG61fee6v6rwbnEuKhpSKih0tuhqeFbCmMmErhtu3BYlOZaXWjffgOstMM4S/3iQD31lI5bGLTrs97yQ==
@ -598,6 +598,14 @@
jest-diff "^26.0.0"
pretty-format "^26.0.0"
"@types/jest@^26.0.20":
version "26.0.20"
resolved "https://registry.yarnpkg.com/@types/jest/-/jest-26.0.20.tgz#cd2f2702ecf69e86b586e1f5223a60e454056307"
integrity sha512-9zi2Y+5USJRxd0FsahERhBwlcvFh6D2GLQnY2FH2BzK8J9s9omvNHIbvABwIluXa0fD8XVKMLTO0aOEuUfACAA==
dependencies:
jest-diff "^26.0.0"
pretty-format "^26.0.0"
"@types/json-schema@^7.0.3":
version "7.0.6"
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.6.tgz#f4c7ec43e81b319a9815115031709f26987891f0"
@ -1825,13 +1833,13 @@ eslint-visitor-keys@^2.0.0:
resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-2.0.0.tgz#21fdc8fbcd9c795cc0321f0563702095751511a8"
integrity sha512-QudtT6av5WXels9WjIM7qz1XD1cWGvX4gGXvp/zBn9nXG02D0utdU3Em2m/QjTnrsk6bBjmCygl3rmj118msQQ==
eslint@^7.10.0:
version "7.17.0"
resolved "https://registry.yarnpkg.com/eslint/-/eslint-7.17.0.tgz#4ccda5bf12572ad3bf760e6f195886f50569adb0"
integrity sha512-zJk08MiBgwuGoxes5sSQhOtibZ75pz0J35XTRlZOk9xMffhpA9BTbQZxoXZzOl5zMbleShbGwtw+1kGferfFwQ==
eslint@^7.18.0:
version "7.18.0"
resolved "https://registry.yarnpkg.com/eslint/-/eslint-7.18.0.tgz#7fdcd2f3715a41fe6295a16234bd69aed2c75e67"
integrity sha512-fbgTiE8BfUJZuBeq2Yi7J3RB3WGUQ9PNuNbmgi6jt9Iv8qrkxfy19Ds3OpL1Pm7zg3BtTVhvcUZbIRQ0wmSjAQ==
dependencies:
"@babel/code-frame" "^7.0.0"
"@eslint/eslintrc" "^0.2.2"
"@eslint/eslintrc" "^0.3.0"
ajv "^6.10.0"
chalk "^4.0.0"
cross-spawn "^7.0.2"
@ -1855,7 +1863,7 @@ eslint@^7.10.0:
js-yaml "^3.13.1"
json-stable-stringify-without-jsonify "^1.0.1"
levn "^0.4.1"
lodash "^4.17.19"
lodash "^4.17.20"
minimatch "^3.0.4"
natural-compare "^1.4.0"
optionator "^0.9.1"
@ -3225,10 +3233,10 @@ language-tags@^1.0.5:
dependencies:
language-subtag-registry "~0.3.2"
lemmy-js-client@1.0.17-beta6:
version "1.0.17-beta6"
resolved "https://registry.yarnpkg.com/lemmy-js-client/-/lemmy-js-client-1.0.17-beta6.tgz#afe1e1da13172a161c4d976b1ee58fe81eb22829"
integrity sha512-+oX7J7wht8nH4a5NQngK1GNner3TDv6ZOhQQVI5KcK7vynVVIcgveC5KBJArHBAl5acXpLs3Khmx0ZEb+sErJA==
lemmy-js-client@0.10.0-rc.4:
version "0.10.0-rc.4"
resolved "https://registry.yarnpkg.com/lemmy-js-client/-/lemmy-js-client-0.10.0-rc.4.tgz#ac6fe6940fc5f73260ddb166ce0ef3c0520901fc"
integrity sha512-yJPnvGaWneOOwjKEqb4qXtQk+4DbRgO+hEzSin2GgUgnxluY43gemwiCPt6EnV+j4ueKoi0+QORVg2RuRC2PaQ==
leven@^3.1.0:
version "3.1.0"

View file

@ -1,7 +0,0 @@
#!/bin/sh
cargo update
cargo fmt
cargo check
cargo clippy
cargo outdated -R

View file

@ -35,8 +35,6 @@
tls_enabled: true
# json web token for authorization between server and client
jwt_secret: "changeme"
# path to built documentation
docs_dir: "/app/documentation"
# address where pictrs is available
pictrs_url: "http://pictrs:8080"
# address where iframely is available
@ -65,12 +63,13 @@
# whether to enable activitypub federation.
enabled: false
# Allows and blocks are described here:
# https://lemmy.ml/docs/administration_federation.html#instance-allowlist-and-blocklist
# https://join.lemmy.ml/docs/en/federation/administration.html#instance-allowlist-and-blocklist
#
# comma separated list of instances with which federation is allowed
allowed_instances: ""
# Only one of these blocks should be uncommented
# allowed_instances: ["instance1.tld","instance2.tld"]
# comma separated list of instances which are blocked from federating
blocked_instances: ""
# blocked_instances: []
}
captcha: {
enabled: true

50
crates/api/Cargo.toml Normal file
View file

@ -0,0 +1,50 @@
[package]
name = "lemmy_api"
version = "0.1.0"
edition = "2018"
[lib]
name = "lemmy_api"
path = "src/lib.rs"
doctest = false
[dependencies]
lemmy_apub = { path = "../apub" }
lemmy_utils = { path = "../utils" }
lemmy_db_queries = { path = "../db_queries" }
lemmy_db_schema = { path = "../db_schema" }
lemmy_db_views = { path = "../db_views" }
lemmy_db_views_moderator = { path = "../db_views_moderator" }
lemmy_db_views_actor = { path = "../db_views_actor" }
lemmy_api_structs = { path = "../api_structs" }
lemmy_websocket = { path = "../websocket" }
diesel = "1.4.5"
bcrypt = "0.9.0"
chrono = { version = "0.4.19", features = ["serde"] }
serde_json = { version = "1.0.61", features = ["preserve_order"] }
serde = { version = "1.0.123", features = ["derive"] }
actix = "0.10.0"
actix-web = { version = "3.3.2", default-features = false }
actix-rt = { version = "1.1.1", default-features = false }
awc = { version = "2.0.3", default-features = false }
log = "0.4.14"
rand = "0.8.3"
strum = "0.20.0"
strum_macros = "0.20.1"
lazy_static = "1.4.0"
url = { version = "2.2.1", features = ["serde"] }
openssl = "0.10.32"
http = "0.2.3"
http-signature-normalization-actix = { version = "0.4.1", default-features = false, features = ["sha-2"] }
base64 = "0.13.0"
tokio = "0.3.6"
futures = "0.3.12"
itertools = "0.10.0"
uuid = { version = "0.8.2", features = ["serde", "v4"] }
sha2 = "0.9.3"
async-trait = "0.1.42"
captcha = "0.0.8"
anyhow = "1.0.38"
thiserror = "1.0.23"
background-jobs = "0.8.0"
reqwest = { version = "0.10.10", features = ["json"] }

View file

@ -2,14 +2,15 @@ use crate::{
check_community_ban,
check_downvotes_enabled,
collect_moderated_communities,
get_local_user_view_from_jwt,
get_local_user_view_from_jwt_opt,
get_post,
get_user_from_jwt,
get_user_from_jwt_opt,
is_mod_or_admin,
Perform,
};
use actix_web::web::Data;
use lemmy_apub::{ApubLikeableType, ApubObjectType};
use lemmy_api_structs::{blocking, comment::*, send_local_notifs};
use lemmy_apub::{generate_apub_endpoint, ApubLikeableType, ApubObjectType, EndpointType};
use lemmy_db_queries::{
source::comment::Comment_,
Crud,
@ -19,16 +20,18 @@ use lemmy_db_queries::{
Saveable,
SortType,
};
use lemmy_db_schema::source::{comment::*, comment_report::*, moderator::*};
use lemmy_db_schema::{
source::{comment::*, comment_report::*, moderator::*},
LocalUserId,
};
use lemmy_db_views::{
comment_report_view::{CommentReportQueryBuilder, CommentReportView},
comment_view::{CommentQueryBuilder, CommentView},
local_user_view::LocalUserView,
};
use lemmy_structs::{blocking, comment::*, send_local_notifs};
use lemmy_utils::{
apub::{make_apub_endpoint, EndpointType},
utils::{remove_slurs, scrape_text_for_mentions},
APIError,
ApiError,
ConnectionId,
LemmyError,
};
@ -49,7 +52,7 @@ impl Perform for CreateComment {
websocket_id: Option<ConnectionId>,
) -> Result<CommentResponse, LemmyError> {
let data: &CreateComment = &self;
let user = get_user_from_jwt(&data.auth, context.pool()).await?;
let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
let content_slurs_removed = remove_slurs(&data.content.to_owned());
@ -57,18 +60,31 @@ impl Perform for CreateComment {
let post_id = data.post_id;
let post = get_post(post_id, context.pool()).await?;
check_community_ban(user.id, post.community_id, context.pool()).await?;
check_community_ban(local_user_view.person.id, post.community_id, context.pool()).await?;
// Check if post is locked, no new comments
if post.locked {
return Err(APIError::err("locked").into());
return Err(ApiError::err("locked").into());
}
// If there's a parent_id, check to make sure that comment is in that post
if let Some(parent_id) = data.parent_id {
// Make sure the parent comment exists
let parent =
match blocking(context.pool(), move |conn| Comment::read(&conn, parent_id)).await? {
Ok(comment) => comment,
Err(_e) => return Err(ApiError::err("couldnt_create_comment").into()),
};
if parent.post_id != post_id {
return Err(ApiError::err("couldnt_create_comment").into());
}
}
let comment_form = CommentForm {
content: content_slurs_removed,
parent_id: data.parent_id.to_owned(),
post_id: data.post_id,
creator_id: user.id,
creator_id: local_user_view.person.id,
removed: None,
deleted: None,
read: None,
@ -86,23 +102,26 @@ impl Perform for CreateComment {
.await?
{
Ok(comment) => comment,
Err(_e) => return Err(APIError::err("couldnt_create_comment").into()),
Err(_e) => return Err(ApiError::err("couldnt_create_comment").into()),
};
// Necessary to update the ap_id
let inserted_comment_id = inserted_comment.id;
let updated_comment: Comment = match blocking(context.pool(), move |conn| {
let updated_comment: Comment =
match blocking(context.pool(), move |conn| -> Result<Comment, LemmyError> {
let apub_id =
make_apub_endpoint(EndpointType::Comment, &inserted_comment_id.to_string()).to_string();
Comment::update_ap_id(&conn, inserted_comment_id, apub_id)
generate_apub_endpoint(EndpointType::Comment, &inserted_comment_id.to_string())?;
Ok(Comment::update_ap_id(&conn, inserted_comment_id, apub_id)?)
})
.await?
{
Ok(comment) => comment,
Err(_e) => return Err(APIError::err("couldnt_create_comment").into()),
Err(_e) => return Err(ApiError::err("couldnt_create_comment").into()),
};
updated_comment.send_create(&user, context).await?;
updated_comment
.send_create(&local_user_view.person, context)
.await?;
// Scan the comment for user mentions, add those rows
let post_id = post.id;
@ -110,7 +129,7 @@ impl Perform for CreateComment {
let recipient_ids = send_local_notifs(
mentions,
updated_comment.clone(),
&user,
local_user_view.person.clone(),
post,
context.pool(),
true,
@ -121,38 +140,40 @@ impl Perform for CreateComment {
let like_form = CommentLikeForm {
comment_id: inserted_comment.id,
post_id,
user_id: user.id,
person_id: local_user_view.person.id,
score: 1,
};
let like = move |conn: &'_ _| CommentLike::like(&conn, &like_form);
if blocking(context.pool(), like).await?.is_err() {
return Err(APIError::err("couldnt_like_comment").into());
return Err(ApiError::err("couldnt_like_comment").into());
}
updated_comment.send_like(&user, context).await?;
updated_comment
.send_like(&local_user_view.person, context)
.await?;
let user_id = user.id;
let person_id = local_user_view.person.id;
let mut comment_view = blocking(context.pool(), move |conn| {
CommentView::read(&conn, inserted_comment.id, Some(user_id))
CommentView::read(&conn, inserted_comment.id, Some(person_id))
})
.await??;
// If its a comment to yourself, mark it as read
let comment_id = comment_view.comment.id;
if user.id == comment_view.get_recipient_id() {
if local_user_view.person.id == comment_view.get_recipient_id() {
match blocking(context.pool(), move |conn| {
Comment::update_read(conn, comment_id, true)
})
.await?
{
Ok(comment) => comment,
Err(_e) => return Err(APIError::err("couldnt_update_comment").into()),
Err(_e) => return Err(ApiError::err("couldnt_update_comment").into()),
};
comment_view.comment.read = true;
}
let res = CommentResponse {
let mut res = CommentResponse {
comment_view,
recipient_ids,
form_id: data.form_id.to_owned(),
@ -164,6 +185,8 @@ impl Perform for CreateComment {
websocket_id,
});
res.recipient_ids = Vec::new(); // Necessary to avoid doubles
Ok(res)
}
}
@ -178,35 +201,42 @@ impl Perform for EditComment {
websocket_id: Option<ConnectionId>,
) -> Result<CommentResponse, LemmyError> {
let data: &EditComment = &self;
let user = get_user_from_jwt(&data.auth, context.pool()).await?;
let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
let edit_id = data.edit_id;
let comment_id = data.comment_id;
let orig_comment = blocking(context.pool(), move |conn| {
CommentView::read(&conn, edit_id, None)
CommentView::read(&conn, comment_id, None)
})
.await??;
check_community_ban(user.id, orig_comment.community.id, context.pool()).await?;
check_community_ban(
local_user_view.person.id,
orig_comment.community.id,
context.pool(),
)
.await?;
// Verify that only the creator can edit
if user.id != orig_comment.creator.id {
return Err(APIError::err("no_comment_edit_allowed").into());
if local_user_view.person.id != orig_comment.creator.id {
return Err(ApiError::err("no_comment_edit_allowed").into());
}
// Do the update
let content_slurs_removed = remove_slurs(&data.content.to_owned());
let edit_id = data.edit_id;
let comment_id = data.comment_id;
let updated_comment = match blocking(context.pool(), move |conn| {
Comment::update_content(conn, edit_id, &content_slurs_removed)
Comment::update_content(conn, comment_id, &content_slurs_removed)
})
.await?
{
Ok(comment) => comment,
Err(_e) => return Err(APIError::err("couldnt_update_comment").into()),
Err(_e) => return Err(ApiError::err("couldnt_update_comment").into()),
};
// Send the apub update
updated_comment.send_update(&user, context).await?;
updated_comment
.send_update(&local_user_view.person, context)
.await?;
// Do the mentions / recipients
let updated_comment_content = updated_comment.content.to_owned();
@ -214,17 +244,17 @@ impl Perform for EditComment {
let recipient_ids = send_local_notifs(
mentions,
updated_comment,
&user,
local_user_view.person.clone(),
orig_comment.post,
context.pool(),
false,
)
.await?;
let edit_id = data.edit_id;
let user_id = user.id;
let comment_id = data.comment_id;
let person_id = local_user_view.person.id;
let comment_view = blocking(context.pool(), move |conn| {
CommentView::read(conn, edit_id, Some(user_id))
CommentView::read(conn, comment_id, Some(person_id))
})
.await??;
@ -254,44 +284,53 @@ impl Perform for DeleteComment {
websocket_id: Option<ConnectionId>,
) -> Result<CommentResponse, LemmyError> {
let data: &DeleteComment = &self;
let user = get_user_from_jwt(&data.auth, context.pool()).await?;
let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
let edit_id = data.edit_id;
let comment_id = data.comment_id;
let orig_comment = blocking(context.pool(), move |conn| {
CommentView::read(&conn, edit_id, None)
CommentView::read(&conn, comment_id, None)
})
.await??;
check_community_ban(user.id, orig_comment.community.id, context.pool()).await?;
check_community_ban(
local_user_view.person.id,
orig_comment.community.id,
context.pool(),
)
.await?;
// Verify that only the creator can delete
if user.id != orig_comment.creator.id {
return Err(APIError::err("no_comment_edit_allowed").into());
if local_user_view.person.id != orig_comment.creator.id {
return Err(ApiError::err("no_comment_edit_allowed").into());
}
// Do the delete
let deleted = data.deleted;
let updated_comment = match blocking(context.pool(), move |conn| {
Comment::update_deleted(conn, edit_id, deleted)
Comment::update_deleted(conn, comment_id, deleted)
})
.await?
{
Ok(comment) => comment,
Err(_e) => return Err(APIError::err("couldnt_update_comment").into()),
Err(_e) => return Err(ApiError::err("couldnt_update_comment").into()),
};
// Send the apub message
if deleted {
updated_comment.send_delete(&user, context).await?;
updated_comment
.send_delete(&local_user_view.person, context)
.await?;
} else {
updated_comment.send_undo_delete(&user, context).await?;
updated_comment
.send_undo_delete(&local_user_view.person, context)
.await?;
}
// Refetch it
let edit_id = data.edit_id;
let user_id = user.id;
let comment_id = data.comment_id;
let person_id = local_user_view.person.id;
let comment_view = blocking(context.pool(), move |conn| {
CommentView::read(conn, edit_id, Some(user_id))
CommentView::read(conn, comment_id, Some(person_id))
})
.await??;
@ -301,7 +340,7 @@ impl Perform for DeleteComment {
let recipient_ids = send_local_notifs(
mentions,
updated_comment,
&user,
local_user_view.person.clone(),
comment_view_2.post,
context.pool(),
false,
@ -334,34 +373,44 @@ impl Perform for RemoveComment {
websocket_id: Option<ConnectionId>,
) -> Result<CommentResponse, LemmyError> {
let data: &RemoveComment = &self;
let user = get_user_from_jwt(&data.auth, context.pool()).await?;
let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
let edit_id = data.edit_id;
let comment_id = data.comment_id;
let orig_comment = blocking(context.pool(), move |conn| {
CommentView::read(&conn, edit_id, None)
CommentView::read(&conn, comment_id, None)
})
.await??;
check_community_ban(user.id, orig_comment.community.id, context.pool()).await?;
check_community_ban(
local_user_view.person.id,
orig_comment.community.id,
context.pool(),
)
.await?;
// Verify that only a mod or admin can remove
is_mod_or_admin(context.pool(), user.id, orig_comment.community.id).await?;
is_mod_or_admin(
context.pool(),
local_user_view.person.id,
orig_comment.community.id,
)
.await?;
// Do the remove
let removed = data.removed;
let updated_comment = match blocking(context.pool(), move |conn| {
Comment::update_removed(conn, edit_id, removed)
Comment::update_removed(conn, comment_id, removed)
})
.await?
{
Ok(comment) => comment,
Err(_e) => return Err(APIError::err("couldnt_update_comment").into()),
Err(_e) => return Err(ApiError::err("couldnt_update_comment").into()),
};
// Mod tables
let form = ModRemoveCommentForm {
mod_user_id: user.id,
comment_id: data.edit_id,
mod_person_id: local_user_view.person.id,
comment_id: data.comment_id,
removed: Some(removed),
reason: data.reason.to_owned(),
};
@ -372,16 +421,20 @@ impl Perform for RemoveComment {
// Send the apub message
if removed {
updated_comment.send_remove(&user, context).await?;
updated_comment
.send_remove(&local_user_view.person, context)
.await?;
} else {
updated_comment.send_undo_remove(&user, context).await?;
updated_comment
.send_undo_remove(&local_user_view.person, context)
.await?;
}
// Refetch it
let edit_id = data.edit_id;
let user_id = user.id;
let comment_id = data.comment_id;
let person_id = local_user_view.person.id;
let comment_view = blocking(context.pool(), move |conn| {
CommentView::read(conn, edit_id, Some(user_id))
CommentView::read(conn, comment_id, Some(person_id))
})
.await??;
@ -392,7 +445,7 @@ impl Perform for RemoveComment {
let recipient_ids = send_local_notifs(
mentions,
updated_comment,
&user,
local_user_view.person.clone(),
comment_view_2.post,
context.pool(),
false,
@ -425,7 +478,7 @@ impl Perform for MarkCommentAsRead {
_websocket_id: Option<ConnectionId>,
) -> Result<CommentResponse, LemmyError> {
let data: &MarkCommentAsRead = &self;
let user = get_user_from_jwt(&data.auth, context.pool()).await?;
let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
let comment_id = data.comment_id;
let orig_comment = blocking(context.pool(), move |conn| {
@ -433,11 +486,16 @@ impl Perform for MarkCommentAsRead {
})
.await??;
check_community_ban(user.id, orig_comment.community.id, context.pool()).await?;
check_community_ban(
local_user_view.person.id,
orig_comment.community.id,
context.pool(),
)
.await?;
// Verify that only the recipient can mark as read
if user.id != orig_comment.get_recipient_id() {
return Err(APIError::err("no_comment_edit_allowed").into());
if local_user_view.person.id != orig_comment.get_recipient_id() {
return Err(ApiError::err("no_comment_edit_allowed").into());
}
// Do the mark as read
@ -448,14 +506,14 @@ impl Perform for MarkCommentAsRead {
.await?
{
Ok(comment) => comment,
Err(_e) => return Err(APIError::err("couldnt_update_comment").into()),
Err(_e) => return Err(ApiError::err("couldnt_update_comment").into()),
};
// Refetch it
let edit_id = data.comment_id;
let user_id = user.id;
let comment_id = data.comment_id;
let person_id = local_user_view.person.id;
let comment_view = blocking(context.pool(), move |conn| {
CommentView::read(conn, edit_id, Some(user_id))
CommentView::read(conn, comment_id, Some(person_id))
})
.await??;
@ -479,29 +537,29 @@ impl Perform for SaveComment {
_websocket_id: Option<ConnectionId>,
) -> Result<CommentResponse, LemmyError> {
let data: &SaveComment = &self;
let user = get_user_from_jwt(&data.auth, context.pool()).await?;
let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
let comment_saved_form = CommentSavedForm {
comment_id: data.comment_id,
user_id: user.id,
person_id: local_user_view.person.id,
};
if data.save {
let save_comment = move |conn: &'_ _| CommentSaved::save(conn, &comment_saved_form);
if blocking(context.pool(), save_comment).await?.is_err() {
return Err(APIError::err("couldnt_save_comment").into());
return Err(ApiError::err("couldnt_save_comment").into());
}
} else {
let unsave_comment = move |conn: &'_ _| CommentSaved::unsave(conn, &comment_saved_form);
if blocking(context.pool(), unsave_comment).await?.is_err() {
return Err(APIError::err("couldnt_save_comment").into());
return Err(ApiError::err("couldnt_save_comment").into());
}
}
let comment_id = data.comment_id;
let user_id = user.id;
let person_id = local_user_view.person.id;
let comment_view = blocking(context.pool(), move |conn| {
CommentView::read(conn, comment_id, Some(user_id))
CommentView::read(conn, comment_id, Some(person_id))
})
.await??;
@ -523,9 +581,9 @@ impl Perform for CreateCommentLike {
websocket_id: Option<ConnectionId>,
) -> Result<CommentResponse, LemmyError> {
let data: &CreateCommentLike = &self;
let user = get_user_from_jwt(&data.auth, context.pool()).await?;
let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
let mut recipient_ids = Vec::new();
let mut recipient_ids = Vec::<LocalUserId>::new();
// Don't do a downvote if site has downvotes disabled
check_downvotes_enabled(data.score, context.pool()).await?;
@ -536,22 +594,34 @@ impl Perform for CreateCommentLike {
})
.await??;
check_community_ban(user.id, orig_comment.community.id, context.pool()).await?;
check_community_ban(
local_user_view.person.id,
orig_comment.community.id,
context.pool(),
)
.await?;
// Add parent user to recipients
recipient_ids.push(orig_comment.get_recipient_id());
let recipient_id = orig_comment.get_recipient_id();
if let Ok(local_recipient) = blocking(context.pool(), move |conn| {
LocalUserView::read_person(conn, recipient_id)
})
.await?
{
recipient_ids.push(local_recipient.local_user.id);
}
let like_form = CommentLikeForm {
comment_id: data.comment_id,
post_id: orig_comment.post.id,
user_id: user.id,
person_id: local_user_view.person.id,
score: data.score,
};
// Remove any likes first
let user_id = user.id;
let person_id = local_user_view.person.id;
blocking(context.pool(), move |conn| {
CommentLike::remove(conn, user_id, comment_id)
CommentLike::remove(conn, person_id, comment_id)
})
.await??;
@ -562,23 +632,27 @@ impl Perform for CreateCommentLike {
let like_form2 = like_form.clone();
let like = move |conn: &'_ _| CommentLike::like(conn, &like_form2);
if blocking(context.pool(), like).await?.is_err() {
return Err(APIError::err("couldnt_like_comment").into());
return Err(ApiError::err("couldnt_like_comment").into());
}
if like_form.score == 1 {
comment.send_like(&user, context).await?;
comment.send_like(&local_user_view.person, context).await?;
} else if like_form.score == -1 {
comment.send_dislike(&user, context).await?;
comment
.send_dislike(&local_user_view.person, context)
.await?;
}
} else {
comment.send_undo_like(&user, context).await?;
comment
.send_undo_like(&local_user_view.person, context)
.await?;
}
// Have to refetch the comment to get the current state
let comment_id = data.comment_id;
let user_id = user.id;
let person_id = local_user_view.person.id;
let liked_comment = blocking(context.pool(), move |conn| {
CommentView::read(conn, comment_id, Some(user_id))
CommentView::read(conn, comment_id, Some(person_id))
})
.await??;
@ -608,8 +682,8 @@ impl Perform for GetComments {
_websocket_id: Option<ConnectionId>,
) -> Result<GetCommentsResponse, LemmyError> {
let data: &GetComments = &self;
let user = get_user_from_jwt_opt(&data.auth, context.pool()).await?;
let user_id = user.map(|u| u.id);
let local_user_view = get_local_user_view_from_jwt_opt(&data.auth, context.pool()).await?;
let person_id = local_user_view.map(|u| u.person.id);
let type_ = ListingType::from_str(&data.type_)?;
let sort = SortType::from_str(&data.sort)?;
@ -624,7 +698,7 @@ impl Perform for GetComments {
.sort(&sort)
.community_id(community_id)
.community_name(community_name)
.my_user_id(user_id)
.my_person_id(person_id)
.page(page)
.limit(limit)
.list()
@ -632,7 +706,7 @@ impl Perform for GetComments {
.await?;
let comments = match comments {
Ok(comments) => comments,
Err(_) => return Err(APIError::err("couldnt_get_comments").into()),
Err(_) => return Err(ApiError::err("couldnt_get_comments").into()),
};
Ok(GetCommentsResponse { comments })
@ -650,28 +724,28 @@ impl Perform for CreateCommentReport {
websocket_id: Option<ConnectionId>,
) -> Result<CreateCommentReportResponse, LemmyError> {
let data: &CreateCommentReport = &self;
let user = get_user_from_jwt(&data.auth, context.pool()).await?;
let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
// check size of report and check for whitespace
let reason = data.reason.trim();
if reason.is_empty() {
return Err(APIError::err("report_reason_required").into());
return Err(ApiError::err("report_reason_required").into());
}
if reason.len() > 1000 {
return Err(APIError::err("report_too_long").into());
if reason.chars().count() > 1000 {
return Err(ApiError::err("report_too_long").into());
}
let user_id = user.id;
let person_id = local_user_view.person.id;
let comment_id = data.comment_id;
let comment_view = blocking(context.pool(), move |conn| {
CommentView::read(&conn, comment_id, None)
})
.await??;
check_community_ban(user_id, comment_view.community.id, context.pool()).await?;
check_community_ban(person_id, comment_view.community.id, context.pool()).await?;
let report_form = CommentReportForm {
creator_id: user_id,
creator_id: person_id,
comment_id,
original_comment_text: comment_view.comment.content,
reason: data.reason.to_owned(),
@ -683,7 +757,7 @@ impl Perform for CreateCommentReport {
.await?
{
Ok(report) => report,
Err(_e) => return Err(APIError::err("couldnt_create_report").into()),
Err(_e) => return Err(ApiError::err("couldnt_create_report").into()),
};
let res = CreateCommentReportResponse { success: true };
@ -691,7 +765,7 @@ impl Perform for CreateCommentReport {
context.chat_server().do_send(SendUserRoomMessage {
op: UserOperation::CreateCommentReport,
response: res.clone(),
recipient_id: user.id,
local_recipient_id: local_user_view.local_user.id,
websocket_id,
});
@ -717,7 +791,7 @@ impl Perform for ResolveCommentReport {
websocket_id: Option<ConnectionId>,
) -> Result<ResolveCommentReportResponse, LemmyError> {
let data: &ResolveCommentReport = &self;
let user = get_user_from_jwt(&data.auth, context.pool()).await?;
let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
let report_id = data.report_id;
let report = blocking(context.pool(), move |conn| {
@ -725,20 +799,20 @@ impl Perform for ResolveCommentReport {
})
.await??;
let user_id = user.id;
is_mod_or_admin(context.pool(), user_id, report.community.id).await?;
let person_id = local_user_view.person.id;
is_mod_or_admin(context.pool(), person_id, report.community.id).await?;
let resolved = data.resolved;
let resolve_fun = move |conn: &'_ _| {
if resolved {
CommentReport::resolve(conn, report_id, user_id)
CommentReport::resolve(conn, report_id, person_id)
} else {
CommentReport::unresolve(conn, report_id, user_id)
CommentReport::unresolve(conn, report_id, person_id)
}
};
if blocking(context.pool(), resolve_fun).await?.is_err() {
return Err(APIError::err("couldnt_resolve_report").into());
return Err(ApiError::err("couldnt_resolve_report").into());
};
let report_id = data.report_id;
@ -770,12 +844,12 @@ impl Perform for ListCommentReports {
websocket_id: Option<ConnectionId>,
) -> Result<ListCommentReportsResponse, LemmyError> {
let data: &ListCommentReports = &self;
let user = get_user_from_jwt(&data.auth, context.pool()).await?;
let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
let user_id = user.id;
let person_id = local_user_view.person.id;
let community_id = data.community;
let community_ids =
collect_moderated_communities(user_id, community_id, context.pool()).await?;
collect_moderated_communities(person_id, community_id, context.pool()).await?;
let page = data.page;
let limit = data.limit;
@ -793,7 +867,7 @@ impl Perform for ListCommentReports {
context.chat_server().do_send(SendUserRoomMessage {
op: UserOperation::ListCommentReports,
response: res.clone(),
recipient_id: user.id,
local_recipient_id: local_user_view.local_user.id,
websocket_id,
});

View file

@ -1,16 +1,24 @@
use crate::{
check_optional_url,
get_user_from_jwt,
get_user_from_jwt_opt,
check_community_ban,
get_local_user_view_from_jwt,
get_local_user_view_from_jwt_opt,
is_admin,
is_mod_or_admin,
Perform,
};
use actix_web::web::Data;
use anyhow::Context;
use lemmy_apub::ActorType;
use lemmy_api_structs::{blocking, community::*};
use lemmy_apub::{
generate_apub_endpoint,
generate_followers_url,
generate_inbox_url,
generate_shared_inbox_url,
ActorType,
EndpointType,
};
use lemmy_db_queries::{
diesel_option_overwrite,
diesel_option_overwrite_to_url,
source::{
comment::Comment_,
community::{CommunityModerator_, Community_},
@ -21,30 +29,31 @@ use lemmy_db_queries::{
Crud,
Followable,
Joinable,
ListingType,
SortType,
};
use lemmy_db_schema::{
naive_now,
source::{comment::Comment, community::*, moderator::*, post::Post, site::*},
PersonId,
};
use lemmy_db_views::comment_view::CommentQueryBuilder;
use lemmy_db_views_actor::{
community_follower_view::CommunityFollowerView,
community_moderator_view::CommunityModeratorView,
community_view::{CommunityQueryBuilder, CommunityView},
user_view::UserViewSafe,
person_view::PersonViewSafe,
};
use lemmy_structs::{blocking, community::*};
use lemmy_utils::{
apub::{generate_actor_keypair, make_apub_endpoint, EndpointType},
apub::generate_actor_keypair,
location_info,
utils::{check_slurs, check_slurs_opt, is_valid_community_name, naive_from_unix},
APIError,
ApiError,
ConnectionId,
LemmyError,
};
use lemmy_websocket::{
messages::{GetCommunityUsersOnline, JoinCommunityRoom, JoinModRoom, SendCommunityRoomMessage},
messages::{GetCommunityUsersOnline, SendCommunityRoomMessage},
LemmyContext,
UserOperation,
};
@ -60,8 +69,8 @@ impl Perform for GetCommunity {
_websocket_id: Option<ConnectionId>,
) -> Result<GetCommunityResponse, LemmyError> {
let data: &GetCommunity = &self;
let user = get_user_from_jwt_opt(&data.auth, context.pool()).await?;
let user_id = user.map(|u| u.id);
let local_user_view = get_local_user_view_from_jwt_opt(&data.auth, context.pool()).await?;
let person_id = local_user_view.map(|u| u.person.id);
let community_id = match data.id {
Some(id) => id,
@ -73,19 +82,19 @@ impl Perform for GetCommunity {
.await?
{
Ok(community) => community,
Err(_e) => return Err(APIError::err("couldnt_find_community").into()),
Err(_e) => return Err(ApiError::err("couldnt_find_community").into()),
}
.id
}
};
let community_view = match blocking(context.pool(), move |conn| {
CommunityView::read(conn, community_id, user_id)
CommunityView::read(conn, community_id, person_id)
})
.await?
{
Ok(community) => community,
Err(_e) => return Err(APIError::err("couldnt_find_community").into()),
Err(_e) => return Err(ApiError::err("couldnt_find_community").into()),
};
let moderators: Vec<CommunityModeratorView> = match blocking(context.pool(), move |conn| {
@ -94,7 +103,7 @@ impl Perform for GetCommunity {
.await?
{
Ok(moderators) => moderators,
Err(_e) => return Err(APIError::err("couldnt_find_community").into()),
Err(_e) => return Err(ApiError::err("couldnt_find_community").into()),
};
let online = context
@ -124,33 +133,30 @@ impl Perform for CreateCommunity {
_websocket_id: Option<ConnectionId>,
) -> Result<CommunityResponse, LemmyError> {
let data: &CreateCommunity = &self;
let user = get_user_from_jwt(&data.auth, context.pool()).await?;
let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
check_slurs(&data.name)?;
check_slurs(&data.title)?;
check_slurs_opt(&data.description)?;
if !is_valid_community_name(&data.name) {
return Err(APIError::err("invalid_community_name").into());
return Err(ApiError::err("invalid_community_name").into());
}
// Double check for duplicate community actor_ids
let actor_id = make_apub_endpoint(EndpointType::Community, &data.name).to_string();
let actor_id_cloned = actor_id.to_owned();
let community_actor_id = generate_apub_endpoint(EndpointType::Community, &data.name)?;
let actor_id_cloned = community_actor_id.to_owned();
let community_dupe = blocking(context.pool(), move |conn| {
Community::read_from_apub_id(conn, &actor_id_cloned)
})
.await?;
if community_dupe.is_ok() {
return Err(APIError::err("community_already_exists").into());
return Err(ApiError::err("community_already_exists").into());
}
// Check to make sure the icon and banners are urls
let icon = diesel_option_overwrite(&data.icon);
let banner = diesel_option_overwrite(&data.banner);
check_optional_url(&icon)?;
check_optional_url(&banner)?;
let icon = diesel_option_overwrite_to_url(&data.icon)?;
let banner = diesel_option_overwrite_to_url(&data.banner)?;
// When you create a community, make sure the user becomes a moderator and a follower
let keypair = generate_actor_keypair()?;
@ -161,18 +167,20 @@ impl Perform for CreateCommunity {
description: data.description.to_owned(),
icon,
banner,
category_id: data.category_id,
creator_id: user.id,
creator_id: local_user_view.person.id,
removed: None,
deleted: None,
nsfw: data.nsfw,
updated: None,
actor_id: Some(actor_id),
actor_id: Some(community_actor_id.to_owned()),
local: true,
private_key: Some(keypair.private_key),
public_key: Some(keypair.public_key),
last_refreshed_at: None,
published: None,
followers_url: Some(generate_followers_url(&community_actor_id)?),
inbox_url: Some(generate_inbox_url(&community_actor_id)?),
shared_inbox_url: Some(Some(generate_shared_inbox_url(&community_actor_id)?)),
};
let inserted_community = match blocking(context.pool(), move |conn| {
@ -181,35 +189,35 @@ impl Perform for CreateCommunity {
.await?
{
Ok(community) => community,
Err(_e) => return Err(APIError::err("community_already_exists").into()),
Err(_e) => return Err(ApiError::err("community_already_exists").into()),
};
// The community creator becomes a moderator
let community_moderator_form = CommunityModeratorForm {
community_id: inserted_community.id,
user_id: user.id,
person_id: local_user_view.person.id,
};
let join = move |conn: &'_ _| CommunityModerator::join(conn, &community_moderator_form);
if blocking(context.pool(), join).await?.is_err() {
return Err(APIError::err("community_moderator_already_exists").into());
return Err(ApiError::err("community_moderator_already_exists").into());
}
// Follow your own community
let community_follower_form = CommunityFollowerForm {
community_id: inserted_community.id,
user_id: user.id,
person_id: local_user_view.person.id,
pending: false,
};
let follow = move |conn: &'_ _| CommunityFollower::follow(conn, &community_follower_form);
if blocking(context.pool(), follow).await?.is_err() {
return Err(APIError::err("community_follower_already_exists").into());
return Err(ApiError::err("community_follower_already_exists").into());
}
let user_id = user.id;
let person_id = local_user_view.person.id;
let community_view = blocking(context.pool(), move |conn| {
CommunityView::read(conn, inserted_community.id, Some(user_id))
CommunityView::read(conn, inserted_community.id, Some(person_id))
})
.await??;
@ -227,31 +235,30 @@ impl Perform for EditCommunity {
websocket_id: Option<ConnectionId>,
) -> Result<CommunityResponse, LemmyError> {
let data: &EditCommunity = &self;
let user = get_user_from_jwt(&data.auth, context.pool()).await?;
let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
check_slurs(&data.title)?;
check_slurs_opt(&data.description)?;
// Verify its a mod (only mods can edit it)
let edit_id = data.edit_id;
let mods: Vec<i32> = blocking(context.pool(), move |conn| {
CommunityModeratorView::for_community(conn, edit_id)
let community_id = data.community_id;
let mods: Vec<PersonId> = blocking(context.pool(), move |conn| {
CommunityModeratorView::for_community(conn, community_id)
.map(|v| v.into_iter().map(|m| m.moderator.id).collect())
})
.await??;
if !mods.contains(&user.id) {
return Err(APIError::err("not_a_moderator").into());
if !mods.contains(&local_user_view.person.id) {
return Err(ApiError::err("not_a_moderator").into());
}
let edit_id = data.edit_id;
let read_community =
blocking(context.pool(), move |conn| Community::read(conn, edit_id)).await??;
let community_id = data.community_id;
let read_community = blocking(context.pool(), move |conn| {
Community::read(conn, community_id)
})
.await??;
let icon = diesel_option_overwrite(&data.icon);
let banner = diesel_option_overwrite(&data.banner);
check_optional_url(&icon)?;
check_optional_url(&banner)?;
let icon = diesel_option_overwrite_to_url(&data.icon)?;
let banner = diesel_option_overwrite_to_url(&data.banner)?;
let community_form = CommunityForm {
name: read_community.name,
@ -259,7 +266,6 @@ impl Perform for EditCommunity {
description: data.description.to_owned(),
icon,
banner,
category_id: data.category_id.to_owned(),
creator_id: read_community.creator_id,
removed: Some(read_community.removed),
deleted: Some(read_community.deleted),
@ -271,25 +277,28 @@ impl Perform for EditCommunity {
public_key: read_community.public_key,
last_refreshed_at: None,
published: None,
followers_url: None,
inbox_url: None,
shared_inbox_url: None,
};
let edit_id = data.edit_id;
let community_id = data.community_id;
match blocking(context.pool(), move |conn| {
Community::update(conn, edit_id, &community_form)
Community::update(conn, community_id, &community_form)
})
.await?
{
Ok(community) => community,
Err(_e) => return Err(APIError::err("couldnt_update_community").into()),
Err(_e) => return Err(ApiError::err("couldnt_update_community").into()),
};
// TODO there needs to be some kind of an apub update
// process for communities and users
let edit_id = data.edit_id;
let user_id = user.id;
let community_id = data.community_id;
let person_id = local_user_view.person.id;
let community_view = blocking(context.pool(), move |conn| {
CommunityView::read(conn, edit_id, Some(user_id))
CommunityView::read(conn, community_id, Some(person_id))
})
.await??;
@ -311,26 +320,28 @@ impl Perform for DeleteCommunity {
websocket_id: Option<ConnectionId>,
) -> Result<CommunityResponse, LemmyError> {
let data: &DeleteCommunity = &self;
let user = get_user_from_jwt(&data.auth, context.pool()).await?;
let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
// Verify its the creator (only a creator can delete the community)
let edit_id = data.edit_id;
let read_community =
blocking(context.pool(), move |conn| Community::read(conn, edit_id)).await??;
if read_community.creator_id != user.id {
return Err(APIError::err("no_community_edit_allowed").into());
let community_id = data.community_id;
let read_community = blocking(context.pool(), move |conn| {
Community::read(conn, community_id)
})
.await??;
if read_community.creator_id != local_user_view.person.id {
return Err(ApiError::err("no_community_edit_allowed").into());
}
// Do the delete
let edit_id = data.edit_id;
let community_id = data.community_id;
let deleted = data.deleted;
let updated_community = match blocking(context.pool(), move |conn| {
Community::update_deleted(conn, edit_id, deleted)
Community::update_deleted(conn, community_id, deleted)
})
.await?
{
Ok(community) => community,
Err(_e) => return Err(APIError::err("couldnt_update_community").into()),
Err(_e) => return Err(ApiError::err("couldnt_update_community").into()),
};
// Send apub messages
@ -340,10 +351,10 @@ impl Perform for DeleteCommunity {
updated_community.send_undo_delete(context).await?;
}
let edit_id = data.edit_id;
let user_id = user.id;
let community_id = data.community_id;
let person_id = local_user_view.person.id;
let community_view = blocking(context.pool(), move |conn| {
CommunityView::read(conn, edit_id, Some(user_id))
CommunityView::read(conn, community_id, Some(person_id))
})
.await??;
@ -365,21 +376,21 @@ impl Perform for RemoveCommunity {
websocket_id: Option<ConnectionId>,
) -> Result<CommunityResponse, LemmyError> {
let data: &RemoveCommunity = &self;
let user = get_user_from_jwt(&data.auth, context.pool()).await?;
let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
// Verify its an admin (only an admin can remove a community)
is_admin(context.pool(), user.id).await?;
is_admin(&local_user_view)?;
// Do the remove
let edit_id = data.edit_id;
let community_id = data.community_id;
let removed = data.removed;
let updated_community = match blocking(context.pool(), move |conn| {
Community::update_removed(conn, edit_id, removed)
Community::update_removed(conn, community_id, removed)
})
.await?
{
Ok(community) => community,
Err(_e) => return Err(APIError::err("couldnt_update_community").into()),
Err(_e) => return Err(ApiError::err("couldnt_update_community").into()),
};
// Mod tables
@ -388,8 +399,8 @@ impl Perform for RemoveCommunity {
None => None,
};
let form = ModRemoveCommunityForm {
mod_user_id: user.id,
community_id: data.edit_id,
mod_person_id: local_user_view.person.id,
community_id: data.community_id,
removed: Some(removed),
reason: data.reason.to_owned(),
expires,
@ -406,10 +417,10 @@ impl Perform for RemoveCommunity {
updated_community.send_undo_remove(context).await?;
}
let edit_id = data.edit_id;
let user_id = user.id;
let community_id = data.community_id;
let person_id = local_user_view.person.id;
let community_view = blocking(context.pool(), move |conn| {
CommunityView::read(conn, edit_id, Some(user_id))
CommunityView::read(conn, community_id, Some(person_id))
})
.await??;
@ -431,27 +442,30 @@ impl Perform for ListCommunities {
_websocket_id: Option<ConnectionId>,
) -> Result<ListCommunitiesResponse, LemmyError> {
let data: &ListCommunities = &self;
let user = get_user_from_jwt_opt(&data.auth, context.pool()).await?;
let local_user_view = get_local_user_view_from_jwt_opt(&data.auth, context.pool()).await?;
let user_id = match &user {
Some(user) => Some(user.id),
let person_id = match &local_user_view {
Some(uv) => Some(uv.person.id),
None => None,
};
let show_nsfw = match &user {
Some(user) => user.show_nsfw,
// Don't show NSFW by default
let show_nsfw = match &local_user_view {
Some(uv) => uv.local_user.show_nsfw,
None => false,
};
let type_ = ListingType::from_str(&data.type_)?;
let sort = SortType::from_str(&data.sort)?;
let page = data.page;
let limit = data.limit;
let communities = blocking(context.pool(), move |conn| {
CommunityQueryBuilder::create(conn)
.listing_type(&type_)
.sort(&sort)
.show_nsfw(show_nsfw)
.my_user_id(user_id)
.my_person_id(person_id)
.page(page)
.limit(limit)
.list()
@ -473,7 +487,7 @@ impl Perform for FollowCommunity {
_websocket_id: Option<ConnectionId>,
) -> Result<CommunityResponse, LemmyError> {
let data: &FollowCommunity = &self;
let user = get_user_from_jwt(&data.auth, context.pool()).await?;
let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
let community_id = data.community_id;
let community = blocking(context.pool(), move |conn| {
@ -482,39 +496,47 @@ impl Perform for FollowCommunity {
.await??;
let community_follower_form = CommunityFollowerForm {
community_id: data.community_id,
user_id: user.id,
person_id: local_user_view.person.id,
pending: false,
};
if community.local {
if data.follow {
check_community_ban(local_user_view.person.id, community_id, context.pool()).await?;
let follow = move |conn: &'_ _| CommunityFollower::follow(conn, &community_follower_form);
if blocking(context.pool(), follow).await?.is_err() {
return Err(APIError::err("community_follower_already_exists").into());
return Err(ApiError::err("community_follower_already_exists").into());
}
} else {
let unfollow =
move |conn: &'_ _| CommunityFollower::unfollow(conn, &community_follower_form);
if blocking(context.pool(), unfollow).await?.is_err() {
return Err(APIError::err("community_follower_already_exists").into());
return Err(ApiError::err("community_follower_already_exists").into());
}
}
} else if data.follow {
// Dont actually add to the community followers here, because you need
// to wait for the accept
user.send_follow(&community.actor_id()?, context).await?;
local_user_view
.person
.send_follow(&community.actor_id(), context)
.await?;
} else {
user.send_unfollow(&community.actor_id()?, context).await?;
local_user_view
.person
.send_unfollow(&community.actor_id(), context)
.await?;
let unfollow = move |conn: &'_ _| CommunityFollower::unfollow(conn, &community_follower_form);
if blocking(context.pool(), unfollow).await?.is_err() {
return Err(APIError::err("community_follower_already_exists").into());
return Err(ApiError::err("community_follower_already_exists").into());
}
}
let community_id = data.community_id;
let user_id = user.id;
let person_id = local_user_view.person.id;
let mut community_view = blocking(context.pool(), move |conn| {
CommunityView::read(conn, community_id, Some(user_id))
CommunityView::read(conn, community_id, Some(person_id))
})
.await??;
@ -539,16 +561,16 @@ impl Perform for GetFollowedCommunities {
_websocket_id: Option<ConnectionId>,
) -> Result<GetFollowedCommunitiesResponse, LemmyError> {
let data: &GetFollowedCommunities = &self;
let user = get_user_from_jwt(&data.auth, context.pool()).await?;
let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
let user_id = user.id;
let person_id = local_user_view.person.id;
let communities = match blocking(context.pool(), move |conn| {
CommunityFollowerView::for_user(conn, user_id)
CommunityFollowerView::for_person(conn, person_id)
})
.await?
{
Ok(communities) => communities,
_ => return Err(APIError::err("system_err_login").into()),
_ => return Err(ApiError::err("system_err_login").into()),
};
// Return the jwt
@ -566,28 +588,40 @@ impl Perform for BanFromCommunity {
websocket_id: Option<ConnectionId>,
) -> Result<BanFromCommunityResponse, LemmyError> {
let data: &BanFromCommunity = &self;
let user = get_user_from_jwt(&data.auth, context.pool()).await?;
let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
let community_id = data.community_id;
let banned_user_id = data.user_id;
let banned_person_id = data.person_id;
// Verify that only mods or admins can ban
is_mod_or_admin(context.pool(), user.id, community_id).await?;
is_mod_or_admin(context.pool(), local_user_view.person.id, community_id).await?;
let community_user_ban_form = CommunityUserBanForm {
let community_user_ban_form = CommunityPersonBanForm {
community_id: data.community_id,
user_id: data.user_id,
person_id: data.person_id,
};
if data.ban {
let ban = move |conn: &'_ _| CommunityUserBan::ban(conn, &community_user_ban_form);
let ban = move |conn: &'_ _| CommunityPersonBan::ban(conn, &community_user_ban_form);
if blocking(context.pool(), ban).await?.is_err() {
return Err(APIError::err("community_user_already_banned").into());
return Err(ApiError::err("community_user_already_banned").into());
}
// Also unsubscribe them from the community, if they are subscribed
let community_follower_form = CommunityFollowerForm {
community_id: data.community_id,
person_id: banned_person_id,
pending: false,
};
blocking(context.pool(), move |conn: &'_ _| {
CommunityFollower::unfollow(conn, &community_follower_form)
})
.await?
.ok();
} else {
let unban = move |conn: &'_ _| CommunityUserBan::unban(conn, &community_user_ban_form);
let unban = move |conn: &'_ _| CommunityPersonBan::unban(conn, &community_user_ban_form);
if blocking(context.pool(), unban).await?.is_err() {
return Err(APIError::err("community_user_already_banned").into());
return Err(ApiError::err("community_user_already_banned").into());
}
}
@ -595,7 +629,7 @@ impl Perform for BanFromCommunity {
if data.remove_data {
// Posts
blocking(context.pool(), move |conn: &'_ _| {
Post::update_removed_for_creator(conn, banned_user_id, Some(community_id), true)
Post::update_removed_for_creator(conn, banned_person_id, Some(community_id), true)
})
.await??;
@ -603,7 +637,7 @@ impl Perform for BanFromCommunity {
// TODO Diesel doesn't allow updates with joins, so this has to be a loop
let comments = blocking(context.pool(), move |conn| {
CommentQueryBuilder::create(conn)
.creator_id(banned_user_id)
.creator_id(banned_person_id)
.community_id(community_id)
.limit(std::i64::MAX)
.list()
@ -627,8 +661,8 @@ impl Perform for BanFromCommunity {
};
let form = ModBanFromCommunityForm {
mod_user_id: user.id,
other_user_id: data.user_id,
mod_person_id: local_user_view.person.id,
other_person_id: data.person_id,
community_id: data.community_id,
reason: data.reason.to_owned(),
banned: Some(data.ban),
@ -639,14 +673,14 @@ impl Perform for BanFromCommunity {
})
.await??;
let user_id = data.user_id;
let user_view = blocking(context.pool(), move |conn| {
UserViewSafe::read(conn, user_id)
let person_id = data.person_id;
let person_view = blocking(context.pool(), move |conn| {
PersonViewSafe::read(conn, person_id)
})
.await??;
let res = BanFromCommunityResponse {
user_view,
person_view,
banned: data.ban,
};
@ -671,34 +705,34 @@ impl Perform for AddModToCommunity {
websocket_id: Option<ConnectionId>,
) -> Result<AddModToCommunityResponse, LemmyError> {
let data: &AddModToCommunity = &self;
let user = get_user_from_jwt(&data.auth, context.pool()).await?;
let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
let community_moderator_form = CommunityModeratorForm {
community_id: data.community_id,
user_id: data.user_id,
person_id: data.person_id,
};
let community_id = data.community_id;
// Verify that only mods or admins can add mod
is_mod_or_admin(context.pool(), user.id, community_id).await?;
is_mod_or_admin(context.pool(), local_user_view.person.id, community_id).await?;
if data.added {
let join = move |conn: &'_ _| CommunityModerator::join(conn, &community_moderator_form);
if blocking(context.pool(), join).await?.is_err() {
return Err(APIError::err("community_moderator_already_exists").into());
return Err(ApiError::err("community_moderator_already_exists").into());
}
} else {
let leave = move |conn: &'_ _| CommunityModerator::leave(conn, &community_moderator_form);
if blocking(context.pool(), leave).await?.is_err() {
return Err(APIError::err("community_moderator_already_exists").into());
return Err(ApiError::err("community_moderator_already_exists").into());
}
}
// Mod tables
let form = ModAddCommunityForm {
mod_user_id: user.id,
other_user_id: data.user_id,
mod_person_id: local_user_view.person.id,
other_person_id: data.person_id,
community_id: data.community_id,
removed: Some(!data.added),
};
@ -736,7 +770,7 @@ impl Perform for TransferCommunity {
_websocket_id: Option<ConnectionId>,
) -> Result<GetCommunityResponse, LemmyError> {
let data: &TransferCommunity = &self;
let user = get_user_from_jwt(&data.auth, context.pool()).await?;
let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
let community_id = data.community_id;
let read_community = blocking(context.pool(), move |conn| {
@ -749,28 +783,31 @@ impl Perform for TransferCommunity {
})
.await??;
let mut admins = blocking(context.pool(), move |conn| UserViewSafe::admins(conn)).await??;
let mut admins = blocking(context.pool(), move |conn| PersonViewSafe::admins(conn)).await??;
// Making sure the creator, if an admin, is at the top
let creator_index = admins
.iter()
.position(|r| r.user.id == site_creator_id)
.position(|r| r.person.id == site_creator_id)
.context(location_info!())?;
let creator_user = admins.remove(creator_index);
admins.insert(0, creator_user);
let creator_person = admins.remove(creator_index);
admins.insert(0, creator_person);
// Make sure user is the creator, or an admin
if user.id != read_community.creator_id
&& !admins.iter().map(|a| a.user.id).any(|x| x == user.id)
if local_user_view.person.id != read_community.creator_id
&& !admins
.iter()
.map(|a| a.person.id)
.any(|x| x == local_user_view.person.id)
{
return Err(APIError::err("not_an_admin").into());
return Err(ApiError::err("not_an_admin").into());
}
let community_id = data.community_id;
let new_creator = data.user_id;
let new_creator = data.person_id;
let update = move |conn: &'_ _| Community::update_creator(conn, community_id, new_creator);
if blocking(context.pool(), update).await?.is_err() {
return Err(APIError::err("couldnt_update_community").into());
return Err(ApiError::err("couldnt_update_community").into());
};
// You also have to re-do the community_moderator table, reordering it.
@ -781,10 +818,10 @@ impl Perform for TransferCommunity {
.await??;
let creator_index = community_mods
.iter()
.position(|r| r.moderator.id == data.user_id)
.position(|r| r.moderator.id == data.person_id)
.context(location_info!())?;
let creator_user = community_mods.remove(creator_index);
community_mods.insert(0, creator_user);
let creator_person = community_mods.remove(creator_index);
community_mods.insert(0, creator_person);
let community_id = data.community_id;
blocking(context.pool(), move |conn| {
@ -796,19 +833,19 @@ impl Perform for TransferCommunity {
for cmod in &community_mods {
let community_moderator_form = CommunityModeratorForm {
community_id: cmod.community.id,
user_id: cmod.moderator.id,
person_id: cmod.moderator.id,
};
let join = move |conn: &'_ _| CommunityModerator::join(conn, &community_moderator_form);
if blocking(context.pool(), join).await?.is_err() {
return Err(APIError::err("community_moderator_already_exists").into());
return Err(ApiError::err("community_moderator_already_exists").into());
}
}
// Mod tables
let form = ModAddCommunityForm {
mod_user_id: user.id,
other_user_id: data.user_id,
mod_person_id: local_user_view.person.id,
other_person_id: data.person_id,
community_id: data.community_id,
removed: Some(false),
};
@ -818,14 +855,14 @@ impl Perform for TransferCommunity {
.await??;
let community_id = data.community_id;
let user_id = user.id;
let person_id = local_user_view.person.id;
let community_view = match blocking(context.pool(), move |conn| {
CommunityView::read(conn, community_id, Some(user_id))
CommunityView::read(conn, community_id, Some(person_id))
})
.await?
{
Ok(community) => community,
Err(_e) => return Err(APIError::err("couldnt_find_community").into()),
Err(_e) => return Err(ApiError::err("couldnt_find_community").into()),
};
let community_id = data.community_id;
@ -835,7 +872,7 @@ impl Perform for TransferCommunity {
.await?
{
Ok(moderators) => moderators,
Err(_e) => return Err(APIError::err("couldnt_find_community").into()),
Err(_e) => return Err(ApiError::err("couldnt_find_community").into()),
};
// Return the jwt
@ -853,7 +890,7 @@ fn send_community_websocket(
websocket_id: Option<ConnectionId>,
op: UserOperation,
) {
// Strip out the user id and subscribed when sending to others
// Strip out the person id and subscribed when sending to others
let mut res_sent = res.clone();
res_sent.community_view.subscribed = false;
@ -864,47 +901,3 @@ fn send_community_websocket(
websocket_id,
});
}
#[async_trait::async_trait(?Send)]
impl Perform for CommunityJoin {
type Response = CommunityJoinResponse;
async fn perform(
&self,
context: &Data<LemmyContext>,
websocket_id: Option<ConnectionId>,
) -> Result<CommunityJoinResponse, LemmyError> {
let data: &CommunityJoin = &self;
if let Some(ws_id) = websocket_id {
context.chat_server().do_send(JoinCommunityRoom {
community_id: data.community_id,
id: ws_id,
});
}
Ok(CommunityJoinResponse { joined: true })
}
}
#[async_trait::async_trait(?Send)]
impl Perform for ModJoin {
type Response = ModJoinResponse;
async fn perform(
&self,
context: &Data<LemmyContext>,
websocket_id: Option<ConnectionId>,
) -> Result<ModJoinResponse, LemmyError> {
let data: &ModJoin = &self;
if let Some(ws_id) = websocket_id {
context.chat_server().do_send(JoinModRoom {
community_id: data.community_id,
id: ws_id,
});
}
Ok(ModJoinResponse { joined: true })
}
}

View file

@ -1,5 +1,13 @@
use crate::claims::Claims;
use actix_web::{web, web::Data};
use lemmy_api_structs::{
blocking,
comment::*,
community::*,
person::*,
post::*,
site::*,
websocket::*,
};
use lemmy_db_queries::{
source::{
community::{CommunityModerator_, Community_},
@ -8,30 +16,41 @@ use lemmy_db_queries::{
Crud,
DbPool,
};
use lemmy_db_schema::source::{
use lemmy_db_schema::{
source::{
community::{Community, CommunityModerator},
post::Post,
site::Site,
user::User_,
},
CommunityId,
LocalUserId,
PersonId,
PostId,
};
use lemmy_db_views::local_user_view::{LocalUserSettingsView, LocalUserView};
use lemmy_db_views_actor::{
community_user_ban_view::CommunityUserBanView,
community_person_ban_view::CommunityPersonBanView,
community_view::CommunityView,
};
use lemmy_structs::{blocking, comment::*, community::*, post::*, site::*, user::*};
use lemmy_utils::{settings::Settings, APIError, ConnectionId, LemmyError};
use lemmy_utils::{
claims::Claims,
settings::structs::Settings,
ApiError,
ConnectionId,
LemmyError,
};
use lemmy_websocket::{serialize_websocket_message, LemmyContext, UserOperation};
use serde::Deserialize;
use std::process::Command;
use std::{env, process::Command};
use url::Url;
pub mod claims;
pub mod comment;
pub mod community;
pub mod local_user;
pub mod post;
pub mod routes;
pub mod site;
pub mod user;
pub mod version;
pub mod websocket;
#[async_trait::async_trait(?Send)]
pub trait Perform {
@ -46,65 +65,121 @@ pub trait Perform {
pub(crate) async fn is_mod_or_admin(
pool: &DbPool,
user_id: i32,
community_id: i32,
person_id: PersonId,
community_id: CommunityId,
) -> Result<(), LemmyError> {
let is_mod_or_admin = blocking(pool, move |conn| {
CommunityView::is_mod_or_admin(conn, user_id, community_id)
CommunityView::is_mod_or_admin(conn, person_id, community_id)
})
.await?;
if !is_mod_or_admin {
return Err(APIError::err("not_a_mod_or_admin").into());
}
Ok(())
}
pub async fn is_admin(pool: &DbPool, user_id: i32) -> Result<(), LemmyError> {
let user = blocking(pool, move |conn| User_::read(conn, user_id)).await??;
if !user.admin {
return Err(APIError::err("not_an_admin").into());
return Err(ApiError::err("not_a_mod_or_admin").into());
}
Ok(())
}
pub(crate) async fn get_post(post_id: i32, pool: &DbPool) -> Result<Post, LemmyError> {
pub fn is_admin(local_user_view: &LocalUserView) -> Result<(), LemmyError> {
if !local_user_view.local_user.admin {
return Err(ApiError::err("not_an_admin").into());
}
Ok(())
}
pub(crate) async fn get_post(post_id: PostId, pool: &DbPool) -> Result<Post, LemmyError> {
match blocking(pool, move |conn| Post::read(conn, post_id)).await? {
Ok(post) => Ok(post),
Err(_e) => Err(APIError::err("couldnt_find_post").into()),
Err(_e) => Err(ApiError::err("couldnt_find_post").into()),
}
}
pub(crate) async fn get_user_from_jwt(jwt: &str, pool: &DbPool) -> Result<User_, LemmyError> {
pub(crate) async fn get_local_user_view_from_jwt(
jwt: &str,
pool: &DbPool,
) -> Result<LocalUserView, LemmyError> {
let claims = match Claims::decode(&jwt) {
Ok(claims) => claims.claims,
Err(_e) => return Err(APIError::err("not_logged_in").into()),
Err(_e) => return Err(ApiError::err("not_logged_in").into()),
};
let user_id = claims.id;
let user = blocking(pool, move |conn| User_::read(conn, user_id)).await??;
let local_user_id = LocalUserId(claims.sub);
let local_user_view =
blocking(pool, move |conn| LocalUserView::read(conn, local_user_id)).await??;
// Check for a site ban
if user.banned {
return Err(APIError::err("site_ban").into());
}
Ok(user)
if local_user_view.person.banned {
return Err(ApiError::err("site_ban").into());
}
pub(crate) async fn get_user_from_jwt_opt(
check_validator_time(&local_user_view.local_user.validator_time, &claims)?;
Ok(local_user_view)
}
/// Checks if user's token was issued before user's password reset.
pub(crate) fn check_validator_time(
validator_time: &chrono::NaiveDateTime,
claims: &Claims,
) -> Result<(), LemmyError> {
let user_validation_time = validator_time.timestamp();
if user_validation_time > claims.iat {
Err(ApiError::err("not_logged_in").into())
} else {
Ok(())
}
}
pub(crate) async fn get_local_user_view_from_jwt_opt(
jwt: &Option<String>,
pool: &DbPool,
) -> Result<Option<User_>, LemmyError> {
) -> Result<Option<LocalUserView>, LemmyError> {
match jwt {
Some(jwt) => Ok(Some(get_user_from_jwt(jwt, pool).await?)),
Some(jwt) => Ok(Some(get_local_user_view_from_jwt(jwt, pool).await?)),
None => Ok(None),
}
}
pub(crate) async fn get_local_user_settings_view_from_jwt(
jwt: &str,
pool: &DbPool,
) -> Result<LocalUserSettingsView, LemmyError> {
let claims = match Claims::decode(&jwt) {
Ok(claims) => claims.claims,
Err(_e) => return Err(ApiError::err("not_logged_in").into()),
};
let local_user_id = LocalUserId(claims.sub);
let local_user_view = blocking(pool, move |conn| {
LocalUserSettingsView::read(conn, local_user_id)
})
.await??;
// Check for a site ban
if local_user_view.person.banned {
return Err(ApiError::err("site_ban").into());
}
check_validator_time(&local_user_view.local_user.validator_time, &claims)?;
Ok(local_user_view)
}
pub(crate) async fn get_local_user_settings_view_from_jwt_opt(
jwt: &Option<String>,
pool: &DbPool,
) -> Result<Option<LocalUserSettingsView>, LemmyError> {
match jwt {
Some(jwt) => Ok(Some(
get_local_user_settings_view_from_jwt(jwt, pool).await?,
)),
None => Ok(None),
}
}
pub(crate) async fn check_community_ban(
user_id: i32,
community_id: i32,
person_id: PersonId,
community_id: CommunityId,
pool: &DbPool,
) -> Result<(), LemmyError> {
let is_banned = move |conn: &'_ _| CommunityUserBanView::get(conn, user_id, community_id).is_ok();
let is_banned =
move |conn: &'_ _| CommunityPersonBanView::get(conn, person_id, community_id).is_ok();
if blocking(pool, is_banned).await? {
Err(APIError::err("community_ban").into())
Err(ApiError::err("community_ban").into())
} else {
Ok(())
}
@ -114,7 +189,7 @@ pub(crate) async fn check_downvotes_enabled(score: i16, pool: &DbPool) -> Result
if score == -1 {
let site = blocking(pool, move |conn| Site::read_simple(conn)).await??;
if !site.enable_downvotes {
return Err(APIError::err("downvotes_disabled").into());
return Err(ApiError::err("downvotes_disabled").into());
}
}
Ok(())
@ -124,63 +199,64 @@ pub(crate) async fn check_downvotes_enabled(score: i16, pool: &DbPool) -> Result
/// or if a community_id is supplied validates the user is a moderator
/// of that community and returns the community id in a vec
///
/// * `user_id` - the user id of the moderator
/// * `person_id` - the person id of the moderator
/// * `community_id` - optional community id to check for moderator privileges
/// * `pool` - the diesel db pool
pub(crate) async fn collect_moderated_communities(
user_id: i32,
community_id: Option<i32>,
person_id: PersonId,
community_id: Option<CommunityId>,
pool: &DbPool,
) -> Result<Vec<i32>, LemmyError> {
) -> Result<Vec<CommunityId>, LemmyError> {
if let Some(community_id) = community_id {
// if the user provides a community_id, just check for mod/admin privileges
is_mod_or_admin(pool, user_id, community_id).await?;
is_mod_or_admin(pool, person_id, community_id).await?;
Ok(vec![community_id])
} else {
let ids = blocking(pool, move |conn: &'_ _| {
CommunityModerator::get_user_moderated_communities(conn, user_id)
CommunityModerator::get_person_moderated_communities(conn, person_id)
})
.await??;
Ok(ids)
}
}
pub(crate) fn check_optional_url(item: &Option<Option<String>>) -> Result<(), LemmyError> {
if let Some(Some(item)) = &item {
if Url::parse(item).is_err() {
return Err(APIError::err("invalid_url").into());
}
}
Ok(())
}
pub(crate) async fn linked_instances(pool: &DbPool) -> Result<Vec<String>, LemmyError> {
let mut instances: Vec<String> = Vec::new();
if Settings::get().federation.enabled {
pub(crate) async fn build_federated_instances(
pool: &DbPool,
) -> Result<Option<FederatedInstances>, LemmyError> {
if Settings::get().federation().enabled {
let distinct_communities = blocking(pool, move |conn| {
Community::distinct_federated_communities(conn)
})
.await??;
instances = distinct_communities
let allowed = Settings::get().get_allowed_instances();
let blocked = Settings::get().get_blocked_instances();
let mut linked = distinct_communities
.iter()
.map(|actor_id| Ok(Url::parse(actor_id)?.host_str().unwrap_or("").to_string()))
.collect::<Result<Vec<String>, LemmyError>>()?;
instances.append(&mut Settings::get().get_allowed_instances());
instances.retain(|a| {
!Settings::get().get_blocked_instances().contains(a)
&& !a.eq("")
&& !a.eq(&Settings::get().hostname)
});
// Sort and remove dupes
instances.sort_unstable();
instances.dedup();
if let Some(allowed) = allowed.as_ref() {
linked.extend_from_slice(allowed);
}
Ok(instances)
if let Some(blocked) = blocked.as_ref() {
linked.retain(|a| !blocked.contains(a) && !a.eq(&Settings::get().hostname()));
}
// Sort and remove dupes
linked.sort_unstable();
linked.dedup();
Ok(Some(FederatedInstances {
linked,
allowed,
blocked,
}))
} else {
Ok(None)
}
}
pub async fn match_websocket_operation(
@ -194,17 +270,17 @@ pub async fn match_websocket_operation(
UserOperation::Login => do_websocket_operation::<Login>(context, id, op, data).await,
UserOperation::Register => do_websocket_operation::<Register>(context, id, op, data).await,
UserOperation::GetCaptcha => do_websocket_operation::<GetCaptcha>(context, id, op, data).await,
UserOperation::GetUserDetails => {
do_websocket_operation::<GetUserDetails>(context, id, op, data).await
UserOperation::GetPersonDetails => {
do_websocket_operation::<GetPersonDetails>(context, id, op, data).await
}
UserOperation::GetReplies => do_websocket_operation::<GetReplies>(context, id, op, data).await,
UserOperation::AddAdmin => do_websocket_operation::<AddAdmin>(context, id, op, data).await,
UserOperation::BanUser => do_websocket_operation::<BanUser>(context, id, op, data).await,
UserOperation::GetUserMentions => {
do_websocket_operation::<GetUserMentions>(context, id, op, data).await
UserOperation::BanPerson => do_websocket_operation::<BanPerson>(context, id, op, data).await,
UserOperation::GetPersonMentions => {
do_websocket_operation::<GetPersonMentions>(context, id, op, data).await
}
UserOperation::MarkUserMentionAsRead => {
do_websocket_operation::<MarkUserMentionAsRead>(context, id, op, data).await
UserOperation::MarkPersonMentionAsRead => {
do_websocket_operation::<MarkPersonMentionAsRead>(context, id, op, data).await
}
UserOperation::MarkAllAsRead => {
do_websocket_operation::<MarkAllAsRead>(context, id, op, data).await
@ -266,9 +342,6 @@ pub async fn match_websocket_operation(
UserOperation::TransferSite => {
do_websocket_operation::<TransferSite>(context, id, op, data).await
}
UserOperation::ListCategories => {
do_websocket_operation::<ListCategories>(context, id, op, data).await
}
// Community ops
UserOperation::GetCommunity => {
@ -405,7 +478,11 @@ pub(crate) fn captcha_espeak_wav_base64(captcha: &str) -> Result<String, LemmyEr
pub(crate) fn espeak_wav_base64(text: &str) -> Result<String, LemmyError> {
// Make a temp file path
let uuid = uuid::Uuid::new_v4().to_string();
let file_path = format!("/tmp/lemmy_espeak_{}.wav", &uuid);
let file_path = format!(
"{}/lemmy_espeak_{}.wav",
env::temp_dir().to_string_lossy(),
&uuid
);
// Write the wav file
Command::new("espeak")
@ -426,9 +503,81 @@ pub(crate) fn espeak_wav_base64(text: &str) -> Result<String, LemmyError> {
Ok(base64)
}
/// Checks the password length
pub(crate) fn password_length_check(pass: &str) -> Result<(), LemmyError> {
if pass.len() > 60 {
Err(ApiError::err("invalid_password").into())
} else {
Ok(())
}
}
#[cfg(test)]
mod tests {
use crate::captcha_espeak_wav_base64;
use crate::{captcha_espeak_wav_base64, check_validator_time};
use lemmy_db_queries::{establish_unpooled_connection, source::local_user::LocalUser_, Crud};
use lemmy_db_schema::source::{
local_user::{LocalUser, LocalUserForm},
person::{Person, PersonForm},
};
use lemmy_utils::claims::Claims;
#[test]
fn test_should_not_validate_user_token_after_password_change() {
let conn = establish_unpooled_connection();
let new_person = PersonForm {
name: "Gerry9812".into(),
preferred_username: None,
avatar: None,
banner: None,
banned: None,
deleted: None,
published: None,
updated: None,
actor_id: None,
bio: None,
local: None,
private_key: None,
public_key: None,
last_refreshed_at: None,
inbox_url: None,
shared_inbox_url: None,
};
let inserted_person = Person::create(&conn, &new_person).unwrap();
let local_user_form = LocalUserForm {
person_id: inserted_person.id,
email: None,
matrix_user_id: None,
password_encrypted: "123456".to_string(),
admin: None,
show_nsfw: None,
theme: None,
default_sort_type: None,
default_listing_type: None,
lang: None,
show_avatars: None,
send_notifications_to_email: None,
};
let inserted_local_user = LocalUser::create(&conn, &local_user_form).unwrap();
let jwt = Claims::jwt(inserted_local_user.id.0).unwrap();
let claims = Claims::decode(&jwt).unwrap().claims;
let check = check_validator_time(&inserted_local_user.validator_time, &claims);
assert!(check.is_ok());
// The check should fail, since the validator time is now newer than the jwt issue time
let updated_local_user =
LocalUser::update_password(&conn, inserted_local_user.id, &"password111").unwrap();
let check_after = check_validator_time(&updated_local_user.validator_time, &claims);
assert!(check_after.is_err());
let num_deleted = Person::delete(&conn, inserted_person.id).unwrap();
assert_eq!(1, num_deleted);
}
#[test]
fn test_espeak() {

File diff suppressed because it is too large Load diff

View file

@ -1,15 +1,15 @@
use crate::{
check_community_ban,
check_downvotes_enabled,
check_optional_url,
collect_moderated_communities,
get_user_from_jwt,
get_user_from_jwt_opt,
get_local_user_view_from_jwt,
get_local_user_view_from_jwt_opt,
is_mod_or_admin,
Perform,
};
use actix_web::web::Data;
use lemmy_apub::{ApubLikeableType, ApubObjectType};
use lemmy_api_structs::{blocking, post::*};
use lemmy_apub::{generate_apub_endpoint, ApubLikeableType, ApubObjectType, EndpointType};
use lemmy_db_queries::{
source::post::Post_,
Crud,
@ -36,17 +36,15 @@ use lemmy_db_views_actor::{
community_moderator_view::CommunityModeratorView,
community_view::CommunityView,
};
use lemmy_structs::{blocking, post::*};
use lemmy_utils::{
apub::{make_apub_endpoint, EndpointType},
request::fetch_iframely_and_pictrs_data,
utils::{check_slurs, check_slurs_opt, is_valid_post_title},
APIError,
ApiError,
ConnectionId,
LemmyError,
};
use lemmy_websocket::{
messages::{GetPostUsersOnline, JoinPostRoom, SendModRoomMessage, SendPost, SendUserRoomMessage},
messages::{GetPostUsersOnline, SendModRoomMessage, SendPost, SendUserRoomMessage},
LemmyContext,
UserOperation,
};
@ -62,29 +60,28 @@ impl Perform for CreatePost {
websocket_id: Option<ConnectionId>,
) -> Result<PostResponse, LemmyError> {
let data: &CreatePost = &self;
let user = get_user_from_jwt(&data.auth, context.pool()).await?;
let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
check_slurs(&data.name)?;
check_slurs_opt(&data.body)?;
if !is_valid_post_title(&data.name) {
return Err(APIError::err("invalid_post_title").into());
return Err(ApiError::err("invalid_post_title").into());
}
check_community_ban(user.id, data.community_id, context.pool()).await?;
check_optional_url(&Some(data.url.to_owned()))?;
check_community_ban(local_user_view.person.id, data.community_id, context.pool()).await?;
// Fetch Iframely and pictrs cached image
let data_url = data.url.as_ref();
let (iframely_title, iframely_description, iframely_html, pictrs_thumbnail) =
fetch_iframely_and_pictrs_data(context.client(), data.url.to_owned()).await;
fetch_iframely_and_pictrs_data(context.client(), data_url).await;
let post_form = PostForm {
name: data.name.trim().to_owned(),
url: data.url.to_owned(),
url: data_url.map(|u| u.to_owned().into()),
body: data.body.to_owned(),
community_id: data.community_id,
creator_id: user.id,
creator_id: local_user_view.person.id,
removed: None,
deleted: None,
nsfw: data.nsfw,
@ -94,7 +91,7 @@ impl Perform for CreatePost {
embed_title: iframely_title,
embed_description: iframely_description,
embed_html: iframely_html,
thumbnail_url: pictrs_thumbnail,
thumbnail_url: pictrs_thumbnail.map(|u| u.into()),
ap_id: None,
local: true,
published: None,
@ -110,47 +107,50 @@ impl Perform for CreatePost {
"couldnt_create_post"
};
return Err(APIError::err(err_type).into());
return Err(ApiError::err(err_type).into());
}
};
let inserted_post_id = inserted_post.id;
let updated_post = match blocking(context.pool(), move |conn| {
let apub_id =
make_apub_endpoint(EndpointType::Post, &inserted_post_id.to_string()).to_string();
Post::update_ap_id(conn, inserted_post_id, apub_id)
let updated_post = match blocking(context.pool(), move |conn| -> Result<Post, LemmyError> {
let apub_id = generate_apub_endpoint(EndpointType::Post, &inserted_post_id.to_string())?;
Ok(Post::update_ap_id(conn, inserted_post_id, apub_id)?)
})
.await?
{
Ok(post) => post,
Err(_e) => return Err(APIError::err("couldnt_create_post").into()),
Err(_e) => return Err(ApiError::err("couldnt_create_post").into()),
};
updated_post.send_create(&user, context).await?;
updated_post
.send_create(&local_user_view.person, context)
.await?;
// They like their own post by default
let like_form = PostLikeForm {
post_id: inserted_post.id,
user_id: user.id,
person_id: local_user_view.person.id,
score: 1,
};
let like = move |conn: &'_ _| PostLike::like(conn, &like_form);
if blocking(context.pool(), like).await?.is_err() {
return Err(APIError::err("couldnt_like_post").into());
return Err(ApiError::err("couldnt_like_post").into());
}
updated_post.send_like(&user, context).await?;
updated_post
.send_like(&local_user_view.person, context)
.await?;
// Refetch the view
let inserted_post_id = inserted_post.id;
let post_view = match blocking(context.pool(), move |conn| {
PostView::read(conn, inserted_post_id, Some(user.id))
PostView::read(conn, inserted_post_id, Some(local_user_view.person.id))
})
.await?
{
Ok(post) => post,
Err(_e) => return Err(APIError::err("couldnt_find_post").into()),
Err(_e) => return Err(ApiError::err("couldnt_find_post").into()),
};
let res = PostResponse { post_view };
@ -175,23 +175,23 @@ impl Perform for GetPost {
_websocket_id: Option<ConnectionId>,
) -> Result<GetPostResponse, LemmyError> {
let data: &GetPost = &self;
let user = get_user_from_jwt_opt(&data.auth, context.pool()).await?;
let user_id = user.map(|u| u.id);
let local_user_view = get_local_user_view_from_jwt_opt(&data.auth, context.pool()).await?;
let person_id = local_user_view.map(|u| u.person.id);
let id = data.id;
let post_view = match blocking(context.pool(), move |conn| {
PostView::read(conn, id, user_id)
PostView::read(conn, id, person_id)
})
.await?
{
Ok(post) => post,
Err(_e) => return Err(APIError::err("couldnt_find_post").into()),
Err(_e) => return Err(ApiError::err("couldnt_find_post").into()),
};
let id = data.id;
let comments = blocking(context.pool(), move |conn| {
CommentQueryBuilder::create(conn)
.my_user_id(user_id)
.my_person_id(person_id)
.post_id(id)
.limit(9999)
.list()
@ -206,12 +206,12 @@ impl Perform for GetPost {
// Necessary for the sidebar
let community_view = match blocking(context.pool(), move |conn| {
CommunityView::read(conn, community_id, user_id)
CommunityView::read(conn, community_id, person_id)
})
.await?
{
Ok(community) => community,
Err(_e) => return Err(APIError::err("couldnt_find_community").into()),
Err(_e) => return Err(ApiError::err("couldnt_find_community").into()),
};
let online = context
@ -241,15 +241,15 @@ impl Perform for GetPosts {
_websocket_id: Option<ConnectionId>,
) -> Result<GetPostsResponse, LemmyError> {
let data: &GetPosts = &self;
let user = get_user_from_jwt_opt(&data.auth, context.pool()).await?;
let local_user_view = get_local_user_view_from_jwt_opt(&data.auth, context.pool()).await?;
let user_id = match &user {
Some(user) => Some(user.id),
let person_id = match &local_user_view {
Some(uv) => Some(uv.person.id),
None => None,
};
let show_nsfw = match &user {
Some(user) => user.show_nsfw,
let show_nsfw = match &local_user_view {
Some(uv) => uv.local_user.show_nsfw,
None => false,
};
@ -267,7 +267,7 @@ impl Perform for GetPosts {
.show_nsfw(show_nsfw)
.community_id(community_id)
.community_name(community_name)
.my_user_id(user_id)
.my_person_id(person_id)
.page(page)
.limit(limit)
.list()
@ -275,7 +275,7 @@ impl Perform for GetPosts {
.await?
{
Ok(posts) => posts,
Err(_e) => return Err(APIError::err("couldnt_get_posts").into()),
Err(_e) => return Err(ApiError::err("couldnt_get_posts").into()),
};
Ok(GetPostsResponse { posts })
@ -292,7 +292,7 @@ impl Perform for CreatePostLike {
websocket_id: Option<ConnectionId>,
) -> Result<PostResponse, LemmyError> {
let data: &CreatePostLike = &self;
let user = get_user_from_jwt(&data.auth, context.pool()).await?;
let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
// Don't do a downvote if site has downvotes disabled
check_downvotes_enabled(data.score, context.pool()).await?;
@ -301,18 +301,18 @@ impl Perform for CreatePostLike {
let post_id = data.post_id;
let post = blocking(context.pool(), move |conn| Post::read(conn, post_id)).await??;
check_community_ban(user.id, post.community_id, context.pool()).await?;
check_community_ban(local_user_view.person.id, post.community_id, context.pool()).await?;
let like_form = PostLikeForm {
post_id: data.post_id,
user_id: user.id,
person_id: local_user_view.person.id,
score: data.score,
};
// Remove any likes first
let user_id = user.id;
let person_id = local_user_view.person.id;
blocking(context.pool(), move |conn| {
PostLike::remove(conn, user_id, post_id)
PostLike::remove(conn, person_id, post_id)
})
.await??;
@ -322,27 +322,29 @@ impl Perform for CreatePostLike {
let like_form2 = like_form.clone();
let like = move |conn: &'_ _| PostLike::like(conn, &like_form2);
if blocking(context.pool(), like).await?.is_err() {
return Err(APIError::err("couldnt_like_post").into());
return Err(ApiError::err("couldnt_like_post").into());
}
if like_form.score == 1 {
post.send_like(&user, context).await?;
post.send_like(&local_user_view.person, context).await?;
} else if like_form.score == -1 {
post.send_dislike(&user, context).await?;
post.send_dislike(&local_user_view.person, context).await?;
}
} else {
post.send_undo_like(&user, context).await?;
post
.send_undo_like(&local_user_view.person, context)
.await?;
}
let post_id = data.post_id;
let user_id = user.id;
let person_id = local_user_view.person.id;
let post_view = match blocking(context.pool(), move |conn| {
PostView::read(conn, post_id, Some(user_id))
PostView::read(conn, post_id, Some(person_id))
})
.await?
{
Ok(post) => post,
Err(_e) => return Err(APIError::err("couldnt_find_post").into()),
Err(_e) => return Err(ApiError::err("couldnt_find_post").into()),
};
let res = PostResponse { post_view };
@ -367,32 +369,38 @@ impl Perform for EditPost {
websocket_id: Option<ConnectionId>,
) -> Result<PostResponse, LemmyError> {
let data: &EditPost = &self;
let user = get_user_from_jwt(&data.auth, context.pool()).await?;
let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
check_slurs(&data.name)?;
check_slurs_opt(&data.body)?;
if !is_valid_post_title(&data.name) {
return Err(APIError::err("invalid_post_title").into());
return Err(ApiError::err("invalid_post_title").into());
}
let edit_id = data.edit_id;
let orig_post = blocking(context.pool(), move |conn| Post::read(conn, edit_id)).await??;
let post_id = data.post_id;
let orig_post = blocking(context.pool(), move |conn| Post::read(conn, post_id)).await??;
check_community_ban(user.id, orig_post.community_id, context.pool()).await?;
check_community_ban(
local_user_view.person.id,
orig_post.community_id,
context.pool(),
)
.await?;
// Verify that only the creator can edit
if !Post::is_post_creator(user.id, orig_post.creator_id) {
return Err(APIError::err("no_post_edit_allowed").into());
if !Post::is_post_creator(local_user_view.person.id, orig_post.creator_id) {
return Err(ApiError::err("no_post_edit_allowed").into());
}
// Fetch Iframely and Pictrs cached image
let data_url = data.url.as_ref();
let (iframely_title, iframely_description, iframely_html, pictrs_thumbnail) =
fetch_iframely_and_pictrs_data(context.client(), data.url.to_owned()).await;
fetch_iframely_and_pictrs_data(context.client(), data_url).await;
let post_form = PostForm {
name: data.name.trim().to_owned(),
url: data.url.to_owned(),
url: data_url.map(|u| u.to_owned().into()),
body: data.body.to_owned(),
nsfw: data.nsfw,
creator_id: orig_post.creator_id.to_owned(),
@ -405,15 +413,15 @@ impl Perform for EditPost {
embed_title: iframely_title,
embed_description: iframely_description,
embed_html: iframely_html,
thumbnail_url: pictrs_thumbnail,
thumbnail_url: pictrs_thumbnail.map(|u| u.into()),
ap_id: Some(orig_post.ap_id),
local: orig_post.local,
published: None,
};
let edit_id = data.edit_id;
let post_id = data.post_id;
let res = blocking(context.pool(), move |conn| {
Post::update(conn, edit_id, &post_form)
Post::update(conn, post_id, &post_form)
})
.await?;
let updated_post: Post = match res {
@ -425,16 +433,18 @@ impl Perform for EditPost {
"couldnt_update_post"
};
return Err(APIError::err(err_type).into());
return Err(ApiError::err(err_type).into());
}
};
// Send apub update
updated_post.send_update(&user, context).await?;
updated_post
.send_update(&local_user_view.person, context)
.await?;
let edit_id = data.edit_id;
let post_id = data.post_id;
let post_view = blocking(context.pool(), move |conn| {
PostView::read(conn, edit_id, Some(user.id))
PostView::read(conn, post_id, Some(local_user_view.person.id))
})
.await??;
@ -460,37 +470,46 @@ impl Perform for DeletePost {
websocket_id: Option<ConnectionId>,
) -> Result<PostResponse, LemmyError> {
let data: &DeletePost = &self;
let user = get_user_from_jwt(&data.auth, context.pool()).await?;
let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
let edit_id = data.edit_id;
let orig_post = blocking(context.pool(), move |conn| Post::read(conn, edit_id)).await??;
let post_id = data.post_id;
let orig_post = blocking(context.pool(), move |conn| Post::read(conn, post_id)).await??;
check_community_ban(user.id, orig_post.community_id, context.pool()).await?;
check_community_ban(
local_user_view.person.id,
orig_post.community_id,
context.pool(),
)
.await?;
// Verify that only the creator can delete
if !Post::is_post_creator(user.id, orig_post.creator_id) {
return Err(APIError::err("no_post_edit_allowed").into());
if !Post::is_post_creator(local_user_view.person.id, orig_post.creator_id) {
return Err(ApiError::err("no_post_edit_allowed").into());
}
// Update the post
let edit_id = data.edit_id;
let post_id = data.post_id;
let deleted = data.deleted;
let updated_post = blocking(context.pool(), move |conn| {
Post::update_deleted(conn, edit_id, deleted)
Post::update_deleted(conn, post_id, deleted)
})
.await??;
// apub updates
if deleted {
updated_post.send_delete(&user, context).await?;
updated_post
.send_delete(&local_user_view.person, context)
.await?;
} else {
updated_post.send_undo_delete(&user, context).await?;
updated_post
.send_undo_delete(&local_user_view.person, context)
.await?;
}
// Refetch the post
let edit_id = data.edit_id;
let post_id = data.post_id;
let post_view = blocking(context.pool(), move |conn| {
PostView::read(conn, edit_id, Some(user.id))
PostView::read(conn, post_id, Some(local_user_view.person.id))
})
.await??;
@ -516,28 +535,38 @@ impl Perform for RemovePost {
websocket_id: Option<ConnectionId>,
) -> Result<PostResponse, LemmyError> {
let data: &RemovePost = &self;
let user = get_user_from_jwt(&data.auth, context.pool()).await?;
let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
let edit_id = data.edit_id;
let orig_post = blocking(context.pool(), move |conn| Post::read(conn, edit_id)).await??;
let post_id = data.post_id;
let orig_post = blocking(context.pool(), move |conn| Post::read(conn, post_id)).await??;
check_community_ban(user.id, orig_post.community_id, context.pool()).await?;
check_community_ban(
local_user_view.person.id,
orig_post.community_id,
context.pool(),
)
.await?;
// Verify that only the mods can remove
is_mod_or_admin(context.pool(), user.id, orig_post.community_id).await?;
is_mod_or_admin(
context.pool(),
local_user_view.person.id,
orig_post.community_id,
)
.await?;
// Update the post
let edit_id = data.edit_id;
let post_id = data.post_id;
let removed = data.removed;
let updated_post = blocking(context.pool(), move |conn| {
Post::update_removed(conn, edit_id, removed)
Post::update_removed(conn, post_id, removed)
})
.await??;
// Mod tables
let form = ModRemovePostForm {
mod_user_id: user.id,
post_id: data.edit_id,
mod_person_id: local_user_view.person.id,
post_id: data.post_id,
removed: Some(removed),
reason: data.reason.to_owned(),
};
@ -548,16 +577,20 @@ impl Perform for RemovePost {
// apub updates
if removed {
updated_post.send_remove(&user, context).await?;
updated_post
.send_remove(&local_user_view.person, context)
.await?;
} else {
updated_post.send_undo_remove(&user, context).await?;
updated_post
.send_undo_remove(&local_user_view.person, context)
.await?;
}
// Refetch the post
let edit_id = data.edit_id;
let user_id = user.id;
let post_id = data.post_id;
let person_id = local_user_view.person.id;
let post_view = blocking(context.pool(), move |conn| {
PostView::read(conn, edit_id, Some(user_id))
PostView::read(conn, post_id, Some(person_id))
})
.await??;
@ -583,39 +616,51 @@ impl Perform for LockPost {
websocket_id: Option<ConnectionId>,
) -> Result<PostResponse, LemmyError> {
let data: &LockPost = &self;
let user = get_user_from_jwt(&data.auth, context.pool()).await?;
let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
let edit_id = data.edit_id;
let orig_post = blocking(context.pool(), move |conn| Post::read(conn, edit_id)).await??;
let post_id = data.post_id;
let orig_post = blocking(context.pool(), move |conn| Post::read(conn, post_id)).await??;
check_community_ban(user.id, orig_post.community_id, context.pool()).await?;
check_community_ban(
local_user_view.person.id,
orig_post.community_id,
context.pool(),
)
.await?;
// Verify that only the mods can lock
is_mod_or_admin(context.pool(), user.id, orig_post.community_id).await?;
is_mod_or_admin(
context.pool(),
local_user_view.person.id,
orig_post.community_id,
)
.await?;
// Update the post
let edit_id = data.edit_id;
let post_id = data.post_id;
let locked = data.locked;
let updated_post = blocking(context.pool(), move |conn| {
Post::update_locked(conn, edit_id, locked)
Post::update_locked(conn, post_id, locked)
})
.await??;
// Mod tables
let form = ModLockPostForm {
mod_user_id: user.id,
post_id: data.edit_id,
mod_person_id: local_user_view.person.id,
post_id: data.post_id,
locked: Some(locked),
};
blocking(context.pool(), move |conn| ModLockPost::create(conn, &form)).await??;
// apub updates
updated_post.send_update(&user, context).await?;
updated_post
.send_update(&local_user_view.person, context)
.await?;
// Refetch the post
let edit_id = data.edit_id;
let post_id = data.post_id;
let post_view = blocking(context.pool(), move |conn| {
PostView::read(conn, edit_id, Some(user.id))
PostView::read(conn, post_id, Some(local_user_view.person.id))
})
.await??;
@ -641,28 +686,38 @@ impl Perform for StickyPost {
websocket_id: Option<ConnectionId>,
) -> Result<PostResponse, LemmyError> {
let data: &StickyPost = &self;
let user = get_user_from_jwt(&data.auth, context.pool()).await?;
let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
let edit_id = data.edit_id;
let orig_post = blocking(context.pool(), move |conn| Post::read(conn, edit_id)).await??;
let post_id = data.post_id;
let orig_post = blocking(context.pool(), move |conn| Post::read(conn, post_id)).await??;
check_community_ban(user.id, orig_post.community_id, context.pool()).await?;
check_community_ban(
local_user_view.person.id,
orig_post.community_id,
context.pool(),
)
.await?;
// Verify that only the mods can sticky
is_mod_or_admin(context.pool(), user.id, orig_post.community_id).await?;
is_mod_or_admin(
context.pool(),
local_user_view.person.id,
orig_post.community_id,
)
.await?;
// Update the post
let edit_id = data.edit_id;
let post_id = data.post_id;
let stickied = data.stickied;
let updated_post = blocking(context.pool(), move |conn| {
Post::update_stickied(conn, edit_id, stickied)
Post::update_stickied(conn, post_id, stickied)
})
.await??;
// Mod tables
let form = ModStickyPostForm {
mod_user_id: user.id,
post_id: data.edit_id,
mod_person_id: local_user_view.person.id,
post_id: data.post_id,
stickied: Some(stickied),
};
blocking(context.pool(), move |conn| {
@ -672,12 +727,14 @@ impl Perform for StickyPost {
// Apub updates
// TODO stickied should pry work like locked for ease of use
updated_post.send_update(&user, context).await?;
updated_post
.send_update(&local_user_view.person, context)
.await?;
// Refetch the post
let edit_id = data.edit_id;
let post_id = data.post_id;
let post_view = blocking(context.pool(), move |conn| {
PostView::read(conn, edit_id, Some(user.id))
PostView::read(conn, post_id, Some(local_user_view.person.id))
})
.await??;
@ -703,29 +760,29 @@ impl Perform for SavePost {
_websocket_id: Option<ConnectionId>,
) -> Result<PostResponse, LemmyError> {
let data: &SavePost = &self;
let user = get_user_from_jwt(&data.auth, context.pool()).await?;
let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
let post_saved_form = PostSavedForm {
post_id: data.post_id,
user_id: user.id,
person_id: local_user_view.person.id,
};
if data.save {
let save = move |conn: &'_ _| PostSaved::save(conn, &post_saved_form);
if blocking(context.pool(), save).await?.is_err() {
return Err(APIError::err("couldnt_save_post").into());
return Err(ApiError::err("couldnt_save_post").into());
}
} else {
let unsave = move |conn: &'_ _| PostSaved::unsave(conn, &post_saved_form);
if blocking(context.pool(), unsave).await?.is_err() {
return Err(APIError::err("couldnt_save_post").into());
return Err(ApiError::err("couldnt_save_post").into());
}
}
let post_id = data.post_id;
let user_id = user.id;
let person_id = local_user_view.person.id;
let post_view = blocking(context.pool(), move |conn| {
PostView::read(conn, post_id, Some(user_id))
PostView::read(conn, post_id, Some(person_id))
})
.await??;
@ -733,28 +790,6 @@ impl Perform for SavePost {
}
}
#[async_trait::async_trait(?Send)]
impl Perform for PostJoin {
type Response = PostJoinResponse;
async fn perform(
&self,
context: &Data<LemmyContext>,
websocket_id: Option<ConnectionId>,
) -> Result<PostJoinResponse, LemmyError> {
let data: &PostJoin = &self;
if let Some(ws_id) = websocket_id {
context.chat_server().do_send(JoinPostRoom {
post_id: data.post_id,
id: ws_id,
});
}
Ok(PostJoinResponse { joined: true })
}
}
/// Creates a post report and notifies the moderators of the community
#[async_trait::async_trait(?Send)]
impl Perform for CreatePostReport {
@ -766,28 +801,28 @@ impl Perform for CreatePostReport {
websocket_id: Option<ConnectionId>,
) -> Result<CreatePostReportResponse, LemmyError> {
let data: &CreatePostReport = &self;
let user = get_user_from_jwt(&data.auth, context.pool()).await?;
let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
// check size of report and check for whitespace
let reason = data.reason.trim();
if reason.is_empty() {
return Err(APIError::err("report_reason_required").into());
return Err(ApiError::err("report_reason_required").into());
}
if reason.len() > 1000 {
return Err(APIError::err("report_too_long").into());
if reason.chars().count() > 1000 {
return Err(ApiError::err("report_too_long").into());
}
let user_id = user.id;
let person_id = local_user_view.person.id;
let post_id = data.post_id;
let post_view = blocking(context.pool(), move |conn| {
PostView::read(&conn, post_id, None)
})
.await??;
check_community_ban(user_id, post_view.community.id, context.pool()).await?;
check_community_ban(person_id, post_view.community.id, context.pool()).await?;
let report_form = PostReportForm {
creator_id: user_id,
creator_id: person_id,
post_id,
original_post_name: post_view.post.name,
original_post_url: post_view.post.url,
@ -801,7 +836,7 @@ impl Perform for CreatePostReport {
.await?
{
Ok(report) => report,
Err(_e) => return Err(APIError::err("couldnt_create_report").into()),
Err(_e) => return Err(ApiError::err("couldnt_create_report").into()),
};
let res = CreatePostReportResponse { success: true };
@ -809,7 +844,7 @@ impl Perform for CreatePostReport {
context.chat_server().do_send(SendUserRoomMessage {
op: UserOperation::CreatePostReport,
response: res.clone(),
recipient_id: user.id,
local_recipient_id: local_user_view.local_user.id,
websocket_id,
});
@ -835,7 +870,7 @@ impl Perform for ResolvePostReport {
websocket_id: Option<ConnectionId>,
) -> Result<ResolvePostReportResponse, LemmyError> {
let data: &ResolvePostReport = &self;
let user = get_user_from_jwt(&data.auth, context.pool()).await?;
let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
let report_id = data.report_id;
let report = blocking(context.pool(), move |conn| {
@ -843,15 +878,15 @@ impl Perform for ResolvePostReport {
})
.await??;
let user_id = user.id;
is_mod_or_admin(context.pool(), user_id, report.community.id).await?;
let person_id = local_user_view.person.id;
is_mod_or_admin(context.pool(), person_id, report.community.id).await?;
let resolved = data.resolved;
let resolve_fun = move |conn: &'_ _| {
if resolved {
PostReport::resolve(conn, report_id, user_id)
PostReport::resolve(conn, report_id, person_id)
} else {
PostReport::unresolve(conn, report_id, user_id)
PostReport::unresolve(conn, report_id, person_id)
}
};
@ -861,7 +896,7 @@ impl Perform for ResolvePostReport {
};
if blocking(context.pool(), resolve_fun).await?.is_err() {
return Err(APIError::err("couldnt_resolve_report").into());
return Err(ApiError::err("couldnt_resolve_report").into());
};
context.chat_server().do_send(SendModRoomMessage {
@ -887,12 +922,12 @@ impl Perform for ListPostReports {
websocket_id: Option<ConnectionId>,
) -> Result<ListPostReportsResponse, LemmyError> {
let data: &ListPostReports = &self;
let user = get_user_from_jwt(&data.auth, context.pool()).await?;
let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
let user_id = user.id;
let person_id = local_user_view.person.id;
let community_id = data.community;
let community_ids =
collect_moderated_communities(user_id, community_id, context.pool()).await?;
collect_moderated_communities(person_id, community_id, context.pool()).await?;
let page = data.page;
let limit = data.limit;
@ -910,7 +945,7 @@ impl Perform for ListPostReports {
context.chat_server().do_send(SendUserRoomMessage {
op: UserOperation::ListPostReports,
response: res.clone(),
recipient_id: user.id,
local_recipient_id: local_user_view.local_user.id,
websocket_id,
});

View file

@ -1,15 +1,15 @@
use crate::Perform;
use actix_web::{error::ErrorBadRequest, *};
use lemmy_api::Perform;
use lemmy_structs::{comment::*, community::*, post::*, site::*, user::*};
use lemmy_api_structs::{comment::*, community::*, person::*, post::*, site::*, websocket::*};
use lemmy_utils::rate_limit::RateLimit;
use lemmy_websocket::LemmyContext;
use lemmy_websocket::{routes::chat_route, LemmyContext};
use serde::Deserialize;
pub fn config(cfg: &mut web::ServiceConfig, rate_limit: &RateLimit) {
cfg.service(
web::scope("/api/v2")
// Websockets
.service(web::resource("/ws").to(super::websocket::chat_route))
.service(web::resource("/ws").to(chat_route))
// Site
.service(
web::scope("/site")
@ -22,11 +22,6 @@ pub fn config(cfg: &mut web::ServiceConfig, rate_limit: &RateLimit) {
.route("/config", web::get().to(route_get::<GetSiteConfig>))
.route("/config", web::put().to(route_post::<SaveSiteConfig>)),
)
.service(
web::resource("/categories")
.wrap(rate_limit.message())
.route(web::get().to(route_get::<ListCategories>)),
)
.service(
web::resource("/modlog")
.wrap(rate_limit.message())
@ -142,11 +137,11 @@ pub fn config(cfg: &mut web::ServiceConfig, rate_limit: &RateLimit) {
.service(
web::scope("/user")
.wrap(rate_limit.message())
.route("", web::get().to(route_get::<GetUserDetails>))
.route("/mention", web::get().to(route_get::<GetUserMentions>))
.route("", web::get().to(route_get::<GetPersonDetails>))
.route("/mention", web::get().to(route_get::<GetPersonMentions>))
.route(
"/mention/mark_as_read",
web::post().to(route_post::<MarkUserMentionAsRead>),
web::post().to(route_post::<MarkPersonMentionAsRead>),
)
.route("/replies", web::get().to(route_get::<GetReplies>))
.route(
@ -155,7 +150,7 @@ pub fn config(cfg: &mut web::ServiceConfig, rate_limit: &RateLimit) {
)
.route("/join", web::post().to(route_post::<UserJoin>))
// Admin action. I don't like that it's in /user
.route("/ban", web::post().to(route_post::<BanUser>))
.route("/ban", web::post().to(route_post::<BanPerson>))
// Account actions. I don't like that they're in /user maybe /accounts
.route("/login", web::post().to(route_post::<Login>))
.route("/get_captcha", web::get().to(route_get::<GetCaptcha>))

View file

@ -1,17 +1,19 @@
use crate::{
get_user_from_jwt,
get_user_from_jwt_opt,
build_federated_instances,
get_local_user_settings_view_from_jwt,
get_local_user_settings_view_from_jwt_opt,
get_local_user_view_from_jwt,
get_local_user_view_from_jwt_opt,
is_admin,
linked_instances,
version,
Perform,
};
use actix_web::web::Data;
use anyhow::Context;
use lemmy_api_structs::{blocking, person::Register, site::*};
use lemmy_apub::fetcher::search::search_by_apub_id;
use lemmy_db_queries::{
diesel_option_overwrite,
source::{category::Category_, site::Site_},
diesel_option_overwrite_to_url,
source::site::Site_,
Crud,
SearchType,
SortType,
@ -19,7 +21,6 @@ use lemmy_db_queries::{
use lemmy_db_schema::{
naive_now,
source::{
category::Category,
moderator::*,
site::{Site, *},
},
@ -31,7 +32,7 @@ use lemmy_db_views::{
};
use lemmy_db_views_actor::{
community_view::CommunityQueryBuilder,
user_view::{UserQueryBuilder, UserViewSafe},
person_view::{PersonQueryBuilder, PersonViewSafe},
};
use lemmy_db_views_moderator::{
mod_add_community_view::ModAddCommunityView,
@ -44,12 +45,12 @@ use lemmy_db_views_moderator::{
mod_remove_post_view::ModRemovePostView,
mod_sticky_post_view::ModStickyPostView,
};
use lemmy_structs::{blocking, site::*, user::Register};
use lemmy_utils::{
location_info,
settings::Settings,
settings::structs::Settings,
utils::{check_slurs, check_slurs_opt},
APIError,
version,
ApiError,
ConnectionId,
LemmyError,
};
@ -61,24 +62,6 @@ use lemmy_websocket::{
use log::{debug, info};
use std::str::FromStr;
#[async_trait::async_trait(?Send)]
impl Perform for ListCategories {
type Response = ListCategoriesResponse;
async fn perform(
&self,
context: &Data<LemmyContext>,
_websocket_id: Option<ConnectionId>,
) -> Result<ListCategoriesResponse, LemmyError> {
let _data: &ListCategories = &self;
let categories = blocking(context.pool(), move |conn| Category::list_all(conn)).await??;
// Return the jwt
Ok(ListCategoriesResponse { categories })
}
}
#[async_trait::async_trait(?Send)]
impl Perform for GetModlog {
type Response = GetModlogResponse;
@ -91,36 +74,36 @@ impl Perform for GetModlog {
let data: &GetModlog = &self;
let community_id = data.community_id;
let mod_user_id = data.mod_user_id;
let mod_person_id = data.mod_person_id;
let page = data.page;
let limit = data.limit;
let removed_posts = blocking(context.pool(), move |conn| {
ModRemovePostView::list(conn, community_id, mod_user_id, page, limit)
ModRemovePostView::list(conn, community_id, mod_person_id, page, limit)
})
.await??;
let locked_posts = blocking(context.pool(), move |conn| {
ModLockPostView::list(conn, community_id, mod_user_id, page, limit)
ModLockPostView::list(conn, community_id, mod_person_id, page, limit)
})
.await??;
let stickied_posts = blocking(context.pool(), move |conn| {
ModStickyPostView::list(conn, community_id, mod_user_id, page, limit)
ModStickyPostView::list(conn, community_id, mod_person_id, page, limit)
})
.await??;
let removed_comments = blocking(context.pool(), move |conn| {
ModRemoveCommentView::list(conn, community_id, mod_user_id, page, limit)
ModRemoveCommentView::list(conn, community_id, mod_person_id, page, limit)
})
.await??;
let banned_from_community = blocking(context.pool(), move |conn| {
ModBanFromCommunityView::list(conn, community_id, mod_user_id, page, limit)
ModBanFromCommunityView::list(conn, community_id, mod_person_id, page, limit)
})
.await??;
let added_to_community = blocking(context.pool(), move |conn| {
ModAddCommunityView::list(conn, community_id, mod_user_id, page, limit)
ModAddCommunityView::list(conn, community_id, mod_person_id, page, limit)
})
.await??;
@ -128,9 +111,9 @@ impl Perform for GetModlog {
let (removed_communities, banned, added) = if data.community_id.is_none() {
blocking(context.pool(), move |conn| {
Ok((
ModRemoveCommunityView::list(conn, mod_user_id, page, limit)?,
ModBanView::list(conn, mod_user_id, page, limit)?,
ModAddView::list(conn, mod_user_id, page, limit)?,
ModRemoveCommunityView::list(conn, mod_person_id, page, limit)?,
ModBanView::list(conn, mod_person_id, page, limit)?,
ModAddView::list(conn, mod_person_id, page, limit)?,
)) as Result<_, LemmyError>
})
.await??
@ -166,23 +149,23 @@ impl Perform for CreateSite {
let read_site = move |conn: &'_ _| Site::read_simple(conn);
if blocking(context.pool(), read_site).await?.is_ok() {
return Err(APIError::err("site_already_exists").into());
return Err(ApiError::err("site_already_exists").into());
};
let user = get_user_from_jwt(&data.auth, context.pool()).await?;
let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
check_slurs(&data.name)?;
check_slurs_opt(&data.description)?;
// Make sure user is an admin
is_admin(context.pool(), user.id).await?;
is_admin(&local_user_view)?;
let site_form = SiteForm {
name: data.name.to_owned(),
description: data.description.to_owned(),
icon: Some(data.icon.to_owned()),
banner: Some(data.banner.to_owned()),
creator_id: user.id,
icon: Some(data.icon.to_owned().map(|url| url.into())),
banner: Some(data.banner.to_owned().map(|url| url.into())),
creator_id: local_user_view.person.id,
enable_downvotes: data.enable_downvotes,
open_registration: data.open_registration,
enable_nsfw: data.enable_nsfw,
@ -191,7 +174,7 @@ impl Perform for CreateSite {
let create_site = move |conn: &'_ _| Site::create(conn, &site_form);
if blocking(context.pool(), create_site).await?.is_err() {
return Err(APIError::err("site_already_exists").into());
return Err(ApiError::err("site_already_exists").into());
}
let site_view = blocking(context.pool(), move |conn| SiteView::read(conn)).await??;
@ -209,18 +192,18 @@ impl Perform for EditSite {
websocket_id: Option<ConnectionId>,
) -> Result<SiteResponse, LemmyError> {
let data: &EditSite = &self;
let user = get_user_from_jwt(&data.auth, context.pool()).await?;
let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
check_slurs(&data.name)?;
check_slurs_opt(&data.description)?;
// Make sure user is an admin
is_admin(context.pool(), user.id).await?;
is_admin(&local_user_view)?;
let found_site = blocking(context.pool(), move |conn| Site::read_simple(conn)).await??;
let icon = diesel_option_overwrite(&data.icon);
let banner = diesel_option_overwrite(&data.banner);
let icon = diesel_option_overwrite_to_url(&data.icon)?;
let banner = diesel_option_overwrite_to_url(&data.banner)?;
let site_form = SiteForm {
name: data.name.to_owned(),
@ -236,7 +219,7 @@ impl Perform for EditSite {
let update_site = move |conn: &'_ _| Site::update(conn, 1, &site_form);
if blocking(context.pool(), update_site).await?.is_err() {
return Err(APIError::err("couldnt_update_site").into());
return Err(ApiError::err("couldnt_update_site").into());
}
let site_view = blocking(context.pool(), move |conn| SiteView::read(conn)).await??;
@ -268,13 +251,12 @@ impl Perform for GetSite {
Ok(site_view) => Some(site_view),
// If the site isn't created yet, check the setup
Err(_) => {
if let Some(setup) = Settings::get().setup.as_ref() {
if let Some(setup) = Settings::get().setup().as_ref() {
let register = Register {
username: setup.admin_username.to_owned(),
email: setup.admin_email.to_owned(),
password: setup.admin_password.to_owned(),
password_verify: setup.admin_password.to_owned(),
admin: true,
show_nsfw: true,
captcha_uuid: None,
captcha_answer: None,
@ -301,20 +283,20 @@ impl Perform for GetSite {
}
};
let mut admins = blocking(context.pool(), move |conn| UserViewSafe::admins(conn)).await??;
let mut admins = blocking(context.pool(), move |conn| PersonViewSafe::admins(conn)).await??;
// Make sure the site creator is the top admin
if let Some(site_view) = site_view.to_owned() {
let site_creator_id = site_view.creator.id;
// TODO investigate why this is sometimes coming back null
// Maybe user_.admin isn't being set to true?
if let Some(creator_index) = admins.iter().position(|r| r.user.id == site_creator_id) {
let creator_user = admins.remove(creator_index);
admins.insert(0, creator_user);
if let Some(creator_index) = admins.iter().position(|r| r.person.id == site_creator_id) {
let creator_person = admins.remove(creator_index);
admins.insert(0, creator_person);
}
}
let banned = blocking(context.pool(), move |conn| UserViewSafe::banned(conn)).await??;
let banned = blocking(context.pool(), move |conn| PersonViewSafe::banned(conn)).await??;
let online = context
.chat_server()
@ -322,14 +304,8 @@ impl Perform for GetSite {
.await
.unwrap_or(1);
let my_user = get_user_from_jwt_opt(&data.auth, context.pool())
.await?
.map(|mut u| {
u.password_encrypted = "".to_string();
u.private_key = None;
u.public_key = None;
u
});
let my_user = get_local_user_settings_view_from_jwt_opt(&data.auth, context.pool()).await?;
let federated_instances = build_federated_instances(context.pool()).await?;
Ok(GetSiteResponse {
site_view,
@ -338,7 +314,7 @@ impl Perform for GetSite {
online,
version: version::VERSION.to_string(),
my_user,
federated_instances: linked_instances(context.pool()).await?,
federated_instances,
})
}
}
@ -359,8 +335,8 @@ impl Perform for Search {
Err(e) => debug!("Failed to resolve search query as activitypub ID: {}", e),
}
let user = get_user_from_jwt_opt(&data.auth, context.pool()).await?;
let user_id = user.map(|u| u.id);
let local_user_view = get_local_user_view_from_jwt_opt(&data.auth, context.pool()).await?;
let person_id = local_user_view.map(|u| u.person.id);
let type_ = SearchType::from_str(&data.type_)?;
@ -385,7 +361,7 @@ impl Perform for Search {
.show_nsfw(true)
.community_id(community_id)
.community_name(community_name)
.my_user_id(user_id)
.my_person_id(person_id)
.search_term(q)
.page(page)
.limit(limit)
@ -398,7 +374,7 @@ impl Perform for Search {
CommentQueryBuilder::create(&conn)
.sort(&sort)
.search_term(q)
.my_user_id(user_id)
.my_person_id(person_id)
.page(page)
.limit(limit)
.list()
@ -410,6 +386,7 @@ impl Perform for Search {
CommunityQueryBuilder::create(conn)
.sort(&sort)
.search_term(q)
.my_person_id(person_id)
.page(page)
.limit(limit)
.list()
@ -418,7 +395,7 @@ impl Perform for Search {
}
SearchType::Users => {
users = blocking(context.pool(), move |conn| {
UserQueryBuilder::create(conn)
PersonQueryBuilder::create(conn)
.sort(&sort)
.search_term(q)
.page(page)
@ -434,7 +411,7 @@ impl Perform for Search {
.show_nsfw(true)
.community_id(community_id)
.community_name(community_name)
.my_user_id(user_id)
.my_person_id(person_id)
.search_term(q)
.page(page)
.limit(limit)
@ -449,7 +426,7 @@ impl Perform for Search {
CommentQueryBuilder::create(conn)
.sort(&sort)
.search_term(q)
.my_user_id(user_id)
.my_person_id(person_id)
.page(page)
.limit(limit)
.list()
@ -463,6 +440,7 @@ impl Perform for Search {
CommunityQueryBuilder::create(conn)
.sort(&sort)
.search_term(q)
.my_person_id(person_id)
.page(page)
.limit(limit)
.list()
@ -473,7 +451,7 @@ impl Perform for Search {
let sort = SortType::from_str(&data.sort)?;
users = blocking(context.pool(), move |conn| {
UserQueryBuilder::create(conn)
PersonQueryBuilder::create(conn)
.sort(&sort)
.search_term(q)
.page(page)
@ -487,6 +465,7 @@ impl Perform for Search {
PostQueryBuilder::create(conn)
.sort(&sort)
.show_nsfw(true)
.my_person_id(person_id)
.community_id(community_id)
.community_name(community_name)
.url_search(q)
@ -519,32 +498,27 @@ impl Perform for TransferSite {
_websocket_id: Option<ConnectionId>,
) -> Result<GetSiteResponse, LemmyError> {
let data: &TransferSite = &self;
let mut user = get_user_from_jwt(&data.auth, context.pool()).await?;
let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
is_admin(context.pool(), user.id).await?;
// TODO add a User_::read_safe() for this.
user.password_encrypted = "".to_string();
user.private_key = None;
user.public_key = None;
is_admin(&local_user_view)?;
let read_site = blocking(context.pool(), move |conn| Site::read_simple(conn)).await??;
// Make sure user is the creator
if read_site.creator_id != user.id {
return Err(APIError::err("not_an_admin").into());
if read_site.creator_id != local_user_view.person.id {
return Err(ApiError::err("not_an_admin").into());
}
let new_creator_id = data.user_id;
let new_creator_id = data.person_id;
let transfer_site = move |conn: &'_ _| Site::transfer(conn, new_creator_id);
if blocking(context.pool(), transfer_site).await?.is_err() {
return Err(APIError::err("couldnt_update_site").into());
return Err(ApiError::err("couldnt_update_site").into());
};
// Mod tables
let form = ModAddForm {
mod_user_id: user.id,
other_user_id: data.user_id,
mod_person_id: local_user_view.person.id,
other_person_id: data.person_id,
removed: Some(false),
};
@ -552,15 +526,18 @@ impl Perform for TransferSite {
let site_view = blocking(context.pool(), move |conn| SiteView::read(conn)).await??;
let mut admins = blocking(context.pool(), move |conn| UserViewSafe::admins(conn)).await??;
let mut admins = blocking(context.pool(), move |conn| PersonViewSafe::admins(conn)).await??;
let creator_index = admins
.iter()
.position(|r| r.user.id == site_view.creator.id)
.position(|r| r.person.id == site_view.creator.id)
.context(location_info!())?;
let creator_user = admins.remove(creator_index);
admins.insert(0, creator_user);
let creator_person = admins.remove(creator_index);
admins.insert(0, creator_person);
let banned = blocking(context.pool(), move |conn| UserViewSafe::banned(conn)).await??;
let banned = blocking(context.pool(), move |conn| PersonViewSafe::banned(conn)).await??;
let federated_instances = build_federated_instances(context.pool()).await?;
let my_user = Some(get_local_user_settings_view_from_jwt(&data.auth, context.pool()).await?);
Ok(GetSiteResponse {
site_view: Some(site_view),
@ -568,8 +545,8 @@ impl Perform for TransferSite {
banned,
online: 0,
version: version::VERSION.to_string(),
my_user: Some(user),
federated_instances: linked_instances(context.pool()).await?,
my_user,
federated_instances,
})
}
}
@ -584,10 +561,10 @@ impl Perform for GetSiteConfig {
_websocket_id: Option<ConnectionId>,
) -> Result<GetSiteConfigResponse, LemmyError> {
let data: &GetSiteConfig = &self;
let user = get_user_from_jwt(&data.auth, context.pool()).await?;
let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
// Only let admins read this
is_admin(context.pool(), user.id).await?;
is_admin(&local_user_view)?;
let config_hjson = Settings::read_config_file()?;
@ -605,16 +582,15 @@ impl Perform for SaveSiteConfig {
_websocket_id: Option<ConnectionId>,
) -> Result<GetSiteConfigResponse, LemmyError> {
let data: &SaveSiteConfig = &self;
let user = get_user_from_jwt(&data.auth, context.pool()).await?;
let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
// Only let admins read this
let user_id = user.id;
is_admin(context.pool(), user_id).await?;
is_admin(&local_user_view)?;
// Make sure docker doesn't have :ro at the end of the volume, so its not a read-only filesystem
let config_hjson = match Settings::save_config_file(&data.config_hjson) {
Ok(config_hjson) => config_hjson,
Err(_e) => return Err(APIError::err("couldnt_update_site").into()),
Err(_e) => return Err(ApiError::err("couldnt_update_site").into()),
};
Ok(GetSiteConfigResponse { config_hjson })

View file

@ -0,0 +1,97 @@
use crate::{get_local_user_view_from_jwt, Perform};
use actix_web::web::Data;
use lemmy_api_structs::websocket::*;
use lemmy_utils::{ConnectionId, LemmyError};
use lemmy_websocket::{
messages::{JoinCommunityRoom, JoinModRoom, JoinPostRoom, JoinUserRoom},
LemmyContext,
};
#[async_trait::async_trait(?Send)]
impl Perform for UserJoin {
type Response = UserJoinResponse;
async fn perform(
&self,
context: &Data<LemmyContext>,
websocket_id: Option<ConnectionId>,
) -> Result<UserJoinResponse, LemmyError> {
let data: &UserJoin = &self;
let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
if let Some(ws_id) = websocket_id {
context.chat_server().do_send(JoinUserRoom {
local_user_id: local_user_view.local_user.id,
id: ws_id,
});
}
Ok(UserJoinResponse { joined: true })
}
}
#[async_trait::async_trait(?Send)]
impl Perform for CommunityJoin {
type Response = CommunityJoinResponse;
async fn perform(
&self,
context: &Data<LemmyContext>,
websocket_id: Option<ConnectionId>,
) -> Result<CommunityJoinResponse, LemmyError> {
let data: &CommunityJoin = &self;
if let Some(ws_id) = websocket_id {
context.chat_server().do_send(JoinCommunityRoom {
community_id: data.community_id,
id: ws_id,
});
}
Ok(CommunityJoinResponse { joined: true })
}
}
#[async_trait::async_trait(?Send)]
impl Perform for ModJoin {
type Response = ModJoinResponse;
async fn perform(
&self,
context: &Data<LemmyContext>,
websocket_id: Option<ConnectionId>,
) -> Result<ModJoinResponse, LemmyError> {
let data: &ModJoin = &self;
if let Some(ws_id) = websocket_id {
context.chat_server().do_send(JoinModRoom {
community_id: data.community_id,
id: ws_id,
});
}
Ok(ModJoinResponse { joined: true })
}
}
#[async_trait::async_trait(?Send)]
impl Perform for PostJoin {
type Response = PostJoinResponse;
async fn perform(
&self,
context: &Data<LemmyContext>,
websocket_id: Option<ConnectionId>,
) -> Result<PostJoinResponse, LemmyError> {
let data: &PostJoin = &self;
if let Some(ws_id) = websocket_id {
context.chat_server().do_send(JoinPostRoom {
post_id: data.post_id,
id: ws_id,
});
}
Ok(PostJoinResponse { joined: true })
}
}

View file

@ -0,0 +1,24 @@
[package]
name = "lemmy_api_structs"
version = "0.1.0"
edition = "2018"
[lib]
name = "lemmy_api_structs"
path = "src/lib.rs"
doctest = false
[dependencies]
lemmy_db_queries = { path = "../db_queries" }
lemmy_db_views = { path = "../db_views" }
lemmy_db_views_moderator = { path = "../db_views_moderator" }
lemmy_db_views_actor = { path = "../db_views_actor" }
lemmy_db_schema = { path = "../db_schema" }
lemmy_utils = { path = "../utils" }
serde = { version = "1.0.123", features = ["derive"] }
log = "0.4.14"
diesel = "1.4.5"
actix-web = "3.3.2"
chrono = { version = "0.4.19", features = ["serde"] }
serde_json = { version = "1.0.61", features = ["preserve_order"] }
url = "2.2.1"

View file

@ -1,11 +1,12 @@
use lemmy_db_schema::{CommentId, CommunityId, LocalUserId, PostId};
use lemmy_db_views::{comment_report_view::CommentReportView, comment_view::CommentView};
use serde::{Deserialize, Serialize};
#[derive(Deserialize)]
pub struct CreateComment {
pub content: String,
pub parent_id: Option<i32>,
pub post_id: i32,
pub parent_id: Option<CommentId>,
pub post_id: PostId,
pub form_id: Option<String>,
pub auth: String,
}
@ -13,21 +14,21 @@ pub struct CreateComment {
#[derive(Deserialize)]
pub struct EditComment {
pub content: String,
pub edit_id: i32,
pub comment_id: CommentId,
pub form_id: Option<String>,
pub auth: String,
}
#[derive(Deserialize)]
pub struct DeleteComment {
pub edit_id: i32,
pub comment_id: CommentId,
pub deleted: bool,
pub auth: String,
}
#[derive(Deserialize)]
pub struct RemoveComment {
pub edit_id: i32,
pub comment_id: CommentId,
pub removed: bool,
pub reason: Option<String>,
pub auth: String,
@ -35,14 +36,14 @@ pub struct RemoveComment {
#[derive(Deserialize)]
pub struct MarkCommentAsRead {
pub comment_id: i32,
pub comment_id: CommentId,
pub read: bool,
pub auth: String,
}
#[derive(Deserialize)]
pub struct SaveComment {
pub comment_id: i32,
pub comment_id: CommentId,
pub save: bool,
pub auth: String,
}
@ -50,13 +51,13 @@ pub struct SaveComment {
#[derive(Serialize, Clone)]
pub struct CommentResponse {
pub comment_view: CommentView,
pub recipient_ids: Vec<i32>, // TODO another way to do this? Maybe a UserMention belongs to Comment
pub recipient_ids: Vec<LocalUserId>,
pub form_id: Option<String>, // An optional front end ID, to tell which is coming back
}
#[derive(Deserialize)]
pub struct CreateCommentLike {
pub comment_id: i32,
pub comment_id: CommentId,
pub score: i16,
pub auth: String,
}
@ -67,7 +68,7 @@ pub struct GetComments {
pub sort: String,
pub page: Option<i64>,
pub limit: Option<i64>,
pub community_id: Option<i32>,
pub community_id: Option<CommunityId>,
pub community_name: Option<String>,
pub auth: Option<String>,
}
@ -79,7 +80,7 @@ pub struct GetCommentsResponse {
#[derive(Serialize, Deserialize)]
pub struct CreateCommentReport {
pub comment_id: i32,
pub comment_id: CommentId,
pub reason: String,
pub auth: String,
}
@ -108,7 +109,7 @@ pub struct ListCommentReports {
pub page: Option<i64>,
pub limit: Option<i64>,
/// if no community is given, it returns reports for all communities moderated by the auth user
pub community: Option<i32>,
pub community: Option<CommunityId>,
pub auth: String,
}

View file

@ -1,14 +1,15 @@
use lemmy_db_schema::{CommunityId, PersonId};
use lemmy_db_views_actor::{
community_follower_view::CommunityFollowerView,
community_moderator_view::CommunityModeratorView,
community_view::CommunityView,
user_view::UserViewSafe,
person_view::PersonViewSafe,
};
use serde::{Deserialize, Serialize};
#[derive(Deserialize)]
pub struct GetCommunity {
pub id: Option<i32>,
pub id: Option<CommunityId>,
pub name: Option<String>,
pub auth: Option<String>,
}
@ -27,7 +28,6 @@ pub struct CreateCommunity {
pub description: Option<String>,
pub icon: Option<String>,
pub banner: Option<String>,
pub category_id: i32,
pub nsfw: bool,
pub auth: String,
}
@ -39,6 +39,7 @@ pub struct CommunityResponse {
#[derive(Deserialize, Debug)]
pub struct ListCommunities {
pub type_: String,
pub sort: String,
pub page: Option<i64>,
pub limit: Option<i64>,
@ -52,8 +53,8 @@ pub struct ListCommunitiesResponse {
#[derive(Deserialize, Clone)]
pub struct BanFromCommunity {
pub community_id: i32,
pub user_id: i32,
pub community_id: CommunityId,
pub person_id: PersonId,
pub ban: bool,
pub remove_data: bool,
pub reason: Option<String>,
@ -63,14 +64,14 @@ pub struct BanFromCommunity {
#[derive(Serialize, Clone)]
pub struct BanFromCommunityResponse {
pub user_view: UserViewSafe,
pub person_view: PersonViewSafe,
pub banned: bool,
}
#[derive(Deserialize)]
pub struct AddModToCommunity {
pub community_id: i32,
pub user_id: i32,
pub community_id: CommunityId,
pub person_id: PersonId,
pub added: bool,
pub auth: String,
}
@ -82,26 +83,25 @@ pub struct AddModToCommunityResponse {
#[derive(Deserialize)]
pub struct EditCommunity {
pub edit_id: i32,
pub community_id: CommunityId,
pub title: String,
pub description: Option<String>,
pub icon: Option<String>,
pub banner: Option<String>,
pub category_id: i32,
pub nsfw: bool,
pub auth: String,
}
#[derive(Deserialize)]
pub struct DeleteCommunity {
pub edit_id: i32,
pub community_id: CommunityId,
pub deleted: bool,
pub auth: String,
}
#[derive(Deserialize)]
pub struct RemoveCommunity {
pub edit_id: i32,
pub community_id: CommunityId,
pub removed: bool,
pub reason: Option<String>,
pub expires: Option<i64>,
@ -110,7 +110,7 @@ pub struct RemoveCommunity {
#[derive(Deserialize)]
pub struct FollowCommunity {
pub community_id: i32,
pub community_id: CommunityId,
pub follow: bool,
pub auth: String,
}
@ -127,27 +127,7 @@ pub struct GetFollowedCommunitiesResponse {
#[derive(Deserialize)]
pub struct TransferCommunity {
pub community_id: i32,
pub user_id: i32,
pub community_id: CommunityId,
pub person_id: PersonId,
pub auth: String,
}
#[derive(Deserialize, Debug)]
pub struct CommunityJoin {
pub community_id: i32,
}
#[derive(Serialize, Clone)]
pub struct CommunityJoinResponse {
pub joined: bool,
}
#[derive(Deserialize, Debug)]
pub struct ModJoin {
pub community_id: i32,
}
#[derive(Serialize, Clone)]
pub struct ModJoinResponse {
pub joined: bool,
}

View file

@ -0,0 +1,191 @@
pub mod comment;
pub mod community;
pub mod person;
pub mod post;
pub mod site;
pub mod websocket;
use diesel::PgConnection;
use lemmy_db_queries::{Crud, DbPool};
use lemmy_db_schema::{
source::{
comment::Comment,
person::Person,
person_mention::{PersonMention, PersonMentionForm},
post::Post,
},
LocalUserId,
};
use lemmy_db_views::local_user_view::LocalUserView;
use lemmy_utils::{email::send_email, settings::structs::Settings, utils::MentionData, LemmyError};
use log::error;
use serde::{Deserialize, Serialize};
use url::Url;
#[derive(Serialize, Deserialize, Debug)]
pub struct WebFingerLink {
pub rel: Option<String>,
#[serde(rename(serialize = "type", deserialize = "type"))]
pub type_: Option<String>,
pub href: Option<Url>,
#[serde(skip_serializing_if = "Option::is_none")]
pub template: Option<String>,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct WebFingerResponse {
pub subject: String,
pub aliases: Vec<Url>,
pub links: Vec<WebFingerLink>,
}
pub async fn blocking<F, T>(pool: &DbPool, f: F) -> Result<T, LemmyError>
where
F: FnOnce(&diesel::PgConnection) -> T + Send + 'static,
T: Send + 'static,
{
let pool = pool.clone();
let res = actix_web::web::block(move || {
let conn = pool.get()?;
let res = (f)(&conn);
Ok(res) as Result<_, LemmyError>
})
.await?;
Ok(res)
}
pub async fn send_local_notifs(
mentions: Vec<MentionData>,
comment: Comment,
person: Person,
post: Post,
pool: &DbPool,
do_send_email: bool,
) -> Result<Vec<LocalUserId>, LemmyError> {
let ids = blocking(pool, move |conn| {
do_send_local_notifs(conn, &mentions, &comment, &person, &post, do_send_email)
})
.await?;
Ok(ids)
}
fn do_send_local_notifs(
conn: &PgConnection,
mentions: &[MentionData],
comment: &Comment,
person: &Person,
post: &Post,
do_send_email: bool,
) -> Vec<LocalUserId> {
let mut recipient_ids = Vec::new();
// Send the local mentions
for mention in mentions
.iter()
.filter(|m| m.is_local() && m.name.ne(&person.name))
.collect::<Vec<&MentionData>>()
{
if let Ok(mention_user_view) = LocalUserView::read_from_name(&conn, &mention.name) {
// TODO
// At some point, make it so you can't tag the parent creator either
// This can cause two notifications, one for reply and the other for mention
recipient_ids.push(mention_user_view.local_user.id);
let user_mention_form = PersonMentionForm {
recipient_id: mention_user_view.person.id,
comment_id: comment.id,
read: None,
};
// Allow this to fail softly, since comment edits might re-update or replace it
// Let the uniqueness handle this fail
PersonMention::create(&conn, &user_mention_form).ok();
// Send an email to those local users that have notifications on
if do_send_email {
send_email_to_user(
&mention_user_view,
"Mentioned by",
"Person Mention",
&comment.content,
)
}
}
}
// Send notifs to the parent commenter / poster
match comment.parent_id {
Some(parent_id) => {
if let Ok(parent_comment) = Comment::read(&conn, parent_id) {
// Don't send a notif to yourself
if parent_comment.creator_id != person.id {
// Get the parent commenter local_user
if let Ok(parent_user_view) = LocalUserView::read_person(&conn, parent_comment.creator_id)
{
recipient_ids.push(parent_user_view.local_user.id);
if do_send_email {
send_email_to_user(
&parent_user_view,
"Reply from",
"Comment Reply",
&comment.content,
)
}
}
}
}
}
// Its a post
None => {
if post.creator_id != person.id {
if let Ok(parent_user_view) = LocalUserView::read_person(&conn, post.creator_id) {
recipient_ids.push(parent_user_view.local_user.id);
if do_send_email {
send_email_to_user(
&parent_user_view,
"Reply from",
"Post Reply",
&comment.content,
)
}
}
}
}
};
recipient_ids
}
pub fn send_email_to_user(
local_user_view: &LocalUserView,
subject_text: &str,
body_text: &str,
comment_content: &str,
) {
if local_user_view.person.banned || !local_user_view.local_user.send_notifications_to_email {
return;
}
if let Some(user_email) = &local_user_view.local_user.email {
let subject = &format!(
"{} - {} {}",
subject_text,
Settings::get().hostname(),
local_user_view.person.name,
);
let html = &format!(
"<h1>{}</h1><br><div>{} - {}</div><br><a href={}/inbox>inbox</a>",
body_text,
local_user_view.person.name,
comment_content,
Settings::get().get_protocol_and_hostname()
);
match send_email(subject, &user_email, &local_user_view.person.name, html) {
Ok(_o) => _o,
Err(e) => error!("{}", e),
};
}
}

View file

@ -6,8 +6,8 @@ use lemmy_db_views::{
use lemmy_db_views_actor::{
community_follower_view::CommunityFollowerView,
community_moderator_view::CommunityModeratorView,
user_mention_view::UserMentionView,
user_view::{UserViewDangerous, UserViewSafe},
person_mention_view::PersonMentionView,
person_view::PersonViewSafe,
};
use serde::{Deserialize, Serialize};
@ -16,6 +16,7 @@ pub struct Login {
pub username_or_email: String,
pub password: String,
}
use lemmy_db_schema::{CommunityId, PersonId, PersonMentionId, PrivateMessageId};
#[derive(Deserialize)]
pub struct Register {
@ -23,7 +24,6 @@ pub struct Register {
pub email: Option<String>,
pub password: String,
pub password_verify: String,
pub admin: bool,
pub show_nsfw: bool,
pub captcha_uuid: Option<String>,
pub captcha_answer: Option<String>,
@ -34,7 +34,7 @@ pub struct GetCaptcha {}
#[derive(Serialize)]
pub struct GetCaptchaResponse {
pub ok: Option<CaptchaResponse>,
pub ok: Option<CaptchaResponse>, // Will be None if captchas are disabled
}
#[derive(Serialize)]
@ -46,11 +46,11 @@ pub struct CaptchaResponse {
#[derive(Deserialize)]
pub struct SaveUserSettings {
pub show_nsfw: bool,
pub theme: String,
pub default_sort_type: i16,
pub default_listing_type: i16,
pub lang: String,
pub show_nsfw: Option<bool>,
pub theme: Option<String>,
pub default_sort_type: Option<i16>,
pub default_listing_type: Option<i16>,
pub lang: Option<String>,
pub avatar: Option<String>,
pub banner: Option<String>,
pub preferred_username: Option<String>,
@ -60,8 +60,8 @@ pub struct SaveUserSettings {
pub new_password: Option<String>,
pub new_password_verify: Option<String>,
pub old_password: Option<String>,
pub show_avatars: bool,
pub send_notifications_to_email: bool,
pub show_avatars: Option<bool>,
pub send_notifications_to_email: Option<bool>,
pub auth: String,
}
@ -71,21 +71,20 @@ pub struct LoginResponse {
}
#[derive(Deserialize)]
pub struct GetUserDetails {
pub user_id: Option<i32>,
pub struct GetPersonDetails {
pub person_id: Option<PersonId>,
pub username: Option<String>,
pub sort: String,
pub page: Option<i64>,
pub limit: Option<i64>,
pub community_id: Option<i32>,
pub community_id: Option<CommunityId>,
pub saved_only: bool,
pub auth: Option<String>,
}
#[derive(Serialize)]
pub struct GetUserDetailsResponse {
pub user_view: Option<UserViewSafe>,
pub user_view_dangerous: Option<UserViewDangerous>,
pub struct GetPersonDetailsResponse {
pub person_view: PersonViewSafe,
pub follows: Vec<CommunityFollowerView>,
pub moderates: Vec<CommunityModeratorView>,
pub comments: Vec<CommentView>,
@ -98,8 +97,8 @@ pub struct GetRepliesResponse {
}
#[derive(Serialize)]
pub struct GetUserMentionsResponse {
pub mentions: Vec<UserMentionView>,
pub struct GetPersonMentionsResponse {
pub mentions: Vec<PersonMentionView>,
}
#[derive(Deserialize)]
@ -109,19 +108,19 @@ pub struct MarkAllAsRead {
#[derive(Deserialize)]
pub struct AddAdmin {
pub user_id: i32,
pub person_id: PersonId,
pub added: bool,
pub auth: String,
}
#[derive(Serialize, Clone)]
pub struct AddAdminResponse {
pub admins: Vec<UserViewSafe>,
pub admins: Vec<PersonViewSafe>,
}
#[derive(Deserialize)]
pub struct BanUser {
pub user_id: i32,
pub struct BanPerson {
pub person_id: PersonId,
pub ban: bool,
pub remove_data: bool,
pub reason: Option<String>,
@ -130,8 +129,8 @@ pub struct BanUser {
}
#[derive(Serialize, Clone)]
pub struct BanUserResponse {
pub user_view: UserViewSafe,
pub struct BanPersonResponse {
pub person_view: PersonViewSafe,
pub banned: bool,
}
@ -145,7 +144,7 @@ pub struct GetReplies {
}
#[derive(Deserialize)]
pub struct GetUserMentions {
pub struct GetPersonMentions {
pub sort: String,
pub page: Option<i64>,
pub limit: Option<i64>,
@ -154,15 +153,15 @@ pub struct GetUserMentions {
}
#[derive(Deserialize)]
pub struct MarkUserMentionAsRead {
pub user_mention_id: i32,
pub struct MarkPersonMentionAsRead {
pub person_mention_id: PersonMentionId,
pub read: bool,
pub auth: String,
}
#[derive(Serialize, Clone)]
pub struct UserMentionResponse {
pub user_mention_view: UserMentionView,
pub struct PersonMentionResponse {
pub person_mention_view: PersonMentionView,
}
#[derive(Deserialize)]
@ -189,27 +188,27 @@ pub struct PasswordChange {
#[derive(Deserialize)]
pub struct CreatePrivateMessage {
pub content: String,
pub recipient_id: i32,
pub recipient_id: PersonId,
pub auth: String,
}
#[derive(Deserialize)]
pub struct EditPrivateMessage {
pub edit_id: i32,
pub private_message_id: PrivateMessageId,
pub content: String,
pub auth: String,
}
#[derive(Deserialize)]
pub struct DeletePrivateMessage {
pub edit_id: i32,
pub private_message_id: PrivateMessageId,
pub deleted: bool,
pub auth: String,
}
#[derive(Deserialize)]
pub struct MarkPrivateMessageAsRead {
pub edit_id: i32,
pub private_message_id: PrivateMessageId,
pub read: bool,
pub auth: String,
}
@ -232,25 +231,15 @@ pub struct PrivateMessageResponse {
pub private_message_view: PrivateMessageView,
}
#[derive(Deserialize, Debug)]
pub struct UserJoin {
pub auth: String,
}
#[derive(Serialize, Clone)]
pub struct UserJoinResponse {
pub joined: bool,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct GetReportCount {
pub community: Option<i32>,
pub community: Option<CommunityId>,
pub auth: String,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct GetReportCountResponse {
pub community: Option<i32>,
pub community: Option<CommunityId>,
pub comment_reports: i64,
pub post_reports: i64,
}

View file

@ -1,3 +1,4 @@
use lemmy_db_schema::{CommunityId, PostId};
use lemmy_db_views::{
comment_view::CommentView,
post_report_view::PostReportView,
@ -8,14 +9,15 @@ use lemmy_db_views_actor::{
community_view::CommunityView,
};
use serde::{Deserialize, Serialize};
use url::Url;
#[derive(Deserialize, Debug)]
pub struct CreatePost {
pub name: String,
pub url: Option<String>,
pub url: Option<Url>,
pub body: Option<String>,
pub nsfw: bool,
pub community_id: i32,
pub community_id: CommunityId,
pub auth: String,
}
@ -26,7 +28,7 @@ pub struct PostResponse {
#[derive(Deserialize)]
pub struct GetPost {
pub id: i32,
pub id: PostId,
pub auth: Option<String>,
}
@ -45,7 +47,7 @@ pub struct GetPosts {
pub sort: String,
pub page: Option<i64>,
pub limit: Option<i64>,
pub community_id: Option<i32>,
pub community_id: Option<CommunityId>,
pub community_name: Option<String>,
pub auth: Option<String>,
}
@ -57,16 +59,16 @@ pub struct GetPostsResponse {
#[derive(Deserialize)]
pub struct CreatePostLike {
pub post_id: i32,
pub post_id: PostId,
pub score: i16,
pub auth: String,
}
#[derive(Deserialize)]
pub struct EditPost {
pub edit_id: i32,
pub post_id: PostId,
pub name: String,
pub url: Option<String>,
pub url: Option<Url>,
pub body: Option<String>,
pub nsfw: bool,
pub auth: String,
@ -74,14 +76,14 @@ pub struct EditPost {
#[derive(Deserialize)]
pub struct DeletePost {
pub edit_id: i32,
pub post_id: PostId,
pub deleted: bool,
pub auth: String,
}
#[derive(Deserialize)]
pub struct RemovePost {
pub edit_id: i32,
pub post_id: PostId,
pub removed: bool,
pub reason: Option<String>,
pub auth: String,
@ -89,38 +91,28 @@ pub struct RemovePost {
#[derive(Deserialize)]
pub struct LockPost {
pub edit_id: i32,
pub post_id: PostId,
pub locked: bool,
pub auth: String,
}
#[derive(Deserialize)]
pub struct StickyPost {
pub edit_id: i32,
pub post_id: PostId,
pub stickied: bool,
pub auth: String,
}
#[derive(Deserialize)]
pub struct SavePost {
pub post_id: i32,
pub post_id: PostId,
pub save: bool,
pub auth: String,
}
#[derive(Deserialize, Debug)]
pub struct PostJoin {
pub post_id: i32,
}
#[derive(Serialize, Clone)]
pub struct PostJoinResponse {
pub joined: bool,
}
#[derive(Serialize, Deserialize)]
pub struct CreatePostReport {
pub post_id: i32,
pub post_id: PostId,
pub reason: String,
pub auth: String,
}
@ -147,7 +139,7 @@ pub struct ResolvePostReportResponse {
pub struct ListPostReports {
pub page: Option<i64>,
pub limit: Option<i64>,
pub community: Option<i32>,
pub community: Option<CommunityId>,
pub auth: String,
}

View file

@ -1,6 +1,11 @@
use lemmy_db_schema::source::{category::*, user::User_};
use lemmy_db_views::{comment_view::CommentView, post_view::PostView, site_view::SiteView};
use lemmy_db_views_actor::{community_view::CommunityView, user_view::UserViewSafe};
use lemmy_db_schema::{CommunityId, PersonId};
use lemmy_db_views::{
comment_view::CommentView,
local_user_view::LocalUserSettingsView,
post_view::PostView,
site_view::SiteView,
};
use lemmy_db_views_actor::{community_view::CommunityView, person_view::PersonViewSafe};
use lemmy_db_views_moderator::{
mod_add_community_view::ModAddCommunityView,
mod_add_view::ModAddView,
@ -13,20 +18,13 @@ use lemmy_db_views_moderator::{
mod_sticky_post_view::ModStickyPostView,
};
use serde::{Deserialize, Serialize};
#[derive(Deserialize)]
pub struct ListCategories {}
#[derive(Serialize)]
pub struct ListCategoriesResponse {
pub categories: Vec<Category>,
}
use url::Url;
#[derive(Deserialize, Debug)]
pub struct Search {
pub q: String,
pub type_: String,
pub community_id: Option<i32>,
pub community_id: Option<CommunityId>,
pub community_name: Option<String>,
pub sort: String,
pub page: Option<i64>,
@ -40,13 +38,13 @@ pub struct SearchResponse {
pub comments: Vec<CommentView>,
pub posts: Vec<PostView>,
pub communities: Vec<CommunityView>,
pub users: Vec<UserViewSafe>,
pub users: Vec<PersonViewSafe>,
}
#[derive(Deserialize)]
pub struct GetModlog {
pub mod_user_id: Option<i32>,
pub community_id: Option<i32>,
pub mod_person_id: Option<PersonId>,
pub community_id: Option<CommunityId>,
pub page: Option<i64>,
pub limit: Option<i64>,
}
@ -68,8 +66,8 @@ pub struct GetModlogResponse {
pub struct CreateSite {
pub name: String,
pub description: Option<String>,
pub icon: Option<String>,
pub banner: Option<String>,
pub icon: Option<Url>,
pub banner: Option<Url>,
pub enable_downvotes: bool,
pub open_registration: bool,
pub enable_nsfw: bool,
@ -101,17 +99,17 @@ pub struct SiteResponse {
#[derive(Serialize)]
pub struct GetSiteResponse {
pub site_view: Option<SiteView>, // Because the site might not be set up yet
pub admins: Vec<UserViewSafe>,
pub banned: Vec<UserViewSafe>,
pub admins: Vec<PersonViewSafe>,
pub banned: Vec<PersonViewSafe>,
pub online: usize,
pub version: String,
pub my_user: Option<User_>,
pub federated_instances: Vec<String>,
pub my_user: Option<LocalUserSettingsView>,
pub federated_instances: Option<FederatedInstances>, // Federation may be disabled
}
#[derive(Deserialize)]
pub struct TransferSite {
pub user_id: i32,
pub person_id: PersonId,
pub auth: String,
}
@ -130,3 +128,10 @@ pub struct SaveSiteConfig {
pub config_hjson: String,
pub auth: String,
}
#[derive(Serialize)]
pub struct FederatedInstances {
pub linked: Vec<String>,
pub allowed: Option<Vec<String>>,
pub blocked: Option<Vec<String>>,
}

View file

@ -0,0 +1,42 @@
use lemmy_db_schema::{CommunityId, PostId};
use serde::{Deserialize, Serialize};
#[derive(Deserialize, Debug)]
pub struct UserJoin {
pub auth: String,
}
#[derive(Serialize, Clone)]
pub struct UserJoinResponse {
pub joined: bool,
}
#[derive(Deserialize, Debug)]
pub struct CommunityJoin {
pub community_id: CommunityId,
}
#[derive(Serialize, Clone)]
pub struct CommunityJoinResponse {
pub joined: bool,
}
#[derive(Deserialize, Debug)]
pub struct ModJoin {
pub community_id: CommunityId,
}
#[derive(Serialize, Clone)]
pub struct ModJoinResponse {
pub joined: bool,
}
#[derive(Deserialize, Debug)]
pub struct PostJoin {
pub post_id: PostId,
}
#[derive(Serialize, Clone)]
pub struct PostJoinResponse {
pub joined: bool,
}

View file

@ -1,52 +1,52 @@
[package]
name = "lemmy_apub"
version = "0.1.0"
authors = ["Felix Ableitner <me@nutomic.com>"]
edition = "2018"
[lib]
name = "lemmy_apub"
path = "src/lib.rs"
doctest = false
[dependencies]
lemmy_utils = { path = "../lemmy_utils" }
lemmy_db_queries = { path = "../lemmy_db_queries" }
lemmy_db_schema = { path = "../lemmy_db_schema" }
lemmy_db_views = { path = "../lemmy_db_views" }
lemmy_db_views_actor = { path = "../lemmy_db_views_actor" }
lemmy_structs = { path = "../lemmy_structs" }
lemmy_websocket = { path = "../lemmy_websocket" }
lemmy_utils = { path = "../utils" }
lemmy_db_queries = { path = "../db_queries" }
lemmy_db_schema = { path = "../db_schema" }
lemmy_db_views = { path = "../db_views" }
lemmy_db_views_actor = { path = "../db_views_actor" }
lemmy_api_structs = { path = "../api_structs" }
lemmy_websocket = { path = "../websocket" }
diesel = "1.4.5"
activitystreams = "0.7.0-alpha.8"
activitystreams = "0.7.0-alpha.10"
activitystreams-ext = "0.1.0-alpha.2"
bcrypt = "0.9.0"
chrono = { version = "0.4.19", features = ["serde"] }
serde_json = { version = "1.0.60", features = ["preserve_order"] }
serde = { version = "1.0.118", features = ["derive"] }
serde_json = { version = "1.0.61", features = ["preserve_order"] }
serde = { version = "1.0.123", features = ["derive"] }
actix = "0.10.0"
actix-web = { version = "3.3.2", default-features = false }
actix-rt = { version = "1.1.1", default-features = false }
awc = { version = "2.0.3", default-features = false }
log = "0.4.11"
rand = "0.8.0"
log = "0.4.14"
rand = "0.8.3"
strum = "0.20.0"
strum_macros = "0.20.1"
lazy_static = "1.4.0"
url = { version = "2.2.0", features = ["serde"] }
url = { version = "2.2.1", features = ["serde"] }
percent-encoding = "2.1.0"
openssl = "0.10.31"
http = "0.2.2"
openssl = "0.10.32"
http = "0.2.3"
http-signature-normalization-actix = { version = "0.4.1", default-features = false, features = ["sha-2"] }
http-signature-normalization-reqwest = { version = "0.1.3", default-features = false, features = ["sha-2"] }
base64 = "0.13.0"
tokio = "0.3.6"
futures = "0.3.8"
itertools = "0.9.0"
uuid = { version = "0.8.1", features = ["serde", "v4"] }
sha2 = "0.9.2"
futures = "0.3.12"
itertools = "0.10.0"
uuid = { version = "0.8.2", features = ["serde", "v4"] }
sha2 = "0.9.3"
async-trait = "0.1.42"
anyhow = "1.0.36"
thiserror = "1.0.22"
anyhow = "1.0.38"
thiserror = "1.0.23"
background-jobs = "0.8.0"
reqwest = { version = "0.10.10", features = ["json"] }
backtrace = "0.3.55"
backtrace = "0.3.56"

View file

@ -1,16 +1,16 @@
use crate::{activities::receive::get_actor_as_user, objects::FromApub, ActorType, NoteExt};
use crate::{activities::receive::get_actor_as_person, objects::FromApub, ActorType, NoteExt};
use activitystreams::{
activity::{ActorAndObjectRefExt, Create, Dislike, Like, Remove, Update},
base::ExtendsExt,
};
use anyhow::Context;
use lemmy_api_structs::{blocking, comment::CommentResponse, send_local_notifs};
use lemmy_db_queries::{source::comment::Comment_, Crud, Likeable};
use lemmy_db_schema::source::{
comment::{Comment, CommentLike, CommentLikeForm},
post::Post,
};
use lemmy_db_views::comment_view::CommentView;
use lemmy_structs::{blocking, comment::CommentResponse, send_local_notifs};
use lemmy_utils::{location_info, utils::scrape_text_for_mentions, LemmyError};
use lemmy_websocket::{messages::SendComment, LemmyContext, UserOperation};
@ -19,11 +19,11 @@ pub(crate) async fn receive_create_comment(
context: &LemmyContext,
request_counter: &mut i32,
) -> Result<(), LemmyError> {
let user = get_actor_as_user(&create, context, request_counter).await?;
let person = get_actor_as_person(&create, context, request_counter).await?;
let note = NoteExt::from_any_base(create.object().to_owned().one().context(location_info!())?)?
.context(location_info!())?;
let comment = Comment::from_apub(&note, context, user.actor_id()?, request_counter).await?;
let comment = Comment::from_apub(&note, context, person.actor_id(), request_counter).await?;
let post_id = comment.post_id;
let post = blocking(context.pool(), move |conn| Post::read(conn, post_id)).await??;
@ -33,8 +33,15 @@ pub(crate) async fn receive_create_comment(
// Its much easier to scrape them from the comment body, since the API has to do that
// anyway.
let mentions = scrape_text_for_mentions(&comment.content);
let recipient_ids =
send_local_notifs(mentions, comment.clone(), &user, post, context.pool(), true).await?;
let recipient_ids = send_local_notifs(
mentions,
comment.clone(),
person,
post,
context.pool(),
true,
)
.await?;
// Refetch the view
let comment_view = blocking(context.pool(), move |conn| {
@ -64,9 +71,9 @@ pub(crate) async fn receive_update_comment(
) -> Result<(), LemmyError> {
let note = NoteExt::from_any_base(update.object().to_owned().one().context(location_info!())?)?
.context(location_info!())?;
let user = get_actor_as_user(&update, context, request_counter).await?;
let person = get_actor_as_person(&update, context, request_counter).await?;
let comment = Comment::from_apub(&note, context, user.actor_id()?, request_counter).await?;
let comment = Comment::from_apub(&note, context, person.actor_id(), request_counter).await?;
let comment_id = comment.id;
let post_id = comment.post_id;
@ -74,7 +81,7 @@ pub(crate) async fn receive_update_comment(
let mentions = scrape_text_for_mentions(&comment.content);
let recipient_ids =
send_local_notifs(mentions, comment, &user, post, context.pool(), false).await?;
send_local_notifs(mentions, comment, person, post, context.pool(), false).await?;
// Refetch the view
let comment_view = blocking(context.pool(), move |conn| {
@ -103,18 +110,18 @@ pub(crate) async fn receive_like_comment(
context: &LemmyContext,
request_counter: &mut i32,
) -> Result<(), LemmyError> {
let user = get_actor_as_user(&like, context, request_counter).await?;
let person = get_actor_as_person(&like, context, request_counter).await?;
let comment_id = comment.id;
let like_form = CommentLikeForm {
comment_id,
post_id: comment.post_id,
user_id: user.id,
person_id: person.id,
score: 1,
};
let user_id = user.id;
let person_id = person.id;
blocking(context.pool(), move |conn| {
CommentLike::remove(conn, user_id, comment_id)?;
CommentLike::remove(conn, person_id, comment_id)?;
CommentLike::like(conn, &like_form)
})
.await??;
@ -148,18 +155,18 @@ pub(crate) async fn receive_dislike_comment(
context: &LemmyContext,
request_counter: &mut i32,
) -> Result<(), LemmyError> {
let user = get_actor_as_user(&dislike, context, request_counter).await?;
let person = get_actor_as_person(&dislike, context, request_counter).await?;
let comment_id = comment.id;
let like_form = CommentLikeForm {
comment_id,
post_id: comment.post_id,
user_id: user.id,
person_id: person.id,
score: -1,
};
let user_id = user.id;
let person_id = person.id;
blocking(context.pool(), move |conn| {
CommentLike::remove(conn, user_id, comment_id)?;
CommentLike::remove(conn, person_id, comment_id)?;
CommentLike::like(conn, &like_form)
})
.await??;

View file

@ -1,9 +1,9 @@
use crate::activities::receive::get_actor_as_user;
use crate::activities::receive::get_actor_as_person;
use activitystreams::activity::{Dislike, Like};
use lemmy_api_structs::{blocking, comment::CommentResponse};
use lemmy_db_queries::{source::comment::Comment_, Likeable};
use lemmy_db_schema::source::comment::{Comment, CommentLike};
use lemmy_db_views::comment_view::CommentView;
use lemmy_structs::{blocking, comment::CommentResponse};
use lemmy_utils::LemmyError;
use lemmy_websocket::{messages::SendComment, LemmyContext, UserOperation};
@ -13,12 +13,12 @@ pub(crate) async fn receive_undo_like_comment(
context: &LemmyContext,
request_counter: &mut i32,
) -> Result<(), LemmyError> {
let user = get_actor_as_user(like, context, request_counter).await?;
let person = get_actor_as_person(like, context, request_counter).await?;
let comment_id = comment.id;
let user_id = user.id;
let person_id = person.id;
blocking(context.pool(), move |conn| {
CommentLike::remove(conn, user_id, comment_id)
CommentLike::remove(conn, person_id, comment_id)
})
.await??;
@ -51,12 +51,12 @@ pub(crate) async fn receive_undo_dislike_comment(
context: &LemmyContext,
request_counter: &mut i32,
) -> Result<(), LemmyError> {
let user = get_actor_as_user(dislike, context, request_counter).await?;
let person = get_actor_as_person(dislike, context, request_counter).await?;
let comment_id = comment.id;
let user_id = user.id;
let person_id = person.id;
blocking(context.pool(), move |conn| {
CommentLike::remove(conn, user_id, comment_id)
CommentLike::remove(conn, person_id, comment_id)
})
.await??;

View file

@ -4,10 +4,10 @@ use activitystreams::{
base::{AnyBase, ExtendsExt},
};
use anyhow::Context;
use lemmy_api_structs::{blocking, community::CommunityResponse};
use lemmy_db_queries::{source::community::Community_, ApubObject};
use lemmy_db_schema::source::community::Community;
use lemmy_db_views_actor::community_view::CommunityView;
use lemmy_structs::{blocking, community::CommunityResponse};
use lemmy_utils::{location_info, LemmyError};
use lemmy_websocket::{messages::SendCommunityRoomMessage, LemmyContext, UserOperation};
use url::Url;
@ -55,7 +55,7 @@ pub(crate) async fn receive_remove_community(
.single_xsd_any_uri()
.context(location_info!())?;
let community = blocking(context.pool(), move |conn| {
Community::read_from_apub_id(conn, community_uri.as_str())
Community::read_from_apub_id(conn, &community_uri.into())
})
.await??;
@ -137,7 +137,7 @@ pub(crate) async fn receive_undo_remove_community(
.single_xsd_any_uri()
.context(location_info!())?;
let community = blocking(context.pool(), move |conn| {
Community::read_from_apub_id(conn, community_uri.as_str())
Community::read_from_apub_id(conn, &community_uri.into())
})
.await??;

View file

@ -1,11 +1,11 @@
use crate::fetcher::user::get_or_fetch_and_upsert_user;
use crate::fetcher::person::get_or_fetch_and_upsert_person;
use activitystreams::{
activity::{ActorAndObjectRef, ActorAndObjectRefExt},
base::{AsBase, BaseExt},
error::DomainError,
};
use anyhow::{anyhow, Context};
use lemmy_db_schema::source::user::User_;
use lemmy_db_schema::source::person::Person;
use lemmy_utils::{location_info, LemmyError};
use lemmy_websocket::LemmyContext;
use log::debug;
@ -28,18 +28,18 @@ where
Err(anyhow!("Activity not supported").into())
}
/// Reads the actor field of an activity and returns the corresponding `User_`.
pub(crate) async fn get_actor_as_user<T, A>(
/// Reads the actor field of an activity and returns the corresponding `Person`.
pub(crate) async fn get_actor_as_person<T, A>(
activity: &T,
context: &LemmyContext,
request_counter: &mut i32,
) -> Result<User_, LemmyError>
) -> Result<Person, LemmyError>
where
T: AsBase<A> + ActorAndObjectRef,
{
let actor = activity.actor()?;
let user_uri = actor.as_single_xsd_any_uri().context(location_info!())?;
get_or_fetch_and_upsert_user(&user_uri, context, request_counter).await
let person_uri = actor.as_single_xsd_any_uri().context(location_info!())?;
get_or_fetch_and_upsert_person(&person_uri, context, request_counter).await
}
/// Ensure that the ID of an incoming activity comes from the same domain as the actor. Optionally

View file

@ -1,13 +1,13 @@
use crate::{activities::receive::get_actor_as_user, objects::FromApub, ActorType, PageExt};
use crate::{activities::receive::get_actor_as_person, objects::FromApub, ActorType, PageExt};
use activitystreams::{
activity::{Create, Dislike, Like, Remove, Update},
prelude::*,
};
use anyhow::Context;
use lemmy_api_structs::{blocking, post::PostResponse};
use lemmy_db_queries::{source::post::Post_, Likeable};
use lemmy_db_schema::source::post::{Post, PostLike, PostLikeForm};
use lemmy_db_views::post_view::PostView;
use lemmy_structs::{blocking, post::PostResponse};
use lemmy_utils::{location_info, LemmyError};
use lemmy_websocket::{messages::SendPost, LemmyContext, UserOperation};
@ -16,11 +16,11 @@ pub(crate) async fn receive_create_post(
context: &LemmyContext,
request_counter: &mut i32,
) -> Result<(), LemmyError> {
let user = get_actor_as_user(&create, context, request_counter).await?;
let person = get_actor_as_person(&create, context, request_counter).await?;
let page = PageExt::from_any_base(create.object().to_owned().one().context(location_info!())?)?
.context(location_info!())?;
let post = Post::from_apub(&page, context, user.actor_id()?, request_counter).await?;
let post = Post::from_apub(&page, context, person.actor_id(), request_counter).await?;
// Refetch the view
let post_id = post.id;
@ -45,11 +45,11 @@ pub(crate) async fn receive_update_post(
context: &LemmyContext,
request_counter: &mut i32,
) -> Result<(), LemmyError> {
let user = get_actor_as_user(&update, context, request_counter).await?;
let person = get_actor_as_person(&update, context, request_counter).await?;
let page = PageExt::from_any_base(update.object().to_owned().one().context(location_info!())?)?
.context(location_info!())?;
let post = Post::from_apub(&page, context, user.actor_id()?, request_counter).await?;
let post = Post::from_apub(&page, context, person.actor_id(), request_counter).await?;
let post_id = post.id;
// Refetch the view
@ -75,17 +75,17 @@ pub(crate) async fn receive_like_post(
context: &LemmyContext,
request_counter: &mut i32,
) -> Result<(), LemmyError> {
let user = get_actor_as_user(&like, context, request_counter).await?;
let person = get_actor_as_person(&like, context, request_counter).await?;
let post_id = post.id;
let like_form = PostLikeForm {
post_id,
user_id: user.id,
person_id: person.id,
score: 1,
};
let user_id = user.id;
let person_id = person.id;
blocking(context.pool(), move |conn| {
PostLike::remove(conn, user_id, post_id)?;
PostLike::remove(conn, person_id, post_id)?;
PostLike::like(conn, &like_form)
})
.await??;
@ -113,17 +113,17 @@ pub(crate) async fn receive_dislike_post(
context: &LemmyContext,
request_counter: &mut i32,
) -> Result<(), LemmyError> {
let user = get_actor_as_user(&dislike, context, request_counter).await?;
let person = get_actor_as_person(&dislike, context, request_counter).await?;
let post_id = post.id;
let like_form = PostLikeForm {
post_id,
user_id: user.id,
person_id: person.id,
score: -1,
};
let user_id = user.id;
let person_id = person.id;
blocking(context.pool(), move |conn| {
PostLike::remove(conn, user_id, post_id)?;
PostLike::remove(conn, person_id, post_id)?;
PostLike::like(conn, &like_form)
})
.await??;

View file

@ -1,9 +1,9 @@
use crate::activities::receive::get_actor_as_user;
use crate::activities::receive::get_actor_as_person;
use activitystreams::activity::{Dislike, Like};
use lemmy_api_structs::{blocking, post::PostResponse};
use lemmy_db_queries::{source::post::Post_, Likeable};
use lemmy_db_schema::source::post::{Post, PostLike};
use lemmy_db_views::post_view::PostView;
use lemmy_structs::{blocking, post::PostResponse};
use lemmy_utils::LemmyError;
use lemmy_websocket::{messages::SendPost, LemmyContext, UserOperation};
@ -13,12 +13,12 @@ pub(crate) async fn receive_undo_like_post(
context: &LemmyContext,
request_counter: &mut i32,
) -> Result<(), LemmyError> {
let user = get_actor_as_user(like, context, request_counter).await?;
let person = get_actor_as_person(like, context, request_counter).await?;
let post_id = post.id;
let user_id = user.id;
let person_id = person.id;
blocking(context.pool(), move |conn| {
PostLike::remove(conn, user_id, post_id)
PostLike::remove(conn, person_id, post_id)
})
.await??;
@ -45,12 +45,12 @@ pub(crate) async fn receive_undo_dislike_post(
context: &LemmyContext,
request_counter: &mut i32,
) -> Result<(), LemmyError> {
let user = get_actor_as_user(dislike, context, request_counter).await?;
let person = get_actor_as_person(dislike, context, request_counter).await?;
let post_id = post.id;
let user_id = user.id;
let person_id = person.id;
blocking(context.pool(), move |conn| {
PostLike::remove(conn, user_id, post_id)
PostLike::remove(conn, person_id, post_id)
})
.await??;

View file

@ -1,7 +1,7 @@
use crate::{
activities::receive::verify_activity_domains_valid,
check_is_apub_id_valid,
fetcher::user::get_or_fetch_and_upsert_user,
fetcher::person::get_or_fetch_and_upsert_person,
inbox::get_activity_to_and_cc,
objects::FromApub,
NoteExt,
@ -13,10 +13,10 @@ use activitystreams::{
public,
};
use anyhow::{anyhow, Context};
use lemmy_api_structs::{blocking, person::PrivateMessageResponse};
use lemmy_db_queries::source::private_message::PrivateMessage_;
use lemmy_db_schema::source::private_message::PrivateMessage;
use lemmy_db_views::private_message_view::PrivateMessageView;
use lemmy_structs::{blocking, user::PrivateMessageResponse};
use lemmy_db_views::{local_user_view::LocalUserView, private_message_view::PrivateMessageView};
use lemmy_utils::{location_info, LemmyError};
use lemmy_websocket::{messages::SendUserRoomMessage, LemmyContext, UserOperation};
use url::Url;
@ -50,12 +50,19 @@ pub(crate) async fn receive_create_private_message(
private_message_view: message,
};
// Send notifications to the local recipient, if one exists
let recipient_id = res.private_message_view.recipient.id;
let local_recipient_id = blocking(context.pool(), move |conn| {
LocalUserView::read_person(conn, recipient_id)
})
.await??
.local_user
.id;
context.chat_server().do_send(SendUserRoomMessage {
op: UserOperation::CreatePrivateMessage,
response: res,
recipient_id,
local_recipient_id,
websocket_id: None,
});
@ -91,11 +98,17 @@ pub(crate) async fn receive_update_private_message(
};
let recipient_id = res.private_message_view.recipient.id;
let local_recipient_id = blocking(context.pool(), move |conn| {
LocalUserView::read_person(conn, recipient_id)
})
.await??
.local_user
.id;
context.chat_server().do_send(SendUserRoomMessage {
op: UserOperation::EditPrivateMessage,
response: res,
recipient_id,
local_recipient_id,
websocket_id: None,
});
@ -123,11 +136,19 @@ pub(crate) async fn receive_delete_private_message(
let res = PrivateMessageResponse {
private_message_view: message,
};
let recipient_id = res.private_message_view.recipient.id;
let local_recipient_id = blocking(context.pool(), move |conn| {
LocalUserView::read_person(conn, recipient_id)
})
.await??
.local_user
.id;
context.chat_server().do_send(SendUserRoomMessage {
op: UserOperation::EditPrivateMessage,
response: res,
recipient_id,
local_recipient_id,
websocket_id: None,
});
@ -160,11 +181,19 @@ pub(crate) async fn receive_undo_delete_private_message(
let res = PrivateMessageResponse {
private_message_view: message,
};
let recipient_id = res.private_message_view.recipient.id;
let local_recipient_id = blocking(context.pool(), move |conn| {
LocalUserView::read_person(conn, recipient_id)
})
.await??
.local_user
.id;
context.chat_server().do_send(SendUserRoomMessage {
op: UserOperation::EditPrivateMessage,
response: res,
recipient_id,
local_recipient_id,
websocket_id: None,
});
@ -181,19 +210,19 @@ where
{
let to_and_cc = get_activity_to_and_cc(activity);
if to_and_cc.len() != 1 {
return Err(anyhow!("Private message can only be addressed to one user").into());
return Err(anyhow!("Private message can only be addressed to one person").into());
}
if to_and_cc.contains(&public()) {
return Err(anyhow!("Private message cant be public").into());
}
let user_id = activity
let person_id = activity
.actor()?
.to_owned()
.single_xsd_any_uri()
.context(location_info!())?;
check_is_apub_id_valid(&user_id)?;
// check that the sender is a user, not a community
get_or_fetch_and_upsert_user(&user_id, &context, request_counter).await?;
check_is_apub_id_valid(&person_id)?;
// check that the sender is a person, not a community
get_or_fetch_and_upsert_person(&person_id, &context, request_counter).await?;
Ok(())
}

View file

@ -2,7 +2,7 @@ use crate::{
activities::send::generate_activity_id,
activity_queue::{send_comment_mentions, send_to_community},
extensions::context::lemmy_context,
fetcher::user::get_or_fetch_and_upsert_user,
fetcher::person::get_or_fetch_and_upsert_person,
objects::ToApub,
ActorType,
ApubLikeableType,
@ -26,12 +26,12 @@ use activitystreams::{
};
use anyhow::anyhow;
use itertools::Itertools;
use lemmy_api_structs::{blocking, WebFingerResponse};
use lemmy_db_queries::{Crud, DbPool};
use lemmy_db_schema::source::{comment::Comment, community::Community, post::Post, user::User_};
use lemmy_structs::{blocking, WebFingerResponse};
use lemmy_db_schema::source::{comment::Comment, community::Community, person::Person, post::Post};
use lemmy_utils::{
request::{retry, RecvError},
settings::Settings,
settings::structs::Settings,
utils::{scrape_text_for_mentions, MentionData},
LemmyError,
};
@ -44,8 +44,8 @@ use url::Url;
#[async_trait::async_trait(?Send)]
impl ApubObjectType for Comment {
/// Send out information about a newly created comment, to the followers of the community and
/// mentioned users.
async fn send_create(&self, creator: &User_, context: &LemmyContext) -> Result<(), LemmyError> {
/// mentioned persons.
async fn send_create(&self, creator: &Person, context: &LemmyContext) -> Result<(), LemmyError> {
let note = self.to_apub(context.pool()).await?;
let post_id = self.post_id;
@ -57,17 +57,17 @@ impl ApubObjectType for Comment {
})
.await??;
let mut maa = collect_non_local_mentions_and_addresses(&self.content, context).await?;
let mut ccs = vec![community.actor_id()?];
ccs.append(&mut maa.addressed_ccs);
ccs.push(get_comment_parent_creator_id(context.pool(), &self).await?);
let maa = collect_non_local_mentions(&self, &community, context).await?;
let mut create = Create::new(creator.actor_id.to_owned(), note.into_any_base()?);
let mut create = Create::new(
creator.actor_id.to_owned().into_inner(),
note.into_any_base()?,
);
create
.set_many_contexts(lemmy_context()?)
.set_id(generate_activity_id(CreateType::Create)?)
.set_to(public())
.set_many_ccs(ccs)
.set_many_ccs(maa.ccs.to_owned())
// Set the mention tags
.set_many_tags(maa.get_tags()?);
@ -77,8 +77,8 @@ impl ApubObjectType for Comment {
}
/// Send out information about an edited post, to the followers of the community and mentioned
/// users.
async fn send_update(&self, creator: &User_, context: &LemmyContext) -> Result<(), LemmyError> {
/// persons.
async fn send_update(&self, creator: &Person, context: &LemmyContext) -> Result<(), LemmyError> {
let note = self.to_apub(context.pool()).await?;
let post_id = self.post_id;
@ -90,17 +90,17 @@ impl ApubObjectType for Comment {
})
.await??;
let mut maa = collect_non_local_mentions_and_addresses(&self.content, context).await?;
let mut ccs = vec![community.actor_id()?];
ccs.append(&mut maa.addressed_ccs);
ccs.push(get_comment_parent_creator_id(context.pool(), &self).await?);
let maa = collect_non_local_mentions(&self, &community, context).await?;
let mut update = Update::new(creator.actor_id.to_owned(), note.into_any_base()?);
let mut update = Update::new(
creator.actor_id.to_owned().into_inner(),
note.into_any_base()?,
);
update
.set_many_contexts(lemmy_context()?)
.set_id(generate_activity_id(UpdateType::Update)?)
.set_to(public())
.set_many_ccs(ccs)
.set_many_ccs(maa.ccs.to_owned())
// Set the mention tags
.set_many_tags(maa.get_tags()?);
@ -109,7 +109,7 @@ impl ApubObjectType for Comment {
Ok(())
}
async fn send_delete(&self, creator: &User_, context: &LemmyContext) -> Result<(), LemmyError> {
async fn send_delete(&self, creator: &Person, context: &LemmyContext) -> Result<(), LemmyError> {
let post_id = self.post_id;
let post = blocking(context.pool(), move |conn| Post::read(conn, post_id)).await??;
@ -119,12 +119,15 @@ impl ApubObjectType for Comment {
})
.await??;
let mut delete = Delete::new(creator.actor_id.to_owned(), Url::parse(&self.ap_id)?);
let mut delete = Delete::new(
creator.actor_id.to_owned().into_inner(),
self.ap_id.to_owned().into_inner(),
);
delete
.set_many_contexts(lemmy_context()?)
.set_id(generate_activity_id(DeleteType::Delete)?)
.set_to(public())
.set_many_ccs(vec![community.actor_id()?]);
.set_many_ccs(vec![community.actor_id()]);
send_to_community(delete, &creator, &community, context).await?;
Ok(())
@ -132,7 +135,7 @@ impl ApubObjectType for Comment {
async fn send_undo_delete(
&self,
creator: &User_,
creator: &Person,
context: &LemmyContext,
) -> Result<(), LemmyError> {
let post_id = self.post_id;
@ -145,26 +148,32 @@ impl ApubObjectType for Comment {
.await??;
// Generate a fake delete activity, with the correct object
let mut delete = Delete::new(creator.actor_id.to_owned(), Url::parse(&self.ap_id)?);
let mut delete = Delete::new(
creator.actor_id.to_owned().into_inner(),
self.ap_id.to_owned().into_inner(),
);
delete
.set_many_contexts(lemmy_context()?)
.set_id(generate_activity_id(DeleteType::Delete)?)
.set_to(public())
.set_many_ccs(vec![community.actor_id()?]);
.set_many_ccs(vec![community.actor_id()]);
// Undo that fake activity
let mut undo = Undo::new(creator.actor_id.to_owned(), delete.into_any_base()?);
let mut undo = Undo::new(
creator.actor_id.to_owned().into_inner(),
delete.into_any_base()?,
);
undo
.set_many_contexts(lemmy_context()?)
.set_id(generate_activity_id(UndoType::Undo)?)
.set_to(public())
.set_many_ccs(vec![community.actor_id()?]);
.set_many_ccs(vec![community.actor_id()]);
send_to_community(undo, &creator, &community, context).await?;
Ok(())
}
async fn send_remove(&self, mod_: &User_, context: &LemmyContext) -> Result<(), LemmyError> {
async fn send_remove(&self, mod_: &Person, context: &LemmyContext) -> Result<(), LemmyError> {
let post_id = self.post_id;
let post = blocking(context.pool(), move |conn| Post::read(conn, post_id)).await??;
@ -174,18 +183,25 @@ impl ApubObjectType for Comment {
})
.await??;
let mut remove = Remove::new(mod_.actor_id.to_owned(), Url::parse(&self.ap_id)?);
let mut remove = Remove::new(
mod_.actor_id.to_owned().into_inner(),
self.ap_id.to_owned().into_inner(),
);
remove
.set_many_contexts(lemmy_context()?)
.set_id(generate_activity_id(RemoveType::Remove)?)
.set_to(public())
.set_many_ccs(vec![community.actor_id()?]);
.set_many_ccs(vec![community.actor_id()]);
send_to_community(remove, &mod_, &community, context).await?;
Ok(())
}
async fn send_undo_remove(&self, mod_: &User_, context: &LemmyContext) -> Result<(), LemmyError> {
async fn send_undo_remove(
&self,
mod_: &Person,
context: &LemmyContext,
) -> Result<(), LemmyError> {
let post_id = self.post_id;
let post = blocking(context.pool(), move |conn| Post::read(conn, post_id)).await??;
@ -196,20 +212,26 @@ impl ApubObjectType for Comment {
.await??;
// Generate a fake delete activity, with the correct object
let mut remove = Remove::new(mod_.actor_id.to_owned(), Url::parse(&self.ap_id)?);
let mut remove = Remove::new(
mod_.actor_id.to_owned().into_inner(),
self.ap_id.to_owned().into_inner(),
);
remove
.set_many_contexts(lemmy_context()?)
.set_id(generate_activity_id(RemoveType::Remove)?)
.set_to(public())
.set_many_ccs(vec![community.actor_id()?]);
.set_many_ccs(vec![community.actor_id()]);
// Undo that fake activity
let mut undo = Undo::new(mod_.actor_id.to_owned(), remove.into_any_base()?);
let mut undo = Undo::new(
mod_.actor_id.to_owned().into_inner(),
remove.into_any_base()?,
);
undo
.set_many_contexts(lemmy_context()?)
.set_id(generate_activity_id(UndoType::Undo)?)
.set_to(public())
.set_many_ccs(vec![community.actor_id()?]);
.set_many_ccs(vec![community.actor_id()]);
send_to_community(undo, &mod_, &community, context).await?;
Ok(())
@ -218,7 +240,7 @@ impl ApubObjectType for Comment {
#[async_trait::async_trait(?Send)]
impl ApubLikeableType for Comment {
async fn send_like(&self, creator: &User_, context: &LemmyContext) -> Result<(), LemmyError> {
async fn send_like(&self, creator: &Person, context: &LemmyContext) -> Result<(), LemmyError> {
let post_id = self.post_id;
let post = blocking(context.pool(), move |conn| Post::read(conn, post_id)).await??;
@ -228,18 +250,21 @@ impl ApubLikeableType for Comment {
})
.await??;
let mut like = Like::new(creator.actor_id.to_owned(), Url::parse(&self.ap_id)?);
let mut like = Like::new(
creator.actor_id.to_owned().into_inner(),
self.ap_id.to_owned().into_inner(),
);
like
.set_many_contexts(lemmy_context()?)
.set_id(generate_activity_id(LikeType::Like)?)
.set_to(public())
.set_many_ccs(vec![community.actor_id()?]);
.set_many_ccs(vec![community.actor_id()]);
send_to_community(like, &creator, &community, context).await?;
Ok(())
}
async fn send_dislike(&self, creator: &User_, context: &LemmyContext) -> Result<(), LemmyError> {
async fn send_dislike(&self, creator: &Person, context: &LemmyContext) -> Result<(), LemmyError> {
let post_id = self.post_id;
let post = blocking(context.pool(), move |conn| Post::read(conn, post_id)).await??;
@ -249,12 +274,15 @@ impl ApubLikeableType for Comment {
})
.await??;
let mut dislike = Dislike::new(creator.actor_id.to_owned(), Url::parse(&self.ap_id)?);
let mut dislike = Dislike::new(
creator.actor_id.to_owned().into_inner(),
self.ap_id.to_owned().into_inner(),
);
dislike
.set_many_contexts(lemmy_context()?)
.set_id(generate_activity_id(DislikeType::Dislike)?)
.set_to(public())
.set_many_ccs(vec![community.actor_id()?]);
.set_many_ccs(vec![community.actor_id()]);
send_to_community(dislike, &creator, &community, context).await?;
Ok(())
@ -262,7 +290,7 @@ impl ApubLikeableType for Comment {
async fn send_undo_like(
&self,
creator: &User_,
creator: &Person,
context: &LemmyContext,
) -> Result<(), LemmyError> {
let post_id = self.post_id;
@ -274,20 +302,26 @@ impl ApubLikeableType for Comment {
})
.await??;
let mut like = Like::new(creator.actor_id.to_owned(), Url::parse(&self.ap_id)?);
let mut like = Like::new(
creator.actor_id.to_owned().into_inner(),
self.ap_id.to_owned().into_inner(),
);
like
.set_many_contexts(lemmy_context()?)
.set_id(generate_activity_id(DislikeType::Dislike)?)
.set_to(public())
.set_many_ccs(vec![community.actor_id()?]);
.set_many_ccs(vec![community.actor_id()]);
// Undo that fake activity
let mut undo = Undo::new(creator.actor_id.to_owned(), like.into_any_base()?);
let mut undo = Undo::new(
creator.actor_id.to_owned().into_inner(),
like.into_any_base()?,
);
undo
.set_many_contexts(lemmy_context()?)
.set_id(generate_activity_id(UndoType::Undo)?)
.set_to(public())
.set_many_ccs(vec![community.actor_id()?]);
.set_many_ccs(vec![community.actor_id()]);
send_to_community(undo, &creator, &community, context).await?;
Ok(())
@ -295,7 +329,7 @@ impl ApubLikeableType for Comment {
}
struct MentionsAndAddresses {
addressed_ccs: Vec<Url>,
ccs: Vec<Url>,
inboxes: Vec<Url>,
tags: Vec<Mention>,
}
@ -312,55 +346,57 @@ impl MentionsAndAddresses {
/// This takes a comment, and builds a list of to_addresses, inboxes,
/// and mention tags, so they know where to be sent to.
/// Addresses are the users / addresses that go in the cc field.
async fn collect_non_local_mentions_and_addresses(
content: &str,
/// Addresses are the persons / addresses that go in the cc field.
async fn collect_non_local_mentions(
comment: &Comment,
community: &Community,
context: &LemmyContext,
) -> Result<MentionsAndAddresses, LemmyError> {
let mut addressed_ccs = vec![];
let parent_creator = get_comment_parent_creator(context.pool(), comment).await?;
let mut addressed_ccs = vec![community.actor_id(), parent_creator.actor_id()];
// Note: dont include community inbox here, as we send to it separately with `send_to_community()`
let mut inboxes = vec![parent_creator.get_shared_inbox_or_inbox_url()];
// Add the mention tag
let mut tags = Vec::new();
// Get the inboxes for any mentions
let mentions = scrape_text_for_mentions(&content)
// Get the person IDs for any mentions
let mentions = scrape_text_for_mentions(&comment.content)
.into_iter()
// Filter only the non-local ones
.filter(|m| !m.is_local())
.collect::<Vec<MentionData>>();
let mut mention_inboxes: Vec<Url> = Vec::new();
for mention in &mentions {
// TODO should it be fetching it every time?
if let Ok(actor_id) = fetch_webfinger_url(mention, context.client()).await {
debug!("mention actor_id: {}", actor_id);
addressed_ccs.push(actor_id.to_owned().to_string().parse()?);
let mention_user = get_or_fetch_and_upsert_user(&actor_id, context, &mut 0).await?;
let shared_inbox = mention_user.get_shared_inbox_url()?;
let mention_person = get_or_fetch_and_upsert_person(&actor_id, context, &mut 0).await?;
inboxes.push(mention_person.get_shared_inbox_or_inbox_url());
mention_inboxes.push(shared_inbox);
let mut mention_tag = Mention::new();
mention_tag.set_href(actor_id).set_name(mention.full_name());
tags.push(mention_tag);
}
}
let inboxes = mention_inboxes.into_iter().unique().collect();
let inboxes = inboxes.into_iter().unique().collect();
Ok(MentionsAndAddresses {
addressed_ccs,
ccs: addressed_ccs,
inboxes,
tags,
})
}
/// Returns the apub ID of the user this comment is responding to. Meaning, in case this is a
/// Returns the apub ID of the person this comment is responding to. Meaning, in case this is a
/// top-level comment, the creator of the post, otherwise the creator of the parent comment.
async fn get_comment_parent_creator_id(
async fn get_comment_parent_creator(
pool: &DbPool,
comment: &Comment,
) -> Result<Url, LemmyError> {
) -> Result<Person, LemmyError> {
let parent_creator_id = if let Some(parent_comment_id) = comment.parent_id {
let parent_comment =
blocking(pool, move |conn| Comment::read(conn, parent_comment_id)).await??;
@ -370,11 +406,10 @@ async fn get_comment_parent_creator_id(
let parent_post = blocking(pool, move |conn| Post::read(conn, parent_post_id)).await??;
parent_post.creator_id
};
let parent_creator = blocking(pool, move |conn| User_::read(conn, parent_creator_id)).await??;
Ok(parent_creator.actor_id()?)
Ok(blocking(pool, move |conn| Person::read(conn, parent_creator_id)).await??)
}
/// Turns a user id like `@name@example.com` into an apub ID, like `https://example.com/user/name`,
/// Turns a person id like `@name@example.com` into an apub ID, like `https://example.com/user/name`,
/// using webfinger.
async fn fetch_webfinger_url(mention: &MentionData, client: &Client) -> Result<Url, LemmyError> {
let fetch_url = format!(
@ -401,7 +436,5 @@ async fn fetch_webfinger_url(mention: &MentionData, client: &Client) -> Result<U
link
.href
.to_owned()
.map(|u| Url::parse(&u))
.transpose()?
.ok_or_else(|| anyhow!("No href found.").into())
}

View file

@ -3,7 +3,8 @@ use crate::{
activity_queue::{send_activity_single_dest, send_to_community_followers},
check_is_apub_id_valid,
extensions::context::lemmy_context,
fetcher::user::get_or_fetch_and_upsert_user,
fetcher::person::get_or_fetch_and_upsert_person,
insert_activity,
ActorType,
};
use activitystreams::{
@ -23,20 +24,22 @@ use activitystreams::{
};
use anyhow::Context;
use itertools::Itertools;
use lemmy_api_structs::blocking;
use lemmy_db_queries::DbPool;
use lemmy_db_schema::source::community::Community;
use lemmy_db_views_actor::community_follower_view::CommunityFollowerView;
use lemmy_structs::blocking;
use lemmy_utils::{location_info, settings::Settings, LemmyError};
use lemmy_utils::{location_info, settings::structs::Settings, LemmyError};
use lemmy_websocket::LemmyContext;
use url::Url;
#[async_trait::async_trait(?Send)]
impl ActorType for Community {
fn actor_id_str(&self) -> String {
self.actor_id.to_owned()
fn is_local(&self) -> bool {
self.local
}
fn actor_id(&self) -> Url {
self.actor_id.to_owned().into_inner()
}
fn public_key(&self) -> Option<String> {
self.public_key.to_owned()
}
@ -44,6 +47,14 @@ impl ActorType for Community {
self.private_key.to_owned()
}
fn get_shared_inbox_or_inbox_url(&self) -> Url {
self
.shared_inbox_url
.clone()
.unwrap_or_else(|| self.inbox_url.to_owned())
.into()
}
async fn send_follow(
&self,
_follow_actor_id: &Url,
@ -60,7 +71,7 @@ impl ActorType for Community {
unimplemented!()
}
/// As a local community, accept the follow request from a remote user.
/// As a local community, accept the follow request from a remote person.
async fn send_accept_follow(
&self,
follow: Follow,
@ -70,26 +81,29 @@ impl ActorType for Community {
.actor()?
.as_single_xsd_any_uri()
.context(location_info!())?;
let user = get_or_fetch_and_upsert_user(actor_uri, context, &mut 0).await?;
let person = get_or_fetch_and_upsert_person(actor_uri, context, &mut 0).await?;
let mut accept = Accept::new(self.actor_id.to_owned(), follow.into_any_base()?);
let mut accept = Accept::new(
self.actor_id.to_owned().into_inner(),
follow.into_any_base()?,
);
accept
.set_many_contexts(lemmy_context()?)
.set_id(generate_activity_id(AcceptType::Accept)?)
.set_to(user.actor_id()?);
.set_to(person.actor_id());
send_activity_single_dest(accept, self, user.get_inbox_url()?, context).await?;
send_activity_single_dest(accept, self, person.inbox_url.into(), context).await?;
Ok(())
}
/// If the creator of a community deletes the community, send this to all followers.
async fn send_delete(&self, context: &LemmyContext) -> Result<(), LemmyError> {
let mut delete = Delete::new(self.actor_id()?, self.actor_id()?);
let mut delete = Delete::new(self.actor_id(), self.actor_id());
delete
.set_many_contexts(lemmy_context()?)
.set_id(generate_activity_id(DeleteType::Delete)?)
.set_to(public())
.set_many_ccs(vec![self.get_followers_url()?]);
.set_many_ccs(vec![self.followers_url.clone().into_inner()]);
send_to_community_followers(delete, self, context).await?;
Ok(())
@ -97,19 +111,19 @@ impl ActorType for Community {
/// If the creator of a community reverts the deletion of a community, send this to all followers.
async fn send_undo_delete(&self, context: &LemmyContext) -> Result<(), LemmyError> {
let mut delete = Delete::new(self.actor_id()?, self.actor_id()?);
let mut delete = Delete::new(self.actor_id(), self.actor_id());
delete
.set_many_contexts(lemmy_context()?)
.set_id(generate_activity_id(DeleteType::Delete)?)
.set_to(public())
.set_many_ccs(vec![self.get_followers_url()?]);
.set_many_ccs(vec![self.followers_url.clone().into_inner()]);
let mut undo = Undo::new(self.actor_id()?, delete.into_any_base()?);
let mut undo = Undo::new(self.actor_id(), delete.into_any_base()?);
undo
.set_many_contexts(lemmy_context()?)
.set_id(generate_activity_id(UndoType::Undo)?)
.set_to(public())
.set_many_ccs(vec![self.get_followers_url()?]);
.set_many_ccs(vec![self.followers_url.clone().into_inner()]);
send_to_community_followers(undo, self, context).await?;
Ok(())
@ -117,12 +131,12 @@ impl ActorType for Community {
/// If an admin removes a community, send this to all followers.
async fn send_remove(&self, context: &LemmyContext) -> Result<(), LemmyError> {
let mut remove = Remove::new(self.actor_id()?, self.actor_id()?);
let mut remove = Remove::new(self.actor_id(), self.actor_id());
remove
.set_many_contexts(lemmy_context()?)
.set_id(generate_activity_id(RemoveType::Remove)?)
.set_to(public())
.set_many_ccs(vec![self.get_followers_url()?]);
.set_many_ccs(vec![self.followers_url.clone().into_inner()]);
send_to_community_followers(remove, self, context).await?;
Ok(())
@ -130,20 +144,20 @@ impl ActorType for Community {
/// If an admin reverts the removal of a community, send this to all followers.
async fn send_undo_remove(&self, context: &LemmyContext) -> Result<(), LemmyError> {
let mut remove = Remove::new(self.actor_id()?, self.actor_id()?);
let mut remove = Remove::new(self.actor_id(), self.actor_id());
remove
.set_many_contexts(lemmy_context()?)
.set_id(generate_activity_id(RemoveType::Remove)?)
.set_to(public())
.set_many_ccs(vec![self.get_followers_url()?]);
.set_many_ccs(vec![self.followers_url.clone().into_inner()]);
// Undo that fake activity
let mut undo = Undo::new(self.actor_id()?, remove.into_any_base()?);
let mut undo = Undo::new(self.actor_id(), remove.into_any_base()?);
undo
.set_many_contexts(lemmy_context()?)
.set_id(generate_activity_id(LikeType::Like)?)
.set_to(public())
.set_many_ccs(vec![self.get_followers_url()?]);
.set_many_ccs(vec![self.followers_url.clone().into_inner()]);
send_to_community_followers(undo, self, context).await?;
Ok(())
@ -151,17 +165,26 @@ impl ActorType for Community {
/// Wraps an activity sent to the community in an announce, and then sends the announce to all
/// community followers.
///
/// If we are announcing a local activity, it hasn't been stored in the database yet, and we need
/// to do it here, so that it can be fetched by ID. Remote activities are inserted into DB in the
/// inbox.
async fn send_announce(
&self,
activity: AnyBase,
context: &LemmyContext,
) -> Result<(), LemmyError> {
let mut announce = Announce::new(self.actor_id.to_owned(), activity);
let inner_id = activity.id().context(location_info!())?;
if inner_id.domain() == Some(&Settings::get().get_hostname_without_port()?) {
insert_activity(inner_id, activity.clone(), true, false, context.pool()).await?;
}
let mut announce = Announce::new(self.actor_id.to_owned().into_inner(), activity);
announce
.set_many_contexts(lemmy_context()?)
.set_id(generate_activity_id(AnnounceType::Announce)?)
.set_to(public())
.set_many_ccs(vec![self.get_followers_url()?]);
.set_many_ccs(vec![self.followers_url.clone().into_inner()]);
send_to_community_followers(announce, self, context).await?;
@ -169,38 +192,21 @@ impl ActorType for Community {
}
/// For a given community, returns the inboxes of all followers.
///
/// TODO: this function is very badly implemented, we should just store shared_inbox_url in
/// CommunityFollowerView
async fn get_follower_inboxes(&self, pool: &DbPool) -> Result<Vec<Url>, LemmyError> {
let id = self.id;
let inboxes = blocking(pool, move |conn| {
let follows = blocking(pool, move |conn| {
CommunityFollowerView::for_community(conn, id)
})
.await??;
let inboxes = inboxes
let inboxes = follows
.into_iter()
.filter(|i| !i.follower.local)
.map(|u| -> Result<Url, LemmyError> {
let url = Url::parse(&u.follower.actor_id)?;
let domain = url.domain().context(location_info!())?;
let port = if let Some(port) = url.port() {
format!(":{}", port)
} else {
"".to_string()
};
Ok(Url::parse(&format!(
"{}://{}{}/inbox",
Settings::get().get_protocol_string(),
domain,
port,
))?)
})
.filter_map(Result::ok)
.filter(|f| !f.follower.local)
.map(|f| f.follower.shared_inbox_url.unwrap_or(f.follower.inbox_url))
.map(|i| i.into_inner())
.unique()
// Don't send to blocked instances
.filter(|inbox| check_is_apub_id_valid(inbox).is_ok())
.unique()
.collect();
Ok(inboxes)

View file

@ -1,12 +1,12 @@
use lemmy_utils::settings::Settings;
use lemmy_utils::settings::structs::Settings;
use url::{ParseError, Url};
use uuid::Uuid;
pub(crate) mod comment;
pub(crate) mod community;
pub(crate) mod person;
pub(crate) mod post;
pub(crate) mod private_message;
pub(crate) mod user;
/// Generate a unique ID for an activity, in the format:
/// `http(s)://example.com/receive/create/202daf0a-1489-45df-8d2e-c8a3173fed36`

View file

@ -13,20 +13,23 @@ use activitystreams::{
base::{AnyBase, BaseExt, ExtendsExt},
object::ObjectExt,
};
use lemmy_api_structs::blocking;
use lemmy_db_queries::{ApubObject, DbPool, Followable};
use lemmy_db_schema::source::{
community::{Community, CommunityFollower, CommunityFollowerForm},
user::User_,
person::Person,
};
use lemmy_structs::blocking;
use lemmy_utils::LemmyError;
use lemmy_websocket::LemmyContext;
use url::Url;
#[async_trait::async_trait(?Send)]
impl ActorType for User_ {
fn actor_id_str(&self) -> String {
self.actor_id.to_owned()
impl ActorType for Person {
fn is_local(&self) -> bool {
self.local
}
fn actor_id(&self) -> Url {
self.actor_id.to_owned().into_inner()
}
fn public_key(&self) -> Option<String> {
@ -37,21 +40,29 @@ impl ActorType for User_ {
self.private_key.to_owned()
}
/// As a given local user, send out a follow request to a remote community.
fn get_shared_inbox_or_inbox_url(&self) -> Url {
self
.shared_inbox_url
.clone()
.unwrap_or_else(|| self.inbox_url.to_owned())
.into()
}
/// As a given local person, send out a follow request to a remote community.
async fn send_follow(
&self,
follow_actor_id: &Url,
context: &LemmyContext,
) -> Result<(), LemmyError> {
let follow_actor_id = follow_actor_id.to_string();
let follow_actor_id = follow_actor_id.to_owned();
let community = blocking(context.pool(), move |conn| {
Community::read_from_apub_id(conn, &follow_actor_id)
Community::read_from_apub_id(conn, &follow_actor_id.into())
})
.await??;
let community_follower_form = CommunityFollowerForm {
community_id: community.id,
user_id: self.id,
person_id: self.id,
pending: true,
};
blocking(&context.pool(), move |conn| {
@ -59,13 +70,13 @@ impl ActorType for User_ {
})
.await?;
let mut follow = Follow::new(self.actor_id.to_owned(), community.actor_id()?);
let mut follow = Follow::new(self.actor_id.to_owned().into_inner(), community.actor_id());
follow
.set_many_contexts(lemmy_context()?)
.set_id(generate_activity_id(FollowType::Follow)?)
.set_to(community.actor_id()?);
.set_to(community.actor_id());
send_activity_single_dest(follow, self, community.get_inbox_url()?, context).await?;
send_activity_single_dest(follow, self, community.inbox_url.into(), context).await?;
Ok(())
}
@ -74,26 +85,29 @@ impl ActorType for User_ {
follow_actor_id: &Url,
context: &LemmyContext,
) -> Result<(), LemmyError> {
let follow_actor_id = follow_actor_id.to_string();
let follow_actor_id = follow_actor_id.to_owned();
let community = blocking(context.pool(), move |conn| {
Community::read_from_apub_id(conn, &follow_actor_id)
Community::read_from_apub_id(conn, &follow_actor_id.into())
})
.await??;
let mut follow = Follow::new(self.actor_id.to_owned(), community.actor_id()?);
let mut follow = Follow::new(self.actor_id.to_owned().into_inner(), community.actor_id());
follow
.set_many_contexts(lemmy_context()?)
.set_id(generate_activity_id(FollowType::Follow)?)
.set_to(community.actor_id()?);
.set_to(community.actor_id());
// Undo that fake activity
let mut undo = Undo::new(Url::parse(&self.actor_id)?, follow.into_any_base()?);
let mut undo = Undo::new(
self.actor_id.to_owned().into_inner(),
follow.into_any_base()?,
);
undo
.set_many_contexts(lemmy_context()?)
.set_id(generate_activity_id(UndoType::Undo)?)
.set_to(community.actor_id()?);
.set_to(community.actor_id());
send_activity_single_dest(undo, self, community.get_inbox_url()?, context).await?;
send_activity_single_dest(undo, self, community.inbox_url.into(), context).await?;
Ok(())
}

View file

@ -21,17 +21,16 @@ use activitystreams::{
prelude::*,
public,
};
use lemmy_api_structs::blocking;
use lemmy_db_queries::Crud;
use lemmy_db_schema::source::{community::Community, post::Post, user::User_};
use lemmy_structs::blocking;
use lemmy_db_schema::source::{community::Community, person::Person, post::Post};
use lemmy_utils::LemmyError;
use lemmy_websocket::LemmyContext;
use url::Url;
#[async_trait::async_trait(?Send)]
impl ApubObjectType for Post {
/// Send out information about a newly created post, to the followers of the community.
async fn send_create(&self, creator: &User_, context: &LemmyContext) -> Result<(), LemmyError> {
async fn send_create(&self, creator: &Person, context: &LemmyContext) -> Result<(), LemmyError> {
let page = self.to_apub(context.pool()).await?;
let community_id = self.community_id;
@ -40,19 +39,22 @@ impl ApubObjectType for Post {
})
.await??;
let mut create = Create::new(creator.actor_id.to_owned(), page.into_any_base()?);
let mut create = Create::new(
creator.actor_id.to_owned().into_inner(),
page.into_any_base()?,
);
create
.set_many_contexts(lemmy_context()?)
.set_id(generate_activity_id(CreateType::Create)?)
.set_to(public())
.set_many_ccs(vec![community.actor_id()?]);
.set_many_ccs(vec![community.actor_id()]);
send_to_community(create, creator, &community, context).await?;
Ok(())
}
/// Send out information about an edited post, to the followers of the community.
async fn send_update(&self, creator: &User_, context: &LemmyContext) -> Result<(), LemmyError> {
async fn send_update(&self, creator: &Person, context: &LemmyContext) -> Result<(), LemmyError> {
let page = self.to_apub(context.pool()).await?;
let community_id = self.community_id;
@ -61,30 +63,36 @@ impl ApubObjectType for Post {
})
.await??;
let mut update = Update::new(creator.actor_id.to_owned(), page.into_any_base()?);
let mut update = Update::new(
creator.actor_id.to_owned().into_inner(),
page.into_any_base()?,
);
update
.set_many_contexts(lemmy_context()?)
.set_id(generate_activity_id(UpdateType::Update)?)
.set_to(public())
.set_many_ccs(vec![community.actor_id()?]);
.set_many_ccs(vec![community.actor_id()]);
send_to_community(update, creator, &community, context).await?;
Ok(())
}
async fn send_delete(&self, creator: &User_, context: &LemmyContext) -> Result<(), LemmyError> {
async fn send_delete(&self, creator: &Person, context: &LemmyContext) -> Result<(), LemmyError> {
let community_id = self.community_id;
let community = blocking(context.pool(), move |conn| {
Community::read(conn, community_id)
})
.await??;
let mut delete = Delete::new(creator.actor_id.to_owned(), Url::parse(&self.ap_id)?);
let mut delete = Delete::new(
creator.actor_id.to_owned().into_inner(),
self.ap_id.to_owned().into_inner(),
);
delete
.set_many_contexts(lemmy_context()?)
.set_id(generate_activity_id(DeleteType::Delete)?)
.set_to(public())
.set_many_ccs(vec![community.actor_id()?]);
.set_many_ccs(vec![community.actor_id()]);
send_to_community(delete, creator, &community, context).await?;
Ok(())
@ -92,7 +100,7 @@ impl ApubObjectType for Post {
async fn send_undo_delete(
&self,
creator: &User_,
creator: &Person,
context: &LemmyContext,
) -> Result<(), LemmyError> {
let community_id = self.community_id;
@ -101,64 +109,83 @@ impl ApubObjectType for Post {
})
.await??;
let mut delete = Delete::new(creator.actor_id.to_owned(), Url::parse(&self.ap_id)?);
let mut delete = Delete::new(
creator.actor_id.to_owned().into_inner(),
self.ap_id.to_owned().into_inner(),
);
delete
.set_many_contexts(lemmy_context()?)
.set_id(generate_activity_id(DeleteType::Delete)?)
.set_to(public())
.set_many_ccs(vec![community.actor_id()?]);
.set_many_ccs(vec![community.actor_id()]);
// Undo that fake activity
let mut undo = Undo::new(creator.actor_id.to_owned(), delete.into_any_base()?);
let mut undo = Undo::new(
creator.actor_id.to_owned().into_inner(),
delete.into_any_base()?,
);
undo
.set_many_contexts(lemmy_context()?)
.set_id(generate_activity_id(UndoType::Undo)?)
.set_to(public())
.set_many_ccs(vec![community.actor_id()?]);
.set_many_ccs(vec![community.actor_id()]);
send_to_community(undo, creator, &community, context).await?;
Ok(())
}
async fn send_remove(&self, mod_: &User_, context: &LemmyContext) -> Result<(), LemmyError> {
async fn send_remove(&self, mod_: &Person, context: &LemmyContext) -> Result<(), LemmyError> {
let community_id = self.community_id;
let community = blocking(context.pool(), move |conn| {
Community::read(conn, community_id)
})
.await??;
let mut remove = Remove::new(mod_.actor_id.to_owned(), Url::parse(&self.ap_id)?);
let mut remove = Remove::new(
mod_.actor_id.to_owned().into_inner(),
self.ap_id.to_owned().into_inner(),
);
remove
.set_many_contexts(lemmy_context()?)
.set_id(generate_activity_id(RemoveType::Remove)?)
.set_to(public())
.set_many_ccs(vec![community.actor_id()?]);
.set_many_ccs(vec![community.actor_id()]);
send_to_community(remove, mod_, &community, context).await?;
Ok(())
}
async fn send_undo_remove(&self, mod_: &User_, context: &LemmyContext) -> Result<(), LemmyError> {
async fn send_undo_remove(
&self,
mod_: &Person,
context: &LemmyContext,
) -> Result<(), LemmyError> {
let community_id = self.community_id;
let community = blocking(context.pool(), move |conn| {
Community::read(conn, community_id)
})
.await??;
let mut remove = Remove::new(mod_.actor_id.to_owned(), Url::parse(&self.ap_id)?);
let mut remove = Remove::new(
mod_.actor_id.to_owned().into_inner(),
self.ap_id.to_owned().into_inner(),
);
remove
.set_many_contexts(lemmy_context()?)
.set_id(generate_activity_id(RemoveType::Remove)?)
.set_to(public())
.set_many_ccs(vec![community.actor_id()?]);
.set_many_ccs(vec![community.actor_id()]);
// Undo that fake activity
let mut undo = Undo::new(mod_.actor_id.to_owned(), remove.into_any_base()?);
let mut undo = Undo::new(
mod_.actor_id.to_owned().into_inner(),
remove.into_any_base()?,
);
undo
.set_many_contexts(lemmy_context()?)
.set_id(generate_activity_id(UndoType::Undo)?)
.set_to(public())
.set_many_ccs(vec![community.actor_id()?]);
.set_many_ccs(vec![community.actor_id()]);
send_to_community(undo, mod_, &community, context).await?;
Ok(())
@ -167,37 +194,43 @@ impl ApubObjectType for Post {
#[async_trait::async_trait(?Send)]
impl ApubLikeableType for Post {
async fn send_like(&self, creator: &User_, context: &LemmyContext) -> Result<(), LemmyError> {
async fn send_like(&self, creator: &Person, context: &LemmyContext) -> Result<(), LemmyError> {
let community_id = self.community_id;
let community = blocking(context.pool(), move |conn| {
Community::read(conn, community_id)
})
.await??;
let mut like = Like::new(creator.actor_id.to_owned(), Url::parse(&self.ap_id)?);
let mut like = Like::new(
creator.actor_id.to_owned().into_inner(),
self.ap_id.to_owned().into_inner(),
);
like
.set_many_contexts(lemmy_context()?)
.set_id(generate_activity_id(LikeType::Like)?)
.set_to(public())
.set_many_ccs(vec![community.actor_id()?]);
.set_many_ccs(vec![community.actor_id()]);
send_to_community(like, &creator, &community, context).await?;
Ok(())
}
async fn send_dislike(&self, creator: &User_, context: &LemmyContext) -> Result<(), LemmyError> {
async fn send_dislike(&self, creator: &Person, context: &LemmyContext) -> Result<(), LemmyError> {
let community_id = self.community_id;
let community = blocking(context.pool(), move |conn| {
Community::read(conn, community_id)
})
.await??;
let mut dislike = Dislike::new(creator.actor_id.to_owned(), Url::parse(&self.ap_id)?);
let mut dislike = Dislike::new(
creator.actor_id.to_owned().into_inner(),
self.ap_id.to_owned().into_inner(),
);
dislike
.set_many_contexts(lemmy_context()?)
.set_id(generate_activity_id(DislikeType::Dislike)?)
.set_to(public())
.set_many_ccs(vec![community.actor_id()?]);
.set_many_ccs(vec![community.actor_id()]);
send_to_community(dislike, &creator, &community, context).await?;
Ok(())
@ -205,7 +238,7 @@ impl ApubLikeableType for Post {
async fn send_undo_like(
&self,
creator: &User_,
creator: &Person,
context: &LemmyContext,
) -> Result<(), LemmyError> {
let community_id = self.community_id;
@ -214,20 +247,26 @@ impl ApubLikeableType for Post {
})
.await??;
let mut like = Like::new(creator.actor_id.to_owned(), Url::parse(&self.ap_id)?);
let mut like = Like::new(
creator.actor_id.to_owned().into_inner(),
self.ap_id.to_owned().into_inner(),
);
like
.set_many_contexts(lemmy_context()?)
.set_id(generate_activity_id(LikeType::Like)?)
.set_to(public())
.set_many_ccs(vec![community.actor_id()?]);
.set_many_ccs(vec![community.actor_id()]);
// Undo that fake activity
let mut undo = Undo::new(creator.actor_id.to_owned(), like.into_any_base()?);
let mut undo = Undo::new(
creator.actor_id.to_owned().into_inner(),
like.into_any_base()?,
);
undo
.set_many_contexts(lemmy_context()?)
.set_id(generate_activity_id(UndoType::Undo)?)
.set_to(public())
.set_many_ccs(vec![community.actor_id()?]);
.set_many_ccs(vec![community.actor_id()]);
send_to_community(undo, &creator, &community, context).await?;
Ok(())

View file

@ -0,0 +1,131 @@
use crate::{
activities::send::generate_activity_id,
activity_queue::send_activity_single_dest,
extensions::context::lemmy_context,
objects::ToApub,
ActorType,
ApubObjectType,
};
use activitystreams::{
activity::{
kind::{CreateType, DeleteType, UndoType, UpdateType},
Create,
Delete,
Undo,
Update,
},
prelude::*,
};
use lemmy_api_structs::blocking;
use lemmy_db_queries::Crud;
use lemmy_db_schema::source::{person::Person, private_message::PrivateMessage};
use lemmy_utils::LemmyError;
use lemmy_websocket::LemmyContext;
#[async_trait::async_trait(?Send)]
impl ApubObjectType for PrivateMessage {
/// Send out information about a newly created private message
async fn send_create(&self, creator: &Person, context: &LemmyContext) -> Result<(), LemmyError> {
let note = self.to_apub(context.pool()).await?;
let recipient_id = self.recipient_id;
let recipient =
blocking(context.pool(), move |conn| Person::read(conn, recipient_id)).await??;
let mut create = Create::new(
creator.actor_id.to_owned().into_inner(),
note.into_any_base()?,
);
create
.set_many_contexts(lemmy_context()?)
.set_id(generate_activity_id(CreateType::Create)?)
.set_to(recipient.actor_id());
send_activity_single_dest(create, creator, recipient.inbox_url.into(), context).await?;
Ok(())
}
/// Send out information about an edited private message, to the followers of the community.
async fn send_update(&self, creator: &Person, context: &LemmyContext) -> Result<(), LemmyError> {
let note = self.to_apub(context.pool()).await?;
let recipient_id = self.recipient_id;
let recipient =
blocking(context.pool(), move |conn| Person::read(conn, recipient_id)).await??;
let mut update = Update::new(
creator.actor_id.to_owned().into_inner(),
note.into_any_base()?,
);
update
.set_many_contexts(lemmy_context()?)
.set_id(generate_activity_id(UpdateType::Update)?)
.set_to(recipient.actor_id());
send_activity_single_dest(update, creator, recipient.inbox_url.into(), context).await?;
Ok(())
}
async fn send_delete(&self, creator: &Person, context: &LemmyContext) -> Result<(), LemmyError> {
let recipient_id = self.recipient_id;
let recipient =
blocking(context.pool(), move |conn| Person::read(conn, recipient_id)).await??;
let mut delete = Delete::new(
creator.actor_id.to_owned().into_inner(),
self.ap_id.to_owned().into_inner(),
);
delete
.set_many_contexts(lemmy_context()?)
.set_id(generate_activity_id(DeleteType::Delete)?)
.set_to(recipient.actor_id());
send_activity_single_dest(delete, creator, recipient.inbox_url.into(), context).await?;
Ok(())
}
async fn send_undo_delete(
&self,
creator: &Person,
context: &LemmyContext,
) -> Result<(), LemmyError> {
let recipient_id = self.recipient_id;
let recipient =
blocking(context.pool(), move |conn| Person::read(conn, recipient_id)).await??;
let mut delete = Delete::new(
creator.actor_id.to_owned().into_inner(),
self.ap_id.to_owned().into_inner(),
);
delete
.set_many_contexts(lemmy_context()?)
.set_id(generate_activity_id(DeleteType::Delete)?)
.set_to(recipient.actor_id());
// Undo that fake activity
let mut undo = Undo::new(
creator.actor_id.to_owned().into_inner(),
delete.into_any_base()?,
);
undo
.set_many_contexts(lemmy_context()?)
.set_id(generate_activity_id(UndoType::Undo)?)
.set_to(recipient.actor_id());
send_activity_single_dest(undo, creator, recipient.inbox_url.into(), context).await?;
Ok(())
}
async fn send_remove(&self, _mod_: &Person, _context: &LemmyContext) -> Result<(), LemmyError> {
unimplemented!()
}
async fn send_undo_remove(
&self,
_mod_: &Person,
_context: &LemmyContext,
) -> Result<(), LemmyError> {
unimplemented!()
}
}

View file

@ -21,13 +21,13 @@ use background_jobs::{
};
use itertools::Itertools;
use lemmy_db_queries::DbPool;
use lemmy_db_schema::source::{community::Community, user::User_};
use lemmy_utils::{location_info, settings::Settings, LemmyError};
use lemmy_db_schema::source::{community::Community, person::Person};
use lemmy_utils::{location_info, settings::structs::Settings, LemmyError};
use lemmy_websocket::LemmyContext;
use log::{debug, warn};
use reqwest::Client;
use serde::{export::fmt::Debug, Deserialize, Serialize};
use std::{collections::BTreeMap, env, future::Future, pin::Pin};
use serde::{Deserialize, Serialize};
use std::{collections::BTreeMap, env, fmt::Debug, future::Future, pin::Pin};
use url::Url;
/// Sends a local activity to a single, remote actor.
@ -88,13 +88,13 @@ where
.await?
.iter()
.unique()
.filter(|inbox| inbox.host_str() != Some(&Settings::get().hostname))
.filter(|inbox| inbox.host_str() != Some(&Settings::get().hostname()))
.filter(|inbox| check_is_apub_id_valid(inbox).is_ok())
.map(|inbox| inbox.to_owned())
.collect();
debug!(
"Sending activity {:?} to followers of {}",
&activity.id_unchecked(),
&activity.id_unchecked().map(|i| i.to_string()),
&community.actor_id
);
@ -112,7 +112,7 @@ where
Ok(())
}
/// Sends an activity from a local user to a remote community.
/// Sends an activity from a local person to a remote community.
///
/// * `activity` the activity to send
/// * `creator` the creator of the activity
@ -120,7 +120,7 @@ where
///
pub(crate) async fn send_to_community<T, Kind>(
activity: T,
creator: &User_,
creator: &Person,
community: &Community,
context: &LemmyContext,
) -> Result<(), LemmyError>
@ -135,7 +135,7 @@ where
.send_announce(activity.into_any_base()?, context)
.await?;
} else {
let inbox = community.get_shared_inbox_url()?;
let inbox = community.get_shared_inbox_or_inbox_url();
check_is_apub_id_valid(&inbox)?;
debug!(
"Sending activity {:?} to community {}",
@ -157,13 +157,13 @@ where
Ok(())
}
/// Sends notification to any users mentioned in a comment
/// Sends notification to any persons mentioned in a comment
///
/// * `creator` user who created the comment
/// * `mentions` list of inboxes of users which are mentioned in the comment
/// * `creator` person who created the comment
/// * `mentions` list of inboxes of persons which are mentioned in the comment
/// * `activity` either a `Create/Note` or `Update/Note`
pub(crate) async fn send_comment_mentions<T, Kind>(
creator: &User_,
creator: &Person,
mentions: Vec<Url>,
activity: T,
context: &LemmyContext,
@ -215,10 +215,17 @@ where
Kind: Serialize,
<T as Extends<Kind>>::Error: From<serde_json::Error> + Send + Sync + 'static,
{
if !Settings::get().federation.enabled || inboxes.is_empty() {
if !Settings::get().federation().enabled || inboxes.is_empty() {
return Ok(());
}
// Don't send anything to ourselves
let hostname = Settings::get().get_hostname_without_port()?;
let inboxes: Vec<&Url> = inboxes
.iter()
.filter(|i| i.domain().expect("valid inbox url") != hostname)
.collect();
let activity = activity.into_any_base()?;
let serialised_activity = serde_json::to_string(&activity)?;
@ -232,8 +239,8 @@ where
for i in inboxes {
let message = SendActivityTask {
activity: serialised_activity.to_owned(),
inbox: i,
actor_id: actor.actor_id()?,
inbox: i.to_owned(),
actor_id: actor.actor_id(),
private_key: actor.private_key().context(location_info!())?,
};
if env::var("LEMMY_TEST_SEND_SYNC").is_ok() {

View file

@ -6,7 +6,6 @@ pub(crate) fn lemmy_context() -> Result<Vec<AnyBase>, LemmyError> {
let context_ext = AnyBase::from_arbitrary_json(json!(
{
"sc": "http://schema.org#",
"category": "sc:category",
"sensitive": "as:sensitive",
"stickied": "as:stickied",
"comments_enabled": {

View file

@ -1,42 +1,20 @@
use activitystreams::unparsed::UnparsedMutExt;
use activitystreams_ext::UnparsedExtension;
use diesel::PgConnection;
use lemmy_db_queries::Crud;
use lemmy_db_schema::source::category::Category;
use lemmy_utils::LemmyError;
use serde::{Deserialize, Serialize};
/// Activitystreams extension to allow (de)serializing additional Community fields `category` and
/// Activitystreams extension to allow (de)serializing additional Community field
/// `sensitive` (called 'nsfw' in Lemmy).
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct GroupExtension {
pub category: GroupCategory,
pub sensitive: bool,
}
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct GroupCategory {
// Using a string because that's how Peertube does it.
pub identifier: String,
pub name: String,
pub sensitive: Option<bool>,
}
impl GroupExtension {
pub fn new(
conn: &PgConnection,
category_id: i32,
sensitive: bool,
) -> Result<GroupExtension, LemmyError> {
let category = Category::read(conn, category_id)?;
let group_category = GroupCategory {
identifier: category_id.to_string(),
name: category.name,
};
pub fn new(sensitive: bool) -> Result<GroupExtension, LemmyError> {
Ok(GroupExtension {
category: group_category,
sensitive,
sensitive: Some(sensitive),
})
}
}
@ -49,13 +27,11 @@ where
fn try_from_unparsed(unparsed_mut: &mut U) -> Result<Self, Self::Error> {
Ok(GroupExtension {
category: unparsed_mut.remove("category")?,
sensitive: unparsed_mut.remove("sensitive")?,
})
}
fn try_into_unparsed(self, unparsed_mut: &mut U) -> Result<(), Self::Error> {
unparsed_mut.insert("category", self.category)?;
unparsed_mut.insert("sensitive", self.sensitive)?;
Ok(())
}

View file

@ -8,9 +8,9 @@ use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct PageExtension {
pub comments_enabled: bool,
pub sensitive: bool,
pub stickied: bool,
pub comments_enabled: Option<bool>,
pub sensitive: Option<bool>,
pub stickied: Option<bool>,
}
impl<U> UnparsedExtension<U> for PageExtension

View file

@ -95,20 +95,20 @@ pub(crate) fn verify_signature(
}
}
/// Extension for actor public key, which is needed on user and community for HTTP signatures.
/// Extension for actor public key, which is needed on person and community for HTTP signatures.
///
/// Taken from: https://docs.rs/activitystreams/0.5.0-alpha.17/activitystreams/ext/index.html
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct PublicKeyExtension {
pub public_key: PublicKey,
}
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct PublicKey {
pub id: String,
pub owner: String,
pub owner: Url,
pub public_key_pem: String,
}

View file

@ -1,29 +1,24 @@
use crate::{
check_is_apub_id_valid,
fetcher::{
fetch::fetch_remote_object,
get_or_fetch_and_upsert_user,
get_or_fetch_and_upsert_person,
is_deleted,
should_refetch_actor,
},
inbox::person_inbox::receive_announce,
objects::FromApub,
ActorType,
GroupExt,
PageExt,
};
use activitystreams::{
base::{BaseExt, ExtendsExt},
actor::ApActorExt,
collection::{CollectionExt, OrderedCollection},
object::ObjectExt,
};
use anyhow::Context;
use diesel::result::Error::NotFound;
use lemmy_api_structs::blocking;
use lemmy_db_queries::{source::community::Community_, ApubObject, Joinable};
use lemmy_db_schema::source::{
community::{Community, CommunityModerator, CommunityModeratorForm},
post::Post,
};
use lemmy_structs::blocking;
use lemmy_db_schema::source::community::{Community, CommunityModerator, CommunityModeratorForm};
use lemmy_utils::{location_info, LemmyError};
use lemmy_websocket::LemmyContext;
use log::debug;
@ -40,7 +35,7 @@ pub(crate) async fn get_or_fetch_and_upsert_community(
) -> Result<Community, LemmyError> {
let apub_id_owned = apub_id.to_owned();
let community = blocking(context.pool(), move |conn| {
Community::read_from_apub_id(conn, apub_id_owned.as_str())
Community::read_from_apub_id(conn, &apub_id_owned.into())
})
.await?;
@ -97,7 +92,7 @@ async fn fetch_remote_community(
let mut creator_and_moderators = Vec::new();
for uri in creator_and_moderator_uris {
let c_or_m = get_or_fetch_and_upsert_user(uri, context, recursion_counter).await?;
let c_or_m = get_or_fetch_and_upsert_person(uri, context, recursion_counter).await?;
creator_and_moderators.push(c_or_m);
}
@ -109,7 +104,7 @@ async fn fetch_remote_community(
for mod_ in creator_and_moderators {
let community_moderator_form = CommunityModeratorForm {
community_id,
user_id: mod_.id,
person_id: mod_.id,
};
CommunityModerator::join(conn, &community_moderator_form)?;
@ -119,29 +114,32 @@ async fn fetch_remote_community(
.await??;
}
// fetch outbox (maybe make this conditional)
let outbox = fetch_remote_object::<OrderedCollection>(
context.client(),
&community.get_outbox_url()?,
recursion_counter,
)
.await?;
let outbox_items = outbox.items().context(location_info!())?.clone();
let mut outbox_items = outbox_items.many().context(location_info!())?;
if outbox_items.len() > 20 {
outbox_items = outbox_items[0..20].to_vec();
}
for o in outbox_items {
let page = PageExt::from_any_base(o)?.context(location_info!())?;
let page_id = page.id_unchecked().context(location_info!())?;
// The post creator may be from a blocked instance, if it errors, then skip it
if check_is_apub_id_valid(page_id).is_err() {
continue;
}
Post::from_apub(&page, context, page_id.to_owned(), recursion_counter).await?;
// TODO: we need to send a websocket update here
// only fetch outbox for new communities, otherwise this can create an infinite loop
if old_community.is_none() {
let outbox = group.inner.outbox()?.context(location_info!())?;
fetch_community_outbox(context, outbox, &community, recursion_counter).await?
}
Ok(community)
}
async fn fetch_community_outbox(
context: &LemmyContext,
outbox: &Url,
community: &Community,
recursion_counter: &mut i32,
) -> Result<(), LemmyError> {
let outbox =
fetch_remote_object::<OrderedCollection>(context.client(), outbox, recursion_counter).await?;
let outbox_activities = outbox.items().context(location_info!())?.clone();
let mut outbox_activities = outbox_activities.many().context(location_info!())?;
if outbox_activities.len() > 20 {
outbox_activities = outbox_activities[0..20].to_vec();
}
for activity in outbox_activities {
receive_announce(context, activity, community, recursion_counter).await?;
}
Ok(())
}

View file

@ -10,8 +10,9 @@ use url::Url;
/// Maximum number of HTTP requests allowed to handle a single incoming activity (or a single object
/// fetch through the search).
///
/// Tests are passing with a value of 5, so 10 should be safe for production.
static MAX_REQUEST_NUMBER: i32 = 10;
/// A community fetch will load the outbox with up to 20 items, and fetch the creator for each item.
/// So we are looking at a maximum of 22 requests (rounded up just to be safe).
static MAX_REQUEST_NUMBER: i32 = 25;
#[derive(Debug, Error)]
pub(in crate::fetcher) struct FetchError {

View file

@ -1,14 +1,14 @@
pub(crate) mod community;
mod fetch;
pub(crate) mod objects;
pub(crate) mod person;
pub mod search;
pub(crate) mod user;
use crate::{
fetcher::{
community::get_or_fetch_and_upsert_community,
fetch::FetchError,
user::get_or_fetch_and_upsert_user,
person::get_or_fetch_and_upsert_person,
},
ActorType,
};
@ -37,8 +37,8 @@ where
false
}
/// Get a remote actor from its apub ID (either a user or a community). Thin wrapper around
/// `get_or_fetch_and_upsert_user()` and `get_or_fetch_and_upsert_community()`.
/// Get a remote actor from its apub ID (either a person or a community). Thin wrapper around
/// `get_or_fetch_and_upsert_person()` and `get_or_fetch_and_upsert_community()`.
///
/// If it exists locally and `!should_refetch_actor()`, it is returned directly from the database.
/// Otherwise it is fetched from the remote instance, stored and returned.
@ -50,7 +50,7 @@ pub(crate) async fn get_or_fetch_and_upsert_actor(
let community = get_or_fetch_and_upsert_community(apub_id, context, recursion_counter).await;
let actor: Box<dyn ActorType> = match community {
Ok(c) => Box::new(c),
Err(_) => Box::new(get_or_fetch_and_upsert_user(apub_id, context, recursion_counter).await?),
Err(_) => Box::new(get_or_fetch_and_upsert_person(apub_id, context, recursion_counter).await?),
};
Ok(actor)
}

View file

@ -1,9 +1,9 @@
use crate::{fetcher::fetch::fetch_remote_object, objects::FromApub, NoteExt, PageExt};
use anyhow::anyhow;
use diesel::result::Error::NotFound;
use lemmy_api_structs::blocking;
use lemmy_db_queries::{ApubObject, Crud};
use lemmy_db_schema::source::{comment::Comment, post::Post};
use lemmy_structs::blocking;
use lemmy_utils::LemmyError;
use lemmy_websocket::LemmyContext;
use log::debug;
@ -20,7 +20,7 @@ pub(crate) async fn get_or_fetch_and_insert_post(
) -> Result<Post, LemmyError> {
let post_ap_id_owned = post_ap_id.to_owned();
let post = blocking(context.pool(), move |conn| {
Post::read_from_apub_id(conn, post_ap_id_owned.as_str())
Post::read_from_apub_id(conn, &post_ap_id_owned.into())
})
.await?;
@ -49,7 +49,7 @@ pub(crate) async fn get_or_fetch_and_insert_comment(
) -> Result<Comment, LemmyError> {
let comment_ap_id_owned = comment_ap_id.to_owned();
let comment = blocking(context.pool(), move |conn| {
Comment::read_from_apub_id(conn, comment_ap_id_owned.as_str())
Comment::read_from_apub_id(conn, &comment_ap_id_owned.into())
})
.await?;

View file

@ -5,66 +5,68 @@ use crate::{
};
use anyhow::anyhow;
use diesel::result::Error::NotFound;
use lemmy_db_queries::{source::user::User, ApubObject};
use lemmy_db_schema::source::user::User_;
use lemmy_structs::blocking;
use lemmy_api_structs::blocking;
use lemmy_db_queries::{source::person::Person_, ApubObject};
use lemmy_db_schema::source::person::Person;
use lemmy_utils::LemmyError;
use lemmy_websocket::LemmyContext;
use log::debug;
use url::Url;
/// Get a user from its apub ID.
/// Get a person from its apub ID.
///
/// If it exists locally and `!should_refetch_actor()`, it is returned directly from the database.
/// Otherwise it is fetched from the remote instance, stored and returned.
pub(crate) async fn get_or_fetch_and_upsert_user(
pub(crate) async fn get_or_fetch_and_upsert_person(
apub_id: &Url,
context: &LemmyContext,
recursion_counter: &mut i32,
) -> Result<User_, LemmyError> {
) -> Result<Person, LemmyError> {
let apub_id_owned = apub_id.to_owned();
let user = blocking(context.pool(), move |conn| {
User_::read_from_apub_id(conn, apub_id_owned.as_ref())
let person = blocking(context.pool(), move |conn| {
Person::read_from_apub_id(conn, &apub_id_owned.into())
})
.await?;
match user {
match person {
// If its older than a day, re-fetch it
Ok(u) if !u.local && should_refetch_actor(u.last_refreshed_at) => {
debug!("Fetching and updating from remote user: {}", apub_id);
debug!("Fetching and updating from remote person: {}", apub_id);
let person =
fetch_remote_object::<PersonExt>(context.client(), apub_id, recursion_counter).await;
if is_deleted(&person) {
// TODO: use User_::update_deleted() once implemented
// TODO: use Person::update_deleted() once implemented
blocking(context.pool(), move |conn| {
User_::delete_account(conn, u.id)
Person::delete_account(conn, u.id)
})
.await??;
return Err(anyhow!("User was deleted by remote instance").into());
return Err(anyhow!("Person was deleted by remote instance").into());
} else if person.is_err() {
return Ok(u);
}
let user = User_::from_apub(&person?, context, apub_id.to_owned(), recursion_counter).await?;
let person =
Person::from_apub(&person?, context, apub_id.to_owned(), recursion_counter).await?;
let user_id = user.id;
let person_id = person.id;
blocking(context.pool(), move |conn| {
User_::mark_as_updated(conn, user_id)
Person::mark_as_updated(conn, person_id)
})
.await??;
Ok(user)
Ok(person)
}
Ok(u) => Ok(u),
Err(NotFound {}) => {
debug!("Fetching and creating remote user: {}", apub_id);
debug!("Fetching and creating remote person: {}", apub_id);
let person =
fetch_remote_object::<PersonExt>(context.client(), apub_id, recursion_counter).await?;
let user = User_::from_apub(&person, context, apub_id.to_owned(), recursion_counter).await?;
let person =
Person::from_apub(&person, context, apub_id.to_owned(), recursion_counter).await?;
Ok(user)
Ok(person)
}
Err(e) => Err(e.into()),
}

View file

@ -2,7 +2,7 @@ use crate::{
fetcher::{
fetch::fetch_remote_object,
get_or_fetch_and_upsert_community,
get_or_fetch_and_upsert_user,
get_or_fetch_and_upsert_person,
is_deleted,
},
find_object_by_id,
@ -15,34 +15,34 @@ use crate::{
};
use activitystreams::base::BaseExt;
use anyhow::{anyhow, Context};
use lemmy_api_structs::{blocking, site::SearchResponse};
use lemmy_db_queries::{
source::{
comment::Comment_,
community::Community_,
person::Person_,
post::Post_,
private_message::PrivateMessage_,
user::User,
},
SearchType,
};
use lemmy_db_schema::source::{
comment::Comment,
community::Community,
person::Person,
post::Post,
private_message::PrivateMessage,
user::User_,
};
use lemmy_db_views::{comment_view::CommentView, post_view::PostView};
use lemmy_db_views_actor::{community_view::CommunityView, user_view::UserViewSafe};
use lemmy_structs::{blocking, site::SearchResponse};
use lemmy_utils::{settings::Settings, LemmyError};
use lemmy_db_views_actor::{community_view::CommunityView, person_view::PersonViewSafe};
use lemmy_utils::{settings::structs::Settings, LemmyError};
use lemmy_websocket::LemmyContext;
use log::debug;
use url::Url;
/// The types of ActivityPub objects that can be fetched directly by searching for their ID.
#[serde(untagged)]
#[derive(serde::Deserialize, Debug)]
#[serde(untagged)]
enum SearchAcceptedObjects {
Person(Box<PersonExt>),
Group(Box<GroupExt>),
@ -66,7 +66,7 @@ pub async fn search_by_apub_id(
debug!("Search for {}", query);
let split = query.split('@').collect::<Vec<&str>>();
// User type will look like ['', username, instance]
// Person type will look like ['', username, instance]
// Community will look like [!community, instance]
let (name, instance) = if split.len() == 3 {
(format!("/u/{}", split[1]), split[2])
@ -100,7 +100,9 @@ pub async fn search_by_apub_id(
delete_object_locally(&query_url, context).await?;
}
build_response(fetch_response?, query_url, recursion_counter, context).await
// Necessary because we get a stack overflow using FetchError
let fet_res = fetch_response.map_err(|e| LemmyError::from(e.inner))?;
build_response(fet_res, query_url, recursion_counter, context).await
}
async fn build_response(
@ -120,13 +122,13 @@ async fn build_response(
match fetch_response {
SearchAcceptedObjects::Person(p) => {
let user_uri = p.inner.id(domain)?.context("person has no id")?;
let person_uri = p.inner.id(domain)?.context("person has no id")?;
let user = get_or_fetch_and_upsert_user(&user_uri, context, recursion_counter).await?;
let person = get_or_fetch_and_upsert_person(&person_uri, context, recursion_counter).await?;
response.users = vec![
blocking(context.pool(), move |conn| {
UserViewSafe::read(conn, user.id)
PersonViewSafe::read(conn, person.id)
})
.await??,
];
@ -180,10 +182,10 @@ async fn delete_object_locally(query_url: &Url, context: &LemmyContext) -> Resul
})
.await??;
}
Object::User(u) => {
Object::Person(u) => {
// TODO: implement update_deleted() for user, move it to ApubObject trait
blocking(context.pool(), move |conn| {
User_::delete_account(conn, u.id)
Person::delete_account(conn, u.id)
})
.await??;
}

View file

@ -4,9 +4,9 @@ use crate::{
};
use actix_web::{body::Body, web, web::Path, HttpResponse};
use diesel::result::Error::NotFound;
use lemmy_api_structs::blocking;
use lemmy_db_queries::Crud;
use lemmy_db_schema::source::comment::Comment;
use lemmy_structs::blocking;
use lemmy_db_schema::{source::comment::Comment, CommentId};
use lemmy_utils::LemmyError;
use lemmy_websocket::LemmyContext;
use serde::Deserialize;
@ -21,7 +21,7 @@ pub async fn get_apub_comment(
info: Path<CommentQuery>,
context: web::Data<LemmyContext>,
) -> Result<HttpResponse<Body>, LemmyError> {
let id = info.comment_id.parse::<i32>()?;
let id = CommentId(info.comment_id.parse::<i32>()?);
let comment = blocking(context.pool(), move |conn| Comment::read(conn, id)).await??;
if !comment.local {
return Err(NotFound.into());

View file

@ -5,14 +5,14 @@ use crate::{
ActorType,
};
use activitystreams::{
base::{AnyBase, BaseExt, ExtendsExt},
base::{AnyBase, BaseExt},
collection::{CollectionExt, OrderedCollection, UnorderedCollection},
};
use actix_web::{body::Body, web, HttpResponse};
use lemmy_db_queries::source::{community::Community_, post::Post_};
use lemmy_db_schema::source::{community::Community, post::Post};
use lemmy_api_structs::blocking;
use lemmy_db_queries::source::{activity::Activity_, community::Community_};
use lemmy_db_schema::source::{activity::Activity, community::Community};
use lemmy_db_views_actor::community_follower_view::CommunityFollowerView;
use lemmy_structs::blocking;
use lemmy_utils::LemmyError;
use lemmy_websocket::LemmyContext;
use serde::Deserialize;
@ -60,7 +60,7 @@ pub async fn get_apub_community_followers(
let mut collection = UnorderedCollection::new();
collection
.set_many_contexts(lemmy_context()?)
.set_id(community.get_followers_url()?)
.set_id(community.followers_url.into())
.set_total_items(community_followers.len() as u64);
Ok(create_apub_response(&collection))
}
@ -76,21 +76,20 @@ pub async fn get_apub_community_outbox(
})
.await??;
let community_id = community.id;
let posts = blocking(context.pool(), move |conn| {
Post::list_for_community(conn, community_id)
let community_actor_id = community.actor_id.to_owned();
let activities = blocking(context.pool(), move |conn| {
Activity::read_community_outbox(conn, &community_actor_id)
})
.await??;
let mut pages: Vec<AnyBase> = vec![];
for p in posts {
pages.push(p.to_apub(context.pool()).await?.into_any_base()?);
}
let len = pages.len();
let activities = activities
.iter()
.map(AnyBase::from_arbitrary_json)
.collect::<Result<Vec<AnyBase>, serde_json::Error>>()?;
let len = activities.len();
let mut collection = OrderedCollection::new();
collection
.set_many_items(pages)
.set_many_items(activities)
.set_many_contexts(lemmy_context()?)
.set_id(community.get_outbox_url()?)
.set_total_items(len as u64);

View file

@ -1,17 +1,18 @@
use crate::APUB_JSON_CONTENT_TYPE;
use actix_web::{body::Body, web, HttpResponse};
use http::StatusCode;
use lemmy_api_structs::blocking;
use lemmy_db_queries::source::activity::Activity_;
use lemmy_db_schema::source::activity::Activity;
use lemmy_structs::blocking;
use lemmy_utils::{settings::Settings, LemmyError};
use lemmy_utils::{settings::structs::Settings, LemmyError};
use lemmy_websocket::LemmyContext;
use serde::{Deserialize, Serialize};
use url::Url;
pub mod comment;
pub mod community;
pub mod person;
pub mod post;
pub mod user;
/// Convert the data to json and turn it into an HTTP Response with the correct ActivityPub
/// headers.
@ -46,12 +47,13 @@ pub async fn get_activity(
context: web::Data<LemmyContext>,
) -> Result<HttpResponse<Body>, LemmyError> {
let settings = Settings::get();
let activity_id = format!(
let activity_id = Url::parse(&format!(
"{}/activities/{}/{}",
settings.get_protocol_and_hostname(),
info.type_,
info.id
);
))?
.into();
let activity = blocking(context.pool(), move |conn| {
Activity::read_from_apub_id(&conn, &activity_id)
})

View file

@ -9,70 +9,70 @@ use activitystreams::{
collection::{CollectionExt, OrderedCollection},
};
use actix_web::{body::Body, web, HttpResponse};
use lemmy_db_queries::source::user::User;
use lemmy_db_schema::source::user::User_;
use lemmy_structs::blocking;
use lemmy_api_structs::blocking;
use lemmy_db_queries::source::person::Person_;
use lemmy_db_schema::source::person::Person;
use lemmy_utils::LemmyError;
use lemmy_websocket::LemmyContext;
use serde::Deserialize;
use url::Url;
#[derive(Deserialize)]
pub struct UserQuery {
pub struct PersonQuery {
user_name: String,
}
/// Return the ActivityPub json representation of a local user over HTTP.
pub async fn get_apub_user_http(
info: web::Path<UserQuery>,
/// Return the ActivityPub json representation of a local person over HTTP.
pub async fn get_apub_person_http(
info: web::Path<PersonQuery>,
context: web::Data<LemmyContext>,
) -> Result<HttpResponse<Body>, LemmyError> {
let user_name = info.into_inner().user_name;
// TODO: this needs to be able to read deleted users, so that it can send tombstones
let user = blocking(context.pool(), move |conn| {
User_::find_by_email_or_username(conn, &user_name)
// TODO: this needs to be able to read deleted persons, so that it can send tombstones
let person = blocking(context.pool(), move |conn| {
Person::find_by_name(conn, &user_name)
})
.await??;
if !user.deleted {
let apub = user.to_apub(context.pool()).await?;
if !person.deleted {
let apub = person.to_apub(context.pool()).await?;
Ok(create_apub_response(&apub))
} else {
Ok(create_apub_tombstone_response(&user.to_tombstone()?))
Ok(create_apub_tombstone_response(&person.to_tombstone()?))
}
}
pub async fn get_apub_user_outbox(
info: web::Path<UserQuery>,
pub async fn get_apub_person_outbox(
info: web::Path<PersonQuery>,
context: web::Data<LemmyContext>,
) -> Result<HttpResponse<Body>, LemmyError> {
let user = blocking(context.pool(), move |conn| {
User_::read_from_name(&conn, &info.user_name)
let person = blocking(context.pool(), move |conn| {
Person::find_by_name(&conn, &info.user_name)
})
.await??;
// TODO: populate the user outbox
// TODO: populate the person outbox
let mut collection = OrderedCollection::new();
collection
.set_many_items(Vec::<Url>::new())
.set_many_contexts(lemmy_context()?)
.set_id(user.get_outbox_url()?)
.set_id(person.get_outbox_url()?)
.set_total_items(0_u64);
Ok(create_apub_response(&collection))
}
pub async fn get_apub_user_inbox(
info: web::Path<UserQuery>,
pub async fn get_apub_person_inbox(
info: web::Path<PersonQuery>,
context: web::Data<LemmyContext>,
) -> Result<HttpResponse<Body>, LemmyError> {
let user = blocking(context.pool(), move |conn| {
User_::read_from_name(&conn, &info.user_name)
let person = blocking(context.pool(), move |conn| {
Person::find_by_name(&conn, &info.user_name)
})
.await??;
let mut collection = OrderedCollection::new();
collection
.set_id(format!("{}/inbox", user.actor_id).parse()?)
.set_id(format!("{}/inbox", person.actor_id.into_inner()).parse()?)
.set_many_contexts(lemmy_context()?);
Ok(create_apub_response(&collection))
}

View file

@ -4,9 +4,9 @@ use crate::{
};
use actix_web::{body::Body, web, HttpResponse};
use diesel::result::Error::NotFound;
use lemmy_api_structs::blocking;
use lemmy_db_queries::Crud;
use lemmy_db_schema::source::post::Post;
use lemmy_structs::blocking;
use lemmy_db_schema::{source::post::Post, PostId};
use lemmy_utils::LemmyError;
use lemmy_websocket::LemmyContext;
use serde::Deserialize;
@ -21,7 +21,7 @@ pub async fn get_apub_post(
info: web::Path<PostQuery>,
context: web::Data<LemmyContext>,
) -> Result<HttpResponse<Body>, LemmyError> {
let id = info.post_id.parse::<i32>()?;
let id = PostId(info.post_id.parse::<i32>()?);
let post = blocking(context.pool(), move |conn| Post::read(conn, id)).await??;
if !post.local {
return Err(NotFound.into());

View file

@ -26,13 +26,16 @@ use activitystreams::{
};
use actix_web::{web, HttpRequest, HttpResponse};
use anyhow::{anyhow, Context};
use lemmy_api_structs::blocking;
use lemmy_db_queries::{source::community::Community_, ApubObject, DbPool, Followable};
use lemmy_db_schema::source::{
use lemmy_db_schema::{
source::{
community::{Community, CommunityFollower, CommunityFollowerForm},
user::User_,
person::Person,
},
CommunityId,
};
use lemmy_db_views_actor::community_user_ban_view::CommunityUserBanView;
use lemmy_structs::blocking;
use lemmy_db_views_actor::community_person_ban_view::CommunityPersonBanView;
use lemmy_utils::{location_info, LemmyError};
use lemmy_websocket::LemmyContext;
use log::info;
@ -44,8 +47,8 @@ use url::Url;
#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd, Deserialize, Serialize)]
#[serde(rename_all = "PascalCase")]
pub enum CommunityValidTypes {
Follow, // follow request from a user
Undo, // unfollow from a user
Follow, // follow request from a person
Undo, // unfollow from a person
Create, // create post or comment
Update, // update post or comment
Like, // upvote post or comment
@ -69,7 +72,7 @@ pub async fn community_inbox(
let actor = inbox_verify_http_signature(&activity, &context, request, request_counter).await?;
// Do nothing if we received the same activity before
let activity_id = get_activity_id(&activity, &actor.actor_id()?)?;
let activity_id = get_activity_id(&activity, &actor.actor_id())?;
if is_activity_already_known(context.pool(), &activity_id).await? {
return Ok(HttpResponse::Ok().finish());
}
@ -81,7 +84,7 @@ pub async fn community_inbox(
})
.await??;
let to_and_cc = get_activity_to_and_cc(&activity);
if !to_and_cc.contains(&&community.actor_id()?) {
if !to_and_cc.contains(&&community.actor_id()) {
return Err(anyhow!("Activity delivered to wrong community").into());
}
@ -92,7 +95,7 @@ pub async fn community_inbox(
"Community {} received activity {:?} from {}",
community.name,
&activity.id_unchecked(),
&actor.actor_id_str()
&actor.actor_id()
);
community_receive_message(
@ -113,21 +116,21 @@ pub(crate) async fn community_receive_message(
context: &LemmyContext,
request_counter: &mut i32,
) -> Result<HttpResponse, LemmyError> {
// Only users can send activities to the community, so we can get the actor as user
// Only persons can send activities to the community, so we can get the actor as person
// unconditionally.
let actor_id = actor.actor_id_str();
let user = blocking(&context.pool(), move |conn| {
User_::read_from_apub_id(&conn, &actor_id)
let actor_id = actor.actor_id();
let person = blocking(&context.pool(), move |conn| {
Person::read_from_apub_id(&conn, &actor_id.into())
})
.await??;
check_community_or_site_ban(&user, &to_community, context.pool()).await?;
check_community_or_site_ban(&person, to_community.id, context.pool()).await?;
let any_base = activity.clone().into_any_base()?;
let actor_url = actor.actor_id()?;
let actor_url = actor.actor_id();
let activity_kind = activity.kind().context(location_info!())?;
let do_announce = match activity_kind {
CommunityValidTypes::Follow => {
handle_follow(any_base.clone(), user, &to_community, &context).await?;
handle_follow(any_base.clone(), person, &to_community, &context).await?;
false
}
CommunityValidTypes::Undo => {
@ -162,7 +165,7 @@ pub(crate) async fn community_receive_message(
}
CommunityValidTypes::Remove => {
// TODO: we dont support remote mods, so this is ignored for now
//receive_remove_for_community(context, any_base.clone(), &user_url).await?
//receive_remove_for_community(context, any_base.clone(), &person_url).await?
false
}
};
@ -178,20 +181,20 @@ pub(crate) async fn community_receive_message(
Ok(HttpResponse::Ok().finish())
}
/// Handle a follow request from a remote user, adding the user as follower and returning an
/// Handle a follow request from a remote person, adding the person as follower and returning an
/// Accept activity.
async fn handle_follow(
activity: AnyBase,
user: User_,
person: Person,
community: &Community,
context: &LemmyContext,
) -> Result<HttpResponse, LemmyError> {
let follow = Follow::from_any_base(activity)?.context(location_info!())?;
verify_activity_domains_valid(&follow, &user.actor_id()?, false)?;
verify_activity_domains_valid(&follow, &person.actor_id(), false)?;
let community_follower_form = CommunityFollowerForm {
community_id: community.id,
user_id: user.id,
person_id: person.id,
pending: false,
};
@ -226,27 +229,27 @@ async fn handle_undo(
}
}
/// Handle `Undo/Follow` from a user, removing the user from followers list.
/// Handle `Undo/Follow` from a person, removing the person from followers list.
async fn handle_undo_follow(
activity: AnyBase,
user_url: Url,
person_url: Url,
community: &Community,
context: &LemmyContext,
) -> Result<(), LemmyError> {
let undo = Undo::from_any_base(activity)?.context(location_info!())?;
verify_activity_domains_valid(&undo, &user_url, true)?;
verify_activity_domains_valid(&undo, &person_url, true)?;
let object = undo.object().to_owned().one().context(location_info!())?;
let follow = Follow::from_any_base(object)?.context(location_info!())?;
verify_activity_domains_valid(&follow, &user_url, false)?;
verify_activity_domains_valid(&follow, &person_url, false)?;
let user = blocking(&context.pool(), move |conn| {
User_::read_from_apub_id(&conn, user_url.as_str())
let person = blocking(&context.pool(), move |conn| {
Person::read_from_apub_id(&conn, &person_url.into())
})
.await??;
let community_follower_form = CommunityFollowerForm {
community_id: community.id,
user_id: user.id,
person_id: person.id,
pending: false,
};
@ -260,18 +263,18 @@ async fn handle_undo_follow(
}
pub(crate) async fn check_community_or_site_ban(
user: &User_,
community: &Community,
person: &Person,
community_id: CommunityId,
pool: &DbPool,
) -> Result<(), LemmyError> {
if user.banned {
return Err(anyhow!("User is banned from site").into());
if person.banned {
return Err(anyhow!("Person is banned from site").into());
}
let user_id = user.id;
let community_id = community.id;
let is_banned = move |conn: &'_ _| CommunityUserBanView::get(conn, user_id, community_id).is_ok();
let person_id = person.id;
let is_banned =
move |conn: &'_ _| CommunityPersonBanView::get(conn, person_id, community_id).is_ok();
if blocking(pool, is_banned).await? {
return Err(anyhow!("User is banned from community").into());
return Err(anyhow!("Person is banned from community").into());
}
Ok(())

View file

@ -12,18 +12,23 @@ use activitystreams::{
};
use actix_web::HttpRequest;
use anyhow::{anyhow, Context};
use lemmy_db_queries::{source::activity::Activity_, ApubObject, DbPool};
use lemmy_db_schema::source::{activity::Activity, community::Community, user::User_};
use lemmy_structs::blocking;
use lemmy_utils::{location_info, settings::Settings, LemmyError};
use lemmy_api_structs::blocking;
use lemmy_db_queries::{
source::{activity::Activity_, community::Community_},
ApubObject,
DbPool,
};
use lemmy_db_schema::source::{activity::Activity, community::Community, person::Person};
use lemmy_utils::{location_info, settings::structs::Settings, LemmyError};
use lemmy_websocket::LemmyContext;
use serde::{export::fmt::Debug, Serialize};
use serde::Serialize;
use std::fmt::Debug;
use url::Url;
pub mod community_inbox;
pub mod person_inbox;
mod receive_for_community;
pub mod shared_inbox;
pub mod user_inbox;
pub(crate) fn get_activity_id<T, Kind>(activity: &T, creator_uri: &Url) -> Result<Url, LemmyError>
where
@ -40,7 +45,7 @@ pub(crate) async fn is_activity_already_known(
pool: &DbPool,
activity_id: &Url,
) -> Result<bool, LemmyError> {
let activity_id = activity_id.to_string();
let activity_id = activity_id.to_owned().into();
let existing = blocking(pool, move |conn| {
Activity::read_from_apub_id(&conn, &activity_id)
})
@ -114,14 +119,17 @@ where
}
/// Returns true if `to_and_cc` contains at least one local user.
pub(crate) async fn is_addressed_to_local_user(
pub(crate) async fn is_addressed_to_local_person(
to_and_cc: &[Url],
pool: &DbPool,
) -> Result<bool, LemmyError> {
for url in to_and_cc {
let url = url.to_string();
let user = blocking(&pool, move |conn| User_::read_from_apub_id(&conn, &url)).await?;
if let Ok(u) = user {
let url = url.to_owned();
let person = blocking(&pool, move |conn| {
Person::read_from_apub_id(&conn, &url.into())
})
.await?;
if let Ok(u) = person {
if u.local {
return Ok(true);
}
@ -137,16 +145,15 @@ pub(crate) async fn is_addressed_to_community_followers(
pool: &DbPool,
) -> Result<Option<Community>, LemmyError> {
for url in to_and_cc {
let url = url.to_string();
// TODO: extremely hacky, we should just store the followers url for each community in the db
if url.ends_with("/followers") {
let community_url = url.replace("/followers", "");
let url = url.to_owned().into();
let community = blocking(&pool, move |conn| {
Community::read_from_apub_id(&conn, &community_url)
// ignore errors here, because the current url might not actually be a followers url
Community::read_from_followers_url(&conn, &url).ok()
})
.await??;
if !community.local {
return Ok(Some(community));
.await?;
if let Some(c) = community {
if !c.local {
return Ok(Some(c));
}
}
}
@ -160,7 +167,7 @@ where
let id = activity.id_unchecked().context(location_info!())?;
let activity_domain = id.domain().context(location_info!())?;
if activity_domain == Settings::get().hostname {
if activity_domain == Settings::get().hostname() {
return Err(
anyhow!(
"Error: received activity which was sent by local instance: {:?}",

View file

@ -25,7 +25,7 @@ use crate::{
inbox_verify_http_signature,
is_activity_already_known,
is_addressed_to_community_followers,
is_addressed_to_local_user,
is_addressed_to_local_person,
is_addressed_to_public,
receive_for_community::{
receive_create_for_community,
@ -48,24 +48,25 @@ use activitystreams::{
use actix_web::{web, HttpRequest, HttpResponse};
use anyhow::{anyhow, Context};
use diesel::NotFound;
use lemmy_db_queries::{source::user::User, ApubObject, Followable};
use lemmy_api_structs::blocking;
use lemmy_db_queries::{source::person::Person_, ApubObject, Followable};
use lemmy_db_schema::source::{
community::{Community, CommunityFollower},
person::Person,
private_message::PrivateMessage,
user::User_,
};
use lemmy_structs::blocking;
use lemmy_utils::{location_info, LemmyError};
use lemmy_websocket::LemmyContext;
use log::debug;
use serde::{Deserialize, Serialize};
use std::fmt::Debug;
use strum_macros::EnumString;
use url::Url;
/// Allowed activities for user inbox.
/// Allowed activities for person inbox.
#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd, Deserialize, Serialize)]
#[serde(rename_all = "PascalCase")]
pub enum UserValidTypes {
pub enum PersonValidTypes {
Accept, // community accepted our follow request
Create, // create private message
Update, // edit private message
@ -75,12 +76,12 @@ pub enum UserValidTypes {
Announce, // post, comment or vote in community
}
pub type UserAcceptedActivities = ActorAndObject<UserValidTypes>;
pub type PersonAcceptedActivities = ActorAndObject<PersonValidTypes>;
/// Handler for all incoming activities to user inboxes.
pub async fn user_inbox(
/// Handler for all incoming activities to person inboxes.
pub async fn person_inbox(
request: HttpRequest,
input: web::Json<UserAcceptedActivities>,
input: web::Json<PersonAcceptedActivities>,
path: web::Path<String>,
context: web::Data<LemmyContext>,
) -> Result<HttpResponse, LemmyError> {
@ -90,36 +91,36 @@ pub async fn user_inbox(
let actor = inbox_verify_http_signature(&activity, &context, request, request_counter).await?;
// Do nothing if we received the same activity before
let activity_id = get_activity_id(&activity, &actor.actor_id()?)?;
let activity_id = get_activity_id(&activity, &actor.actor_id())?;
if is_activity_already_known(context.pool(), &activity_id).await? {
return Ok(HttpResponse::Ok().finish());
}
// Check if the activity is actually meant for us
let username = path.into_inner();
let user = blocking(&context.pool(), move |conn| {
User_::read_from_name(&conn, &username)
let person = blocking(&context.pool(), move |conn| {
Person::find_by_name(&conn, &username)
})
.await??;
let to_and_cc = get_activity_to_and_cc(&activity);
// TODO: we should also accept activities that are sent to community followers
if !to_and_cc.contains(&&user.actor_id()?) {
return Err(anyhow!("Activity delivered to wrong user").into());
if !to_and_cc.contains(&&person.actor_id()) {
return Err(anyhow!("Activity delivered to wrong person").into());
}
assert_activity_not_local(&activity)?;
insert_activity(&activity_id, activity.clone(), false, true, context.pool()).await?;
debug!(
"User {} received activity {:?} from {}",
user.name,
"Person {} received activity {:?} from {}",
person.name,
&activity.id_unchecked(),
&actor.actor_id_str()
&actor.actor_id()
);
user_receive_message(
person_receive_message(
activity.clone(),
Some(user.clone()),
Some(person.clone()),
actor.as_ref(),
&context,
request_counter,
@ -128,36 +129,43 @@ pub async fn user_inbox(
}
/// Receives Accept/Follow, Announce, private messages and community (undo) remove, (undo) delete
pub(crate) async fn user_receive_message(
activity: UserAcceptedActivities,
to_user: Option<User_>,
pub(crate) async fn person_receive_message(
activity: PersonAcceptedActivities,
to_person: Option<Person>,
actor: &dyn ActorType,
context: &LemmyContext,
request_counter: &mut i32,
) -> Result<HttpResponse, LemmyError> {
is_for_user_inbox(context, &activity).await?;
is_for_person_inbox(context, &activity).await?;
let any_base = activity.clone().into_any_base()?;
let kind = activity.kind().context(location_info!())?;
let actor_url = actor.actor_id()?;
let actor_url = actor.actor_id();
match kind {
UserValidTypes::Accept => {
receive_accept(&context, any_base, actor, to_user.unwrap(), request_counter).await?;
PersonValidTypes::Accept => {
receive_accept(
&context,
any_base,
actor,
to_person.expect("person provided"),
request_counter,
)
.await?;
}
UserValidTypes::Announce => {
PersonValidTypes::Announce => {
receive_announce(&context, any_base, actor, request_counter).await?
}
UserValidTypes::Create => {
PersonValidTypes::Create => {
receive_create(&context, any_base, actor_url, request_counter).await?
}
UserValidTypes::Update => {
PersonValidTypes::Update => {
receive_update(&context, any_base, actor_url, request_counter).await?
}
UserValidTypes::Delete => {
PersonValidTypes::Delete => {
receive_delete(context, any_base, &actor_url, request_counter).await?
}
UserValidTypes::Undo => receive_undo(context, any_base, &actor_url, request_counter).await?,
UserValidTypes::Remove => receive_remove_community(&context, any_base, &actor_url).await?,
PersonValidTypes::Undo => receive_undo(context, any_base, &actor_url, request_counter).await?,
PersonValidTypes::Remove => receive_remove_community(&context, any_base, &actor_url).await?,
};
// TODO: would be logical to move websocket notification code here
@ -165,16 +173,16 @@ pub(crate) async fn user_receive_message(
Ok(HttpResponse::Ok().finish())
}
/// Returns true if the activity is addressed directly to one or more local users, or if it is
/// addressed to the followers collection of a remote community, and at least one local user follows
/// Returns true if the activity is addressed directly to one or more local persons, or if it is
/// addressed to the followers collection of a remote community, and at least one local person follows
/// it.
async fn is_for_user_inbox(
async fn is_for_person_inbox(
context: &LemmyContext,
activity: &UserAcceptedActivities,
activity: &PersonAcceptedActivities,
) -> Result<(), LemmyError> {
let to_and_cc = get_activity_to_and_cc(activity);
// Check if it is addressed directly to any local user
if is_addressed_to_local_user(&to_and_cc, context.pool()).await? {
// Check if it is addressed directly to any local person
if is_addressed_to_local_person(&to_and_cc, context.pool()).await? {
return Ok(());
}
@ -197,7 +205,7 @@ async fn is_for_user_inbox(
}
}
Err(anyhow!("Not addressed for any local user").into())
Err(anyhow!("Not addressed for any local person").into())
}
/// Handle accepted follows.
@ -205,15 +213,15 @@ async fn receive_accept(
context: &LemmyContext,
activity: AnyBase,
actor: &dyn ActorType,
user: User_,
person: Person,
request_counter: &mut i32,
) -> Result<(), LemmyError> {
let accept = Accept::from_any_base(activity)?.context(location_info!())?;
verify_activity_domains_valid(&accept, &actor.actor_id()?, false)?;
verify_activity_domains_valid(&accept, &actor.actor_id(), false)?;
let object = accept.object().to_owned().one().context(location_info!())?;
let follow = Follow::from_any_base(object)?.context(location_info!())?;
verify_activity_domains_valid(&follow, &user.actor_id()?, false)?;
verify_activity_domains_valid(&follow, &person.actor_id(), false)?;
let community_uri = accept
.actor()?
@ -225,28 +233,42 @@ async fn receive_accept(
get_or_fetch_and_upsert_community(&community_uri, context, request_counter).await?;
let community_id = community.id;
let user_id = user.id;
let person_id = person.id;
// This will throw an error if no follow was requested
blocking(&context.pool(), move |conn| {
CommunityFollower::follow_accepted(conn, community_id, user_id)
CommunityFollower::follow_accepted(conn, community_id, person_id)
})
.await??;
Ok(())
}
#[derive(EnumString)]
enum AnnouncableActivities {
Create,
Update,
Like,
Dislike,
Delete,
Remove,
Undo,
}
/// Takes an announce and passes the inner activity to the appropriate handler.
async fn receive_announce(
pub async fn receive_announce(
context: &LemmyContext,
activity: AnyBase,
actor: &dyn ActorType,
request_counter: &mut i32,
) -> Result<(), LemmyError> {
let announce = Announce::from_any_base(activity)?.context(location_info!())?;
verify_activity_domains_valid(&announce, &actor.actor_id()?, false)?;
verify_activity_domains_valid(&announce, &actor.actor_id(), false)?;
is_addressed_to_public(&announce)?;
let kind = announce.object().as_single_kind_str();
let kind = announce
.object()
.as_single_kind_str()
.and_then(|s| s.parse().ok());
let inner_activity = announce
.object()
.to_owned()
@ -259,22 +281,23 @@ async fn receive_announce(
return Ok(());
}
use AnnouncableActivities::*;
match kind {
Some("Create") => {
Some(Create) => {
receive_create_for_community(context, inner_activity, &inner_id, request_counter).await
}
Some("Update") => {
Some(Update) => {
receive_update_for_community(context, inner_activity, &inner_id, request_counter).await
}
Some("Like") => {
Some(Like) => {
receive_like_for_community(context, inner_activity, &inner_id, request_counter).await
}
Some("Dislike") => {
Some(Dislike) => {
receive_dislike_for_community(context, inner_activity, &inner_id, request_counter).await
}
Some("Delete") => receive_delete_for_community(context, inner_activity, &inner_id).await,
Some("Remove") => receive_remove_for_community(context, inner_activity, &inner_id).await,
Some("Undo") => {
Some(Delete) => receive_delete_for_community(context, inner_activity, &inner_id).await,
Some(Remove) => receive_remove_for_community(context, inner_activity, &inner_id).await,
Some(Undo) => {
receive_undo_for_community(context, inner_activity, &inner_id, request_counter).await
}
_ => receive_unhandled_activity(inner_activity),
@ -375,18 +398,18 @@ async fn find_community_or_private_message_by_id(
context: &LemmyContext,
apub_id: Url,
) -> Result<CommunityOrPrivateMessage, LemmyError> {
let ap_id = apub_id.to_string();
let ap_id = apub_id.to_owned();
let community = blocking(context.pool(), move |conn| {
Community::read_from_apub_id(conn, &ap_id)
Community::read_from_apub_id(conn, &ap_id.into())
})
.await?;
if let Ok(c) = community {
return Ok(CommunityOrPrivateMessage::Community(c));
}
let ap_id = apub_id.to_string();
let ap_id = apub_id.to_owned();
let private_message = blocking(context.pool(), move |conn| {
PrivateMessage::read_from_apub_id(conn, &ap_id)
PrivateMessage::read_from_apub_id(conn, &ap_id.into())
})
.await?;
if let Ok(p) = private_message {

View file

@ -43,15 +43,22 @@ use activitystreams::{
};
use anyhow::Context;
use diesel::result::Error::NotFound;
use lemmy_api_structs::blocking;
use lemmy_db_queries::Crud;
use lemmy_db_schema::source::site::Site;
use lemmy_structs::blocking;
use lemmy_utils::{location_info, LemmyError};
use lemmy_websocket::LemmyContext;
use strum_macros::EnumString;
use url::Url;
#[derive(EnumString)]
enum PageOrNote {
Page,
Note,
}
/// This file is for post/comment activities received by the community, and for post/comment
/// activities announced by the community and received by the user.
/// activities announced by the community and received by the person.
/// A post or comment being created
pub(in crate::inbox) async fn receive_create_for_community(
@ -64,9 +71,13 @@ pub(in crate::inbox) async fn receive_create_for_community(
verify_activity_domains_valid(&create, &expected_domain, true)?;
is_addressed_to_public(&create)?;
match create.object().as_single_kind_str() {
Some("Page") => receive_create_post(create, context, request_counter).await,
Some("Note") => receive_create_comment(create, context, request_counter).await,
let kind = create
.object()
.as_single_kind_str()
.and_then(|s| s.parse().ok());
match kind {
Some(PageOrNote::Page) => receive_create_post(create, context, request_counter).await,
Some(PageOrNote::Note) => receive_create_comment(create, context, request_counter).await,
_ => receive_unhandled_activity(create),
}
}
@ -82,9 +93,13 @@ pub(in crate::inbox) async fn receive_update_for_community(
verify_activity_domains_valid(&update, &expected_domain, true)?;
is_addressed_to_public(&update)?;
match update.object().as_single_kind_str() {
Some("Page") => receive_update_post(update, context, request_counter).await,
Some("Note") => receive_update_comment(update, context, request_counter).await,
let kind = update
.object()
.as_single_kind_str()
.and_then(|s| s.parse().ok());
match kind {
Some(PageOrNote::Page) => receive_update_post(update, context, request_counter).await,
Some(PageOrNote::Note) => receive_update_comment(update, context, request_counter).await,
_ => receive_unhandled_activity(update),
}
}
@ -100,11 +115,14 @@ pub(in crate::inbox) async fn receive_like_for_community(
verify_activity_domains_valid(&like, &expected_domain, false)?;
is_addressed_to_public(&like)?;
let object_id = get_like_object_id(&like)?;
let object_id = like
.object()
.as_single_xsd_any_uri()
.context(location_info!())?;
match fetch_post_or_comment_by_id(&object_id, context, request_counter).await? {
PostOrComment::Post(post) => receive_like_post(like, post, context, request_counter).await,
PostOrComment::Post(post) => receive_like_post(like, *post, context, request_counter).await,
PostOrComment::Comment(comment) => {
receive_like_comment(like, comment, context, request_counter).await
receive_like_comment(like, *comment, context, request_counter).await
}
}
}
@ -128,13 +146,16 @@ pub(in crate::inbox) async fn receive_dislike_for_community(
verify_activity_domains_valid(&dislike, &expected_domain, false)?;
is_addressed_to_public(&dislike)?;
let object_id = get_like_object_id(&dislike)?;
let object_id = dislike
.object()
.as_single_xsd_any_uri()
.context(location_info!())?;
match fetch_post_or_comment_by_id(&object_id, context, request_counter).await? {
PostOrComment::Post(post) => {
receive_dislike_post(dislike, post, context, request_counter).await
receive_dislike_post(dislike, *post, context, request_counter).await
}
PostOrComment::Comment(comment) => {
receive_dislike_comment(dislike, comment, context, request_counter).await
receive_dislike_comment(dislike, *comment, context, request_counter).await
}
}
}
@ -156,8 +177,8 @@ pub(in crate::inbox) async fn receive_delete_for_community(
.context(location_info!())?;
match find_post_or_comment_by_id(context, object).await {
Ok(PostOrComment::Post(p)) => receive_delete_post(context, p).await,
Ok(PostOrComment::Comment(c)) => receive_delete_comment(context, c).await,
Ok(PostOrComment::Post(p)) => receive_delete_post(context, *p).await,
Ok(PostOrComment::Comment(c)) => receive_delete_comment(context, *c).await,
// if we dont have the object, no need to do anything
Err(_) => Ok(()),
}
@ -194,13 +215,21 @@ pub(in crate::inbox) async fn receive_remove_for_community(
remove.id(community_id.domain().context(location_info!())?)?;
match find_post_or_comment_by_id(context, object).await {
Ok(PostOrComment::Post(p)) => receive_remove_post(context, remove, p).await,
Ok(PostOrComment::Comment(c)) => receive_remove_comment(context, remove, c).await,
Ok(PostOrComment::Post(p)) => receive_remove_post(context, remove, *p).await,
Ok(PostOrComment::Comment(c)) => receive_remove_comment(context, remove, *c).await,
// if we dont have the object, no need to do anything
Err(_) => Ok(()),
}
}
#[derive(EnumString)]
enum UndoableActivities {
Delete,
Remove,
Like,
Dislike,
}
/// A post/comment action being reverted (either a delete, remove, upvote or downvote)
pub(in crate::inbox) async fn receive_undo_for_community(
context: &LemmyContext,
@ -212,13 +241,18 @@ pub(in crate::inbox) async fn receive_undo_for_community(
verify_activity_domains_valid(&undo, &expected_domain.to_owned(), true)?;
is_addressed_to_public(&undo)?;
match undo.object().as_single_kind_str() {
Some("Delete") => receive_undo_delete_for_community(context, undo, expected_domain).await,
Some("Remove") => receive_undo_remove_for_community(context, undo, expected_domain).await,
Some("Like") => {
use UndoableActivities::*;
match undo
.object()
.as_single_kind_str()
.and_then(|s| s.parse().ok())
{
Some(Delete) => receive_undo_delete_for_community(context, undo, expected_domain).await,
Some(Remove) => receive_undo_remove_for_community(context, undo, expected_domain).await,
Some(Like) => {
receive_undo_like_for_community(context, undo, expected_domain, request_counter).await
}
Some("Dislike") => {
Some(Dislike) => {
receive_undo_dislike_for_community(context, undo, expected_domain, request_counter).await
}
_ => receive_unhandled_activity(undo),
@ -242,8 +276,8 @@ pub(in crate::inbox) async fn receive_undo_delete_for_community(
.single_xsd_any_uri()
.context(location_info!())?;
match find_post_or_comment_by_id(context, object).await {
Ok(PostOrComment::Post(p)) => receive_undo_delete_post(context, p).await,
Ok(PostOrComment::Comment(c)) => receive_undo_delete_comment(context, c).await,
Ok(PostOrComment::Post(p)) => receive_undo_delete_post(context, *p).await,
Ok(PostOrComment::Comment(c)) => receive_undo_delete_comment(context, *c).await,
// if we dont have the object, no need to do anything
Err(_) => Ok(()),
}
@ -266,8 +300,8 @@ pub(in crate::inbox) async fn receive_undo_remove_for_community(
.single_xsd_any_uri()
.context(location_info!())?;
match find_post_or_comment_by_id(context, object).await {
Ok(PostOrComment::Post(p)) => receive_undo_remove_post(context, p).await,
Ok(PostOrComment::Comment(c)) => receive_undo_remove_comment(context, c).await,
Ok(PostOrComment::Post(p)) => receive_undo_remove_post(context, *p).await,
Ok(PostOrComment::Comment(c)) => receive_undo_remove_comment(context, *c).await,
// if we dont have the object, no need to do anything
Err(_) => Ok(()),
}
@ -285,13 +319,16 @@ pub(in crate::inbox) async fn receive_undo_like_for_community(
verify_activity_domains_valid(&like, &expected_domain, false)?;
is_addressed_to_public(&like)?;
let object_id = get_like_object_id(&like)?;
let object_id = like
.object()
.as_single_xsd_any_uri()
.context(location_info!())?;
match fetch_post_or_comment_by_id(&object_id, context, request_counter).await? {
PostOrComment::Post(post) => {
receive_undo_like_post(&like, post, context, request_counter).await
receive_undo_like_post(&like, *post, context, request_counter).await
}
PostOrComment::Comment(comment) => {
receive_undo_like_comment(&like, comment, context, request_counter).await
receive_undo_like_comment(&like, *comment, context, request_counter).await
}
}
}
@ -308,13 +345,16 @@ pub(in crate::inbox) async fn receive_undo_dislike_for_community(
verify_activity_domains_valid(&dislike, &expected_domain, false)?;
is_addressed_to_public(&dislike)?;
let object_id = get_like_object_id(&dislike)?;
let object_id = dislike
.object()
.as_single_xsd_any_uri()
.context(location_info!())?;
match fetch_post_or_comment_by_id(&object_id, context, request_counter).await? {
PostOrComment::Post(post) => {
receive_undo_dislike_post(&dislike, post, context, request_counter).await
receive_undo_dislike_post(&dislike, *post, context, request_counter).await
}
PostOrComment::Comment(comment) => {
receive_undo_dislike_comment(&dislike, comment, context, request_counter).await
receive_undo_dislike_comment(&dislike, *comment, context, request_counter).await
}
}
}
@ -325,35 +365,12 @@ async fn fetch_post_or_comment_by_id(
request_counter: &mut i32,
) -> Result<PostOrComment, LemmyError> {
if let Ok(post) = get_or_fetch_and_insert_post(apub_id, context, request_counter).await {
return Ok(PostOrComment::Post(post));
return Ok(PostOrComment::Post(Box::new(post)));
}
if let Ok(comment) = get_or_fetch_and_insert_comment(apub_id, context, request_counter).await {
return Ok(PostOrComment::Comment(comment));
return Ok(PostOrComment::Comment(Box::new(comment)));
}
Err(NotFound.into())
}
fn get_like_object_id<Activity>(like_or_dislike: &Activity) -> Result<Url, LemmyError>
where
Activity: ActorAndObjectRefExt,
{
// TODO: For backwards compatibility with older Lemmy versions where like.object contains a full
// post/comment. This can be removed after some time, using
// `activity.oject().as_single_xsd_any_uri()` instead.
let object = like_or_dislike.object();
if let Some(xsd_uri) = object.as_single_xsd_any_uri() {
Ok(xsd_uri.to_owned())
} else {
Ok(
object
.to_owned()
.one()
.context(location_info!())?
.id()
.context(location_info!())?
.to_owned(),
)
}
}

View file

@ -7,17 +7,17 @@ use crate::{
inbox_verify_http_signature,
is_activity_already_known,
is_addressed_to_community_followers,
is_addressed_to_local_user,
user_inbox::{user_receive_message, UserAcceptedActivities},
is_addressed_to_local_person,
person_inbox::{person_receive_message, PersonAcceptedActivities},
},
insert_activity,
};
use activitystreams::{activity::ActorAndObject, prelude::*};
use actix_web::{web, HttpRequest, HttpResponse};
use anyhow::Context;
use lemmy_api_structs::blocking;
use lemmy_db_queries::{ApubObject, DbPool};
use lemmy_db_schema::source::community::Community;
use lemmy_structs::blocking;
use lemmy_utils::{location_info, LemmyError};
use lemmy_websocket::LemmyContext;
use serde::{Deserialize, Serialize};
@ -54,7 +54,7 @@ pub async fn shared_inbox(
let actor = inbox_verify_http_signature(&activity, &context, request, request_counter).await?;
// Do nothing if we received the same activity before
let actor_id = actor.actor_id()?;
let actor_id = actor.actor_id();
let activity_id = get_activity_id(&activity, &actor_id)?;
if is_activity_already_known(context.pool(), &activity_id).await? {
return Ok(HttpResponse::Ok().finish());
@ -69,9 +69,9 @@ pub async fn shared_inbox(
let mut res: Option<HttpResponse> = None;
let to_and_cc = get_activity_to_and_cc(&activity);
// Handle community first, so in case the sender is banned by the community, it will error out.
// If we handled the user receive first, the activity would be inserted to the database before the
// If we handled the person receive first, the activity would be inserted to the database before the
// community could check for bans.
// Note that an activity can be addressed to a community and to a user (or multiple users) at the
// Note that an activity can be addressed to a community and to a person (or multiple persons) at the
// same time. In this case we still only handle it once, to avoid duplicate websocket
// notifications.
let community = extract_local_community_from_destinations(&to_and_cc, context.pool()).await?;
@ -88,13 +88,13 @@ pub async fn shared_inbox(
)
.await?,
);
} else if is_addressed_to_local_user(&to_and_cc, context.pool()).await? {
let user_activity = UserAcceptedActivities::from_any_base(activity_any_base.clone())?
} else if is_addressed_to_local_person(&to_and_cc, context.pool()).await? {
let person_activity = PersonAcceptedActivities::from_any_base(activity_any_base.clone())?
.context(location_info!())?;
// `to_user` is only used for follow activities (which we dont receive here), so no need to pass
// `to_person` is only used for follow activities (which we dont receive here), so no need to pass
// it in
user_receive_message(
user_activity,
person_receive_message(
person_activity,
None,
actor.as_ref(),
&context,
@ -105,11 +105,11 @@ pub async fn shared_inbox(
.await?
.is_some()
{
let user_activity = UserAcceptedActivities::from_any_base(activity_any_base.clone())?
let person_activity = PersonAcceptedActivities::from_any_base(activity_any_base.clone())?
.context(location_info!())?;
res = Some(
user_receive_message(
user_activity,
person_receive_message(
person_activity,
None,
actor.as_ref(),
&context,
@ -137,8 +137,11 @@ async fn extract_local_community_from_destinations(
pool: &DbPool,
) -> Result<Option<Community>, LemmyError> {
for url in to_and_cc {
let url = url.to_string();
let community = blocking(&pool, move |conn| Community::read_from_apub_id(&conn, &url)).await?;
let url = url.to_owned();
let community = blocking(&pool, move |conn| {
Community::read_from_apub_id(&conn, &url.into())
})
.await?;
if let Ok(c) = community {
if c.local {
return Ok(Some(c));

View file

@ -8,6 +8,7 @@ pub mod fetcher;
pub mod http;
pub mod inbox;
pub mod objects;
pub mod routes;
use crate::extensions::{
group_extensions::GroupExtension,
@ -23,17 +24,20 @@ use activitystreams::{
use activitystreams_ext::{Ext1, Ext2};
use anyhow::{anyhow, Context};
use diesel::NotFound;
use lemmy_api_structs::blocking;
use lemmy_db_queries::{source::activity::Activity_, ApubObject, DbPool};
use lemmy_db_schema::source::{
use lemmy_db_schema::{
source::{
activity::Activity,
comment::Comment,
community::Community,
person::Person as DbPerson,
post::Post,
private_message::PrivateMessage,
user::User_,
},
DbUrl,
};
use lemmy_structs::blocking;
use lemmy_utils::{location_info, settings::Settings, LemmyError};
use lemmy_utils::{location_info, settings::structs::Settings, LemmyError};
use lemmy_websocket::LemmyContext;
use serde::Serialize;
use std::net::IpAddr;
@ -41,7 +45,7 @@ use url::{ParseError, Url};
/// Activitystreams type for community
type GroupExt = Ext2<ApActor<ApObject<Group>>, GroupExtension, PublicKeyExtension>;
/// Activitystreams type for user
/// Activitystreams type for person
type PersonExt = Ext1<ApActor<ApObject<Person>>, PublicKeyExtension>;
/// Activitystreams type for post
type PageExt = Ext1<ApObject<Page>, PageExtension>;
@ -61,15 +65,9 @@ pub static APUB_JSON_CONTENT_TYPE: &str = "application/activity+json";
fn check_is_apub_id_valid(apub_id: &Url) -> Result<(), LemmyError> {
let settings = Settings::get();
let domain = apub_id.domain().context(location_info!())?.to_string();
let local_instance = settings
.hostname
.split(':')
.collect::<Vec<&str>>()
.first()
.context(location_info!())?
.to_string();
let local_instance = settings.get_hostname_without_port()?;
if !settings.federation.enabled {
if !settings.federation().enabled {
return if domain == local_instance {
Ok(())
} else {
@ -86,29 +84,30 @@ fn check_is_apub_id_valid(apub_id: &Url) -> Result<(), LemmyError> {
let host = apub_id.host_str().context(location_info!())?;
let host_as_ip = host.parse::<IpAddr>();
if host == "localhost" || host_as_ip.is_ok() {
return Err(anyhow!("invalid hostname: {:?}", host).into());
return Err(anyhow!("invalid hostname {}: {}", host, apub_id).into());
}
if apub_id.scheme() != Settings::get().get_protocol_string() {
return Err(anyhow!("invalid apub id scheme: {:?}", apub_id.scheme()).into());
return Err(anyhow!("invalid apub id scheme {}: {}", apub_id.scheme(), apub_id).into());
}
let mut allowed_instances = Settings::get().get_allowed_instances();
let allowed_instances = Settings::get().get_allowed_instances();
let blocked_instances = Settings::get().get_blocked_instances();
if allowed_instances.is_empty() && blocked_instances.is_empty() {
if allowed_instances.is_none() && blocked_instances.is_none() {
Ok(())
} else if !allowed_instances.is_empty() {
} else if let Some(mut allowed) = allowed_instances {
// need to allow this explicitly because apub receive might contain objects from our local
// instance. split is needed to remove the port in our federation test setup.
allowed_instances.push(local_instance);
allowed.push(local_instance);
if allowed_instances.contains(&domain) {
if allowed.contains(&domain) {
Ok(())
} else {
Err(anyhow!("{} not in federation allowlist", domain).into())
}
} else if !blocked_instances.is_empty() {
if blocked_instances.contains(&domain) {
} else if let Some(blocked) = blocked_instances {
if blocked.contains(&domain) {
Err(anyhow!("{} is in federation blocklist", domain).into())
} else {
Ok(())
@ -122,31 +121,46 @@ fn check_is_apub_id_valid(apub_id: &Url) -> Result<(), LemmyError> {
/// and actors in Lemmy.
#[async_trait::async_trait(?Send)]
pub trait ApubObjectType {
async fn send_create(&self, creator: &User_, context: &LemmyContext) -> Result<(), LemmyError>;
async fn send_update(&self, creator: &User_, context: &LemmyContext) -> Result<(), LemmyError>;
async fn send_delete(&self, creator: &User_, context: &LemmyContext) -> Result<(), LemmyError>;
async fn send_create(&self, creator: &DbPerson, context: &LemmyContext)
-> Result<(), LemmyError>;
async fn send_update(&self, creator: &DbPerson, context: &LemmyContext)
-> Result<(), LemmyError>;
async fn send_delete(&self, creator: &DbPerson, context: &LemmyContext)
-> Result<(), LemmyError>;
async fn send_undo_delete(
&self,
creator: &User_,
creator: &DbPerson,
context: &LemmyContext,
) -> Result<(), LemmyError>;
async fn send_remove(&self, mod_: &DbPerson, context: &LemmyContext) -> Result<(), LemmyError>;
async fn send_undo_remove(
&self,
mod_: &DbPerson,
context: &LemmyContext,
) -> Result<(), LemmyError>;
async fn send_remove(&self, mod_: &User_, context: &LemmyContext) -> Result<(), LemmyError>;
async fn send_undo_remove(&self, mod_: &User_, context: &LemmyContext) -> Result<(), LemmyError>;
}
#[async_trait::async_trait(?Send)]
pub trait ApubLikeableType {
async fn send_like(&self, creator: &User_, context: &LemmyContext) -> Result<(), LemmyError>;
async fn send_dislike(&self, creator: &User_, context: &LemmyContext) -> Result<(), LemmyError>;
async fn send_undo_like(&self, creator: &User_, context: &LemmyContext)
-> Result<(), LemmyError>;
async fn send_like(&self, creator: &DbPerson, context: &LemmyContext) -> Result<(), LemmyError>;
async fn send_dislike(
&self,
creator: &DbPerson,
context: &LemmyContext,
) -> Result<(), LemmyError>;
async fn send_undo_like(
&self,
creator: &DbPerson,
context: &LemmyContext,
) -> Result<(), LemmyError>;
}
/// Common methods provided by ActivityPub actors (community and user). Not all methods are
/// Common methods provided by ActivityPub actors (community and person). Not all methods are
/// implemented by all actors.
#[async_trait::async_trait(?Send)]
pub trait ActorType {
fn actor_id_str(&self) -> String;
fn is_local(&self) -> bool;
fn actor_id(&self) -> Url;
// TODO: every actor should have a public key, so this shouldnt be an option (needs to be fixed in db)
fn public_key(&self) -> Option<String>;
@ -184,17 +198,71 @@ pub trait ActorType {
/// For a given community, returns the inboxes of all followers.
async fn get_follower_inboxes(&self, pool: &DbPool) -> Result<Vec<Url>, LemmyError>;
fn actor_id(&self) -> Result<Url, ParseError> {
Url::parse(&self.actor_id_str())
fn get_shared_inbox_or_inbox_url(&self) -> Url;
/// Outbox URL is not generally used by Lemmy, so it can be generated on the fly (but only for
/// local actors).
fn get_outbox_url(&self) -> Result<Url, LemmyError> {
if !self.is_local() {
return Err(anyhow!("get_outbox_url() called for remote actor").into());
}
Ok(Url::parse(&format!("{}/outbox", &self.actor_id()))?)
}
// TODO move these to the db rows
fn get_inbox_url(&self) -> Result<Url, ParseError> {
Url::parse(&format!("{}/inbox", &self.actor_id_str()))
fn get_public_key_ext(&self) -> Result<PublicKeyExtension, LemmyError> {
Ok(
PublicKey {
id: format!("{}#main-key", self.actor_id()),
owner: self.actor_id(),
public_key_pem: self.public_key().context(location_info!())?,
}
.to_ext(),
)
}
}
fn get_shared_inbox_url(&self) -> Result<Url, LemmyError> {
let actor_id = self.actor_id()?;
pub enum EndpointType {
Community,
Person,
Post,
Comment,
PrivateMessage,
}
/// Generates the ActivityPub ID for a given object type and ID.
pub fn generate_apub_endpoint(
endpoint_type: EndpointType,
name: &str,
) -> Result<DbUrl, ParseError> {
let point = match endpoint_type {
EndpointType::Community => "c",
EndpointType::Person => "u",
EndpointType::Post => "post",
EndpointType::Comment => "comment",
EndpointType::PrivateMessage => "private_message",
};
Ok(
Url::parse(&format!(
"{}/{}/{}",
Settings::get().get_protocol_and_hostname(),
point,
name
))?
.into(),
)
}
pub fn generate_followers_url(actor_id: &DbUrl) -> Result<DbUrl, ParseError> {
Ok(Url::parse(&format!("{}/followers", actor_id))?.into())
}
pub fn generate_inbox_url(actor_id: &DbUrl) -> Result<DbUrl, ParseError> {
Ok(Url::parse(&format!("{}/inbox", actor_id))?.into())
}
pub fn generate_shared_inbox_url(actor_id: &DbUrl) -> Result<DbUrl, LemmyError> {
let actor_id = actor_id.clone().into_inner();
let url = format!(
"{}://{}{}/inbox",
&actor_id.scheme(),
@ -205,27 +273,7 @@ pub trait ActorType {
"".to_string()
},
);
Ok(Url::parse(&url)?)
}
fn get_outbox_url(&self) -> Result<Url, ParseError> {
Url::parse(&format!("{}/outbox", &self.actor_id_str()))
}
fn get_followers_url(&self) -> Result<Url, ParseError> {
Url::parse(&format!("{}/followers", &self.actor_id_str()))
}
fn get_public_key_ext(&self) -> Result<PublicKeyExtension, LemmyError> {
Ok(
PublicKey {
id: format!("{}#main-key", self.actor_id_str()),
owner: self.actor_id_str(),
public_key_pem: self.public_key().context(location_info!())?,
}
.to_ext(),
)
}
Ok(Url::parse(&url)?.into())
}
/// Store a sent or received activity in the database, for logging purposes. These records are not
@ -240,7 +288,7 @@ pub(crate) async fn insert_activity<T>(
where
T: Serialize + std::fmt::Debug + Send + 'static,
{
let ap_id = ap_id.to_string();
let ap_id = ap_id.to_owned().into();
blocking(pool, move |conn| {
Activity::insert(conn, ap_id, &activity, local, sensitive)
})
@ -249,8 +297,8 @@ where
}
pub(crate) enum PostOrComment {
Comment(Comment),
Post(Post),
Comment(Box<Comment>),
Post(Box<Post>),
}
/// Tries to find a post or comment in the local database, without any network requests.
@ -260,71 +308,71 @@ pub(crate) async fn find_post_or_comment_by_id(
context: &LemmyContext,
apub_id: Url,
) -> Result<PostOrComment, LemmyError> {
let ap_id = apub_id.to_string();
let ap_id = apub_id.clone();
let post = blocking(context.pool(), move |conn| {
Post::read_from_apub_id(conn, &ap_id)
Post::read_from_apub_id(conn, &ap_id.into())
})
.await?;
if let Ok(p) = post {
return Ok(PostOrComment::Post(p));
return Ok(PostOrComment::Post(Box::new(p)));
}
let ap_id = apub_id.to_string();
let ap_id = apub_id.clone();
let comment = blocking(context.pool(), move |conn| {
Comment::read_from_apub_id(conn, &ap_id)
Comment::read_from_apub_id(conn, &ap_id.into())
})
.await?;
if let Ok(c) = comment {
return Ok(PostOrComment::Comment(c));
return Ok(PostOrComment::Comment(Box::new(c)));
}
Err(NotFound.into())
}
pub(crate) enum Object {
Comment(Comment),
Post(Post),
Community(Community),
User(User_),
PrivateMessage(PrivateMessage),
Comment(Box<Comment>),
Post(Box<Post>),
Community(Box<Community>),
Person(Box<DbPerson>),
PrivateMessage(Box<PrivateMessage>),
}
pub(crate) async fn find_object_by_id(
context: &LemmyContext,
apub_id: Url,
) -> Result<Object, LemmyError> {
if let Ok(pc) = find_post_or_comment_by_id(context, apub_id.to_owned()).await {
let ap_id = apub_id.clone();
if let Ok(pc) = find_post_or_comment_by_id(context, ap_id.to_owned()).await {
return Ok(match pc {
PostOrComment::Post(p) => Object::Post(p),
PostOrComment::Comment(c) => Object::Comment(c),
PostOrComment::Post(p) => Object::Post(Box::new(*p)),
PostOrComment::Comment(c) => Object::Comment(Box::new(*c)),
});
}
let ap_id = apub_id.to_string();
let user = blocking(context.pool(), move |conn| {
User_::read_from_apub_id(conn, &ap_id)
let ap_id = apub_id.clone();
let person = blocking(context.pool(), move |conn| {
DbPerson::read_from_apub_id(conn, &ap_id.into())
})
.await?;
if let Ok(u) = user {
return Ok(Object::User(u));
if let Ok(u) = person {
return Ok(Object::Person(Box::new(u)));
}
let ap_id = apub_id.to_string();
let ap_id = apub_id.clone();
let community = blocking(context.pool(), move |conn| {
Community::read_from_apub_id(conn, &ap_id)
Community::read_from_apub_id(conn, &ap_id.into())
})
.await?;
if let Ok(c) = community {
return Ok(Object::Community(c));
return Ok(Object::Community(Box::new(c)));
}
let ap_id = apub_id.to_string();
let private_message = blocking(context.pool(), move |conn| {
PrivateMessage::read_from_apub_id(conn, &ap_id)
PrivateMessage::read_from_apub_id(conn, &apub_id.into())
})
.await?;
if let Ok(pm) = private_message {
return Ok(Object::PrivateMessage(pm));
return Ok(Object::PrivateMessage(Box::new(pm)));
}
Err(NotFound.into())

View file

@ -6,7 +6,7 @@ use crate::{
check_object_for_community_or_site_ban,
create_tombstone,
get_object_from_apub,
get_or_fetch_and_upsert_user,
get_or_fetch_and_upsert_person,
get_source_markdown_value,
set_content_and_source,
FromApub,
@ -18,16 +18,19 @@ use crate::{
use activitystreams::{
object::{kind::NoteType, ApObject, Note, Tombstone},
prelude::*,
public,
};
use anyhow::{anyhow, Context};
use lemmy_api_structs::blocking;
use lemmy_db_queries::{Crud, DbPool};
use lemmy_db_schema::source::{
use lemmy_db_schema::{
source::{
comment::{Comment, CommentForm},
community::Community,
person::Person,
post::Post,
user::User_,
},
CommentId,
};
use lemmy_structs::blocking;
use lemmy_utils::{
location_info,
utils::{convert_datetime, remove_slurs},
@ -44,32 +47,29 @@ impl ToApub for Comment {
let mut comment = ApObject::new(Note::new());
let creator_id = self.creator_id;
let creator = blocking(pool, move |conn| User_::read(conn, creator_id)).await??;
let creator = blocking(pool, move |conn| Person::read(conn, creator_id)).await??;
let post_id = self.post_id;
let post = blocking(pool, move |conn| Post::read(conn, post_id)).await??;
let community_id = post.community_id;
let community = blocking(pool, move |conn| Community::read(conn, community_id)).await??;
// Add a vector containing some important info to the "in_reply_to" field
// [post_ap_id, Option(parent_comment_ap_id)]
let mut in_reply_to_vec = vec![post.ap_id];
let mut in_reply_to_vec = vec![post.ap_id.into_inner()];
if let Some(parent_id) = self.parent_id {
let parent_comment = blocking(pool, move |conn| Comment::read(conn, parent_id)).await??;
in_reply_to_vec.push(parent_comment.ap_id);
in_reply_to_vec.push(parent_comment.ap_id.into_inner());
}
comment
// Not needed when the Post is embedded in a collection (like for community outbox)
.set_many_contexts(lemmy_context()?)
.set_id(Url::parse(&self.ap_id)?)
.set_id(self.ap_id.to_owned().into_inner())
.set_published(convert_datetime(self.published))
.set_to(community.actor_id)
.set_to(public())
.set_many_in_reply_tos(in_reply_to_vec)
.set_attributed_to(creator.actor_id);
.set_attributed_to(creator.actor_id.into_inner());
set_content_and_source(&mut comment, &self.content)?;
@ -81,7 +81,12 @@ impl ToApub for Comment {
}
fn to_tombstone(&self) -> Result<Tombstone, LemmyError> {
create_tombstone(self.deleted, &self.ap_id, self.updated, NoteType::Note)
create_tombstone(
self.deleted,
self.ap_id.to_owned().into(),
self.updated,
NoteType::Note,
)
}
}
@ -98,13 +103,13 @@ impl FromApub for Comment {
expected_domain: Url,
request_counter: &mut i32,
) -> Result<Comment, LemmyError> {
check_object_for_community_or_site_ban(note, context, request_counter).await?;
let comment: Comment =
get_object_from_apub(note, context, expected_domain, request_counter).await?;
let post_id = comment.post_id;
let post = blocking(context.pool(), move |conn| Post::read(conn, post_id)).await??;
check_object_for_community_or_site_ban(note, post.community_id, context, request_counter)
.await?;
if post.locked {
// This is not very efficient because a comment gets inserted just to be deleted right
// afterwards, but it seems to be the easiest way to implement it.
@ -133,7 +138,8 @@ impl FromApubToForm<NoteExt> for CommentForm {
.as_single_xsd_any_uri()
.context(location_info!())?;
let creator = get_or_fetch_and_upsert_user(creator_actor_id, context, request_counter).await?;
let creator =
get_or_fetch_and_upsert_person(creator_actor_id, context, request_counter).await?;
let mut in_reply_tos = note
.in_reply_to()
@ -150,7 +156,7 @@ impl FromApubToForm<NoteExt> for CommentForm {
// The 2nd item, if it exists, is the parent comment apub_id
// For deeply nested comments, FromApub automatically gets called recursively
let parent_id: Option<i32> = match in_reply_tos.next() {
let parent_id: Option<CommentId> = match in_reply_tos.next() {
Some(parent_comment_uri) => {
let parent_comment_ap_id = &parent_comment_uri?;
let parent_comment =

View file

@ -1,6 +1,6 @@
use crate::{
extensions::{context::lemmy_context, group_extensions::GroupExtension},
fetcher::user::get_or_fetch_and_upsert_user,
fetcher::person::get_or_fetch_and_upsert_person,
objects::{
check_object_domain,
create_tombstone,
@ -22,13 +22,13 @@ use activitystreams::{
};
use activitystreams_ext::Ext2;
use anyhow::Context;
use lemmy_api_structs::blocking;
use lemmy_db_queries::DbPool;
use lemmy_db_schema::{
naive_now,
source::community::{Community, CommunityForm},
};
use lemmy_db_views_actor::community_moderator_view::CommunityModeratorView;
use lemmy_structs::blocking;
use lemmy_utils::{
location_info,
utils::{check_slurs, check_slurs_opt, convert_datetime},
@ -51,15 +51,15 @@ impl ToApub for Community {
CommunityModeratorView::for_community(&conn, id)
})
.await??;
let moderators: Vec<String> = moderators
let moderators: Vec<Url> = moderators
.into_iter()
.map(|m| m.moderator.actor_id)
.map(|m| m.moderator.actor_id.into_inner())
.collect();
let mut group = ApObject::new(Group::new());
group
.set_many_contexts(lemmy_context()?)
.set_id(Url::parse(&self.actor_id)?)
.set_id(self.actor_id.to_owned().into())
.set_name(self.title.to_owned())
.set_published(convert_datetime(self.published))
.set_many_attributed_tos(moderators);
@ -73,42 +73,40 @@ impl ToApub for Community {
if let Some(icon_url) = &self.icon {
let mut image = Image::new();
image.set_url(Url::parse(icon_url)?);
image.set_url::<Url>(icon_url.to_owned().into());
group.set_icon(image.into_any_base()?);
}
if let Some(banner_url) = &self.banner {
let mut image = Image::new();
image.set_url(Url::parse(banner_url)?);
image.set_url::<Url>(banner_url.to_owned().into());
group.set_image(image.into_any_base()?);
}
let mut ap_actor = ApActor::new(self.get_inbox_url()?, group);
let mut ap_actor = ApActor::new(self.inbox_url.clone().into(), group);
ap_actor
.set_preferred_username(self.name.to_owned())
.set_outbox(self.get_outbox_url()?)
.set_followers(self.get_followers_url()?)
.set_followers(self.followers_url.clone().into())
.set_endpoints(Endpoints {
shared_inbox: Some(self.get_shared_inbox_url()?),
shared_inbox: Some(self.get_shared_inbox_or_inbox_url()),
..Default::default()
});
let nsfw = self.nsfw;
let category_id = self.category_id;
let group_extension = blocking(pool, move |conn| {
GroupExtension::new(conn, category_id, nsfw)
})
.await??;
Ok(Ext2::new(
ap_actor,
group_extension,
GroupExtension::new(self.nsfw)?,
self.get_public_key_ext()?,
))
}
fn to_tombstone(&self) -> Result<Tombstone, LemmyError> {
create_tombstone(self.deleted, &self.actor_id, self.updated, GroupType::Group)
create_tombstone(
self.deleted,
self.actor_id.to_owned().into(),
self.updated,
GroupType::Group,
)
}
}
@ -145,7 +143,7 @@ impl FromApubToForm<GroupExt> for CommunityForm {
.as_xsd_any_uri()
.context(location_info!())?;
let creator = get_or_fetch_and_upsert_user(creator_uri, context, request_counter).await?;
let creator = get_or_fetch_and_upsert_person(creator_uri, context, request_counter).await?;
let name = group
.inner
.preferred_username()
@ -175,11 +173,10 @@ impl FromApubToForm<GroupExt> for CommunityForm {
.url()
.context(location_info!())?
.as_single_xsd_any_uri()
.map(|u| u.to_string()),
.map(|u| u.to_owned().into()),
),
None => None,
};
let banner = match group.image() {
Some(any_image) => Some(
Image::from_any_base(any_image.as_one().context(location_info!())?.clone())
@ -188,22 +185,27 @@ impl FromApubToForm<GroupExt> for CommunityForm {
.url()
.context(location_info!())?
.as_single_xsd_any_uri()
.map(|u| u.to_string()),
.map(|u| u.to_owned().into()),
),
None => None,
};
let shared_inbox = group
.inner
.endpoints()?
.map(|e| e.shared_inbox)
.flatten()
.map(|s| s.to_owned().into());
Ok(CommunityForm {
name,
title,
description,
category_id: group.ext_one.category.identifier.parse::<i32>()?,
creator_id: creator.id,
removed: None,
published: group.inner.published().map(|u| u.to_owned().naive_local()),
updated: group.inner.updated().map(|u| u.to_owned().naive_local()),
deleted: None,
nsfw: group.ext_one.sensitive,
nsfw: group.ext_one.sensitive.unwrap_or(false),
actor_id: Some(check_object_domain(group, expected_domain)?),
local: false,
private_key: None,
@ -211,6 +213,16 @@ impl FromApubToForm<GroupExt> for CommunityForm {
last_refreshed_at: Some(naive_now()),
icon,
banner,
followers_url: Some(
group
.inner
.followers()?
.context(location_info!())?
.to_owned()
.into(),
),
inbox_url: Some(group.inner.inbox()?.to_owned().into()),
shared_inbox_url: Some(shared_inbox),
})
}
}

View file

@ -1,6 +1,6 @@
use crate::{
check_is_apub_id_valid,
fetcher::{community::get_or_fetch_and_upsert_community, user::get_or_fetch_and_upsert_user},
fetcher::{community::get_or_fetch_and_upsert_community, person::get_or_fetch_and_upsert_person},
inbox::community_inbox::check_community_or_site_ban,
};
use activitystreams::{
@ -11,17 +11,24 @@ use activitystreams::{
};
use anyhow::{anyhow, Context};
use chrono::NaiveDateTime;
use diesel::result::Error::NotFound;
use lemmy_api_structs::blocking;
use lemmy_db_queries::{ApubObject, Crud, DbPool};
use lemmy_structs::blocking;
use lemmy_utils::{location_info, settings::Settings, utils::convert_datetime, LemmyError};
use lemmy_db_schema::{source::community::Community, CommunityId, DbUrl};
use lemmy_utils::{
location_info,
settings::structs::Settings,
utils::{convert_datetime, markdown_to_html},
LemmyError,
};
use lemmy_websocket::LemmyContext;
use url::Url;
pub(crate) mod comment;
pub(crate) mod community;
pub(crate) mod person;
pub(crate) mod post;
pub(crate) mod private_message;
pub(crate) mod user;
/// Trait for converting an object or actor into the respective ActivityPub type.
#[async_trait::async_trait(?Send)]
@ -64,7 +71,7 @@ pub(in crate::objects) trait FromApubToForm<ApubType> {
/// Updated is actually the deletion time
fn create_tombstone<T>(
deleted: bool,
object_id: &str,
object_id: Url,
updated: Option<NaiveDateTime>,
former_type: T,
) -> Result<Tombstone, LemmyError>
@ -74,7 +81,7 @@ where
if deleted {
if let Some(updated) = updated {
let mut tombstone = Tombstone::new();
tombstone.set_id(object_id.parse()?);
tombstone.set_id(object_id);
tombstone.set_former_type(former_type.to_string());
tombstone.set_deleted(convert_datetime(updated));
Ok(tombstone)
@ -89,14 +96,14 @@ where
pub(in crate::objects) fn check_object_domain<T, Kind>(
apub: &T,
expected_domain: Url,
) -> Result<String, LemmyError>
) -> Result<DbUrl, LemmyError>
where
T: Base + AsBase<Kind>,
{
let domain = expected_domain.domain().context(location_info!())?;
let object_id = apub.id(domain)?.context(location_info!())?;
check_is_apub_id_valid(&object_id)?;
Ok(object_id.to_string())
check_is_apub_id_valid(object_id)?;
Ok(object_id.to_owned().into())
}
pub(in crate::objects) fn set_content_and_source<T, Kind1, Kind2>(
@ -112,11 +119,8 @@ where
.set_media_type(mime_markdown()?);
object.set_source(source.into_any_base()?);
// set `content` to markdown for compatibility with older Lemmy versions
// TODO: change this to HTML in a while
object.set_content(markdown_text);
object.set_media_type(mime_markdown()?);
//object.set_content(markdown_to_html(markdown_text));
object.set_content(markdown_to_html(markdown_text));
object.set_media_type(mime_html()?);
Ok(())
}
@ -132,9 +136,7 @@ where
.flatten()
.map(|s| s.to_string());
if content.is_some() {
let source = object.source();
// updated lemmy version, read markdown from `source.content`
if let Some(source) = source {
let source = object.source().context(location_info!())?;
let source = Object::<()>::from_any_base(source.to_owned())?.context(location_info!())?;
check_is_markdown(source.media_type())?;
let source_content = source
@ -145,19 +147,17 @@ where
.to_string();
return Ok(Some(source_content));
}
// older lemmy version, read markdown from `content`
// TODO: remove this after a while
else {
return Ok(content);
}
}
Ok(None)
}
pub(in crate::objects) fn mime_markdown() -> Result<Mime, FromStrError> {
fn mime_markdown() -> Result<Mime, FromStrError> {
"text/markdown".parse()
}
fn mime_html() -> Result<Mime, FromStrError> {
"text/html".parse()
}
pub(in crate::objects) fn check_is_markdown(mime: Option<&Mime>) -> Result<(), LemmyError> {
let mime = mime.context(location_info!())?;
if !mime.eq(&mime_markdown()?) {
@ -172,7 +172,7 @@ pub(in crate::objects) fn check_is_markdown(mime: Option<&Mime>) -> Result<(), L
/// Converts an ActivityPub object (eg `Note`) to a database object (eg `Comment`). If an object
/// with the same ActivityPub ID already exists in the database, it is returned directly. Otherwise
/// the apub object is parsed, inserted and returned.
pub(in crate::objects) async fn get_object_from_apub<From, Kind, To, ToForm>(
pub(in crate::objects) async fn get_object_from_apub<From, Kind, To, ToForm, IdType>(
from: &From,
context: &LemmyContext,
expected_domain: Url,
@ -180,16 +180,16 @@ pub(in crate::objects) async fn get_object_from_apub<From, Kind, To, ToForm>(
) -> Result<To, LemmyError>
where
From: BaseExt<Kind>,
To: ApubObject<ToForm> + Crud<ToForm> + Send + 'static,
To: ApubObject<ToForm> + Crud<ToForm, IdType> + Send + 'static,
ToForm: FromApubToForm<From> + Send + 'static,
{
let object_id = from.id_unchecked().context(location_info!())?.to_owned();
let domain = object_id.domain().context(location_info!())?;
// if its a local object, return it directly from the database
if Settings::get().hostname == domain {
if Settings::get().hostname() == domain {
let object = blocking(context.pool(), move |conn| {
To::read_from_apub_id(conn, object_id.as_str())
To::read_from_apub_id(conn, &object_id.into())
})
.await??;
Ok(object)
@ -205,23 +205,43 @@ where
pub(in crate::objects) async fn check_object_for_community_or_site_ban<T, Kind>(
object: &T,
community_id: CommunityId,
context: &LemmyContext,
request_counter: &mut i32,
) -> Result<(), LemmyError>
where
T: ObjectExt<Kind>,
{
let user_id = object
let person_id = object
.attributed_to()
.context(location_info!())?
.as_single_xsd_any_uri()
.context(location_info!())?;
let user = get_or_fetch_and_upsert_user(user_id, context, request_counter).await?;
let community_id = object
let person = get_or_fetch_and_upsert_person(person_id, context, request_counter).await?;
check_community_or_site_ban(&person, community_id, context.pool()).await
}
pub(in crate::objects) async fn get_to_community<T, Kind>(
object: &T,
context: &LemmyContext,
request_counter: &mut i32,
) -> Result<Community, LemmyError>
where
T: ObjectExt<Kind>,
{
let community_ids = object
.to()
.context(location_info!())?
.as_single_xsd_any_uri()
.context(location_info!())?;
let community = get_or_fetch_and_upsert_community(community_id, context, request_counter).await?;
check_community_or_site_ban(&user, &community, context.pool()).await
.as_many()
.context(location_info!())?
.iter()
.map(|a| a.as_xsd_any_uri().context(location_info!()))
.collect::<Result<Vec<&Url>, anyhow::Error>>()?;
for cid in community_ids {
let community = get_or_fetch_and_upsert_community(&cid, context, request_counter).await;
if community.is_ok() {
return community;
}
}
Err(NotFound.into())
}

View file

@ -18,15 +18,15 @@ use activitystreams::{
};
use activitystreams_ext::Ext1;
use anyhow::Context;
use lemmy_api_structs::blocking;
use lemmy_db_queries::{ApubObject, DbPool};
use lemmy_db_schema::{
naive_now,
source::user::{UserForm, User_},
source::person::{Person as DbPerson, PersonForm},
};
use lemmy_structs::blocking;
use lemmy_utils::{
location_info,
settings::Settings,
settings::structs::Settings,
utils::{check_slurs, check_slurs_opt, convert_datetime},
LemmyError,
};
@ -34,14 +34,14 @@ use lemmy_websocket::LemmyContext;
use url::Url;
#[async_trait::async_trait(?Send)]
impl ToApub for User_ {
impl ToApub for DbPerson {
type ApubType = PersonExt;
async fn to_apub(&self, _pool: &DbPool) -> Result<PersonExt, LemmyError> {
let mut person = ApObject::new(Person::new());
person
.set_many_contexts(lemmy_context()?)
.set_id(Url::parse(&self.actor_id)?)
.set_id(self.actor_id.to_owned().into_inner())
.set_published(convert_datetime(self.published));
if let Some(u) = self.updated {
@ -50,33 +50,30 @@ impl ToApub for User_ {
if let Some(avatar_url) = &self.avatar {
let mut image = Image::new();
image.set_url(Url::parse(avatar_url)?);
image.set_url::<Url>(avatar_url.to_owned().into());
person.set_icon(image.into_any_base()?);
}
if let Some(banner_url) = &self.banner {
let mut image = Image::new();
image.set_url(Url::parse(banner_url)?);
image.set_url::<Url>(banner_url.to_owned().into());
person.set_image(image.into_any_base()?);
}
if let Some(bio) = &self.bio {
set_content_and_source(&mut person, bio)?;
// Also set summary for compatibility with older Lemmy versions.
// TODO: remove this after a while.
person.set_summary(bio.to_owned());
}
if let Some(i) = self.preferred_username.to_owned() {
person.set_name(i);
}
let mut ap_actor = ApActor::new(self.get_inbox_url()?, person);
let mut ap_actor = ApActor::new(self.inbox_url.clone().into(), person);
ap_actor
.set_preferred_username(self.name.to_owned())
.set_outbox(self.get_outbox_url()?)
.set_endpoints(Endpoints {
shared_inbox: Some(self.get_shared_inbox_url()?),
shared_inbox: Some(self.get_shared_inbox_or_inbox_url()),
..Default::default()
});
@ -88,7 +85,7 @@ impl ToApub for User_ {
}
#[async_trait::async_trait(?Send)]
impl FromApub for User_ {
impl FromApub for DbPerson {
type ApubType = PersonExt;
async fn from_apub(
@ -96,26 +93,29 @@ impl FromApub for User_ {
context: &LemmyContext,
expected_domain: Url,
request_counter: &mut i32,
) -> Result<User_, LemmyError> {
let user_id = person.id_unchecked().context(location_info!())?.to_owned();
let domain = user_id.domain().context(location_info!())?;
if domain == Settings::get().hostname {
let user = blocking(context.pool(), move |conn| {
User_::read_from_apub_id(conn, user_id.as_str())
) -> Result<DbPerson, LemmyError> {
let person_id = person.id_unchecked().context(location_info!())?.to_owned();
let domain = person_id.domain().context(location_info!())?;
if domain == Settings::get().hostname() {
let person = blocking(context.pool(), move |conn| {
DbPerson::read_from_apub_id(conn, &person_id.into())
})
.await??;
Ok(user)
Ok(person)
} else {
let user_form =
UserForm::from_apub(person, context, expected_domain, request_counter).await?;
let user = blocking(context.pool(), move |conn| User_::upsert(conn, &user_form)).await??;
Ok(user)
let person_form =
PersonForm::from_apub(person, context, expected_domain, request_counter).await?;
let person = blocking(context.pool(), move |conn| {
DbPerson::upsert(conn, &person_form)
})
.await??;
Ok(person)
}
}
}
#[async_trait::async_trait(?Send)]
impl FromApubToForm<PersonExt> for UserForm {
impl FromApubToForm<PersonExt> for PersonForm {
async fn from_apub(
person: &PersonExt,
_context: &LemmyContext,
@ -129,7 +129,7 @@ impl FromApubToForm<PersonExt> for UserForm {
.url()
.context(location_info!())?
.as_single_xsd_any_uri()
.map(|u| u.to_string()),
.map(|url| url.to_owned()),
),
None => None,
};
@ -142,7 +142,7 @@ impl FromApubToForm<PersonExt> for UserForm {
.url()
.context(location_info!())?
.as_single_xsd_any_uri()
.map(|u| u.to_string()),
.map(|url| url.to_owned()),
),
None => None,
};
@ -158,38 +158,35 @@ impl FromApubToForm<PersonExt> for UserForm {
.flatten()
.map(|n| n.to_owned().xsd_string())
.flatten();
let bio = get_source_markdown_value(person)?;
let shared_inbox = person
.inner
.endpoints()?
.map(|e| e.shared_inbox)
.flatten()
.map(|s| s.to_owned().into());
check_slurs(&name)?;
check_slurs_opt(&preferred_username)?;
check_slurs_opt(&bio)?;
Ok(UserForm {
Ok(PersonForm {
name,
preferred_username: Some(preferred_username),
password_encrypted: "".to_string(),
admin: false,
banned: None,
email: None,
avatar,
banner,
deleted: None,
avatar: avatar.map(|o| o.map(|i| i.into())),
banner: banner.map(|o| o.map(|i| i.into())),
published: person.inner.published().map(|u| u.to_owned().naive_local()),
updated: person.updated().map(|u| u.to_owned().naive_local()),
show_nsfw: false,
theme: "".to_string(),
default_sort_type: 0,
default_listing_type: 0,
lang: "".to_string(),
show_avatars: false,
send_notifications_to_email: false,
matrix_user_id: None,
actor_id: Some(check_object_domain(person, expected_domain)?),
bio: Some(bio),
local: false,
local: Some(false),
private_key: None,
public_key: Some(person.ext_one.public_key.to_owned().public_key_pem),
public_key: Some(Some(person.ext_one.public_key.to_owned().public_key_pem)),
last_refreshed_at: Some(naive_now()),
inbox_url: Some(person.inner.inbox()?.to_owned().into()),
shared_inbox_url: Some(shared_inbox),
})
}
}

View file

@ -1,12 +1,13 @@
use crate::{
extensions::{context::lemmy_context, page_extension::PageExtension},
fetcher::{community::get_or_fetch_and_upsert_community, user::get_or_fetch_and_upsert_user},
fetcher::person::get_or_fetch_and_upsert_person,
objects::{
check_object_domain,
check_object_for_community_or_site_ban,
create_tombstone,
get_object_from_apub,
get_source_markdown_value,
get_to_community,
set_content_and_source,
FromApub,
FromApubToForm,
@ -17,16 +18,20 @@ use crate::{
use activitystreams::{
object::{kind::PageType, ApObject, Image, Page, Tombstone},
prelude::*,
public,
};
use activitystreams_ext::Ext1;
use anyhow::Context;
use lemmy_api_structs::blocking;
use lemmy_db_queries::{Crud, DbPool};
use lemmy_db_schema::source::{
use lemmy_db_schema::{
self,
source::{
community::Community,
person::Person,
post::{Post, PostForm},
user::User_,
},
};
use lemmy_structs::blocking;
use lemmy_utils::{
location_info,
request::fetch_iframely_and_pictrs_data,
@ -45,7 +50,7 @@ impl ToApub for Post {
let mut page = ApObject::new(Page::new());
let creator_id = self.creator_id;
let creator = blocking(pool, move |conn| User_::read(conn, creator_id)).await??;
let creator = blocking(pool, move |conn| Person::read(conn, creator_id)).await??;
let community_id = self.community_id;
let community = blocking(pool, move |conn| Community::read(conn, community_id)).await??;
@ -55,28 +60,26 @@ impl ToApub for Post {
// TODO: need to set proper context defining sensitive/commentsEnabled fields
// https://git.asonix.dog/Aardwolf/activitystreams/issues/5
.set_many_contexts(lemmy_context()?)
.set_id(self.ap_id.parse::<Url>()?)
// Use summary field to be consistent with mastodon content warning.
// https://mastodon.xyz/@Louisa/103987265222901387.json
.set_id(self.ap_id.to_owned().into_inner())
.set_name(self.name.to_owned())
// `summary` field for compatibility with lemmy v0.9.9 and older,
// TODO: remove this after some time
.set_summary(self.name.to_owned())
.set_published(convert_datetime(self.published))
.set_to(community.actor_id)
.set_attributed_to(creator.actor_id);
.set_many_tos(vec![community.actor_id.into_inner(), public()])
.set_attributed_to(creator.actor_id.into_inner());
if let Some(body) = &self.body {
set_content_and_source(&mut page, &body)?;
}
// TODO: hacky code because we get self.url == Some("")
// https://github.com/LemmyNet/lemmy/issues/602
let url = self.url.as_ref().filter(|u| !u.is_empty());
if let Some(u) = url {
page.set_url(Url::parse(u)?);
if let Some(url) = &self.url {
page.set_url::<Url>(url.to_owned().into());
}
if let Some(thumbnail_url) = &self.thumbnail_url {
let mut image = Image::new();
image.set_url(Url::parse(thumbnail_url)?);
image.set_url::<Url>(thumbnail_url.to_owned().into());
page.set_image(image.into_any_base()?);
}
@ -85,15 +88,20 @@ impl ToApub for Post {
}
let ext = PageExtension {
comments_enabled: !self.locked,
sensitive: self.nsfw,
stickied: self.stickied,
comments_enabled: Some(!self.locked),
sensitive: Some(self.nsfw),
stickied: Some(self.stickied),
};
Ok(Ext1::new(page, ext))
}
fn to_tombstone(&self) -> Result<Tombstone, LemmyError> {
create_tombstone(self.deleted, &self.ap_id, self.updated, PageType::Page)
create_tombstone(
self.deleted,
self.ap_id.to_owned().into(),
self.updated,
PageType::Page,
)
}
}
@ -110,8 +118,10 @@ impl FromApub for Post {
expected_domain: Url,
request_counter: &mut i32,
) -> Result<Post, LemmyError> {
check_object_for_community_or_site_ban(page, context, request_counter).await?;
get_object_from_apub(page, context, expected_domain, request_counter).await
let post: Post = get_object_from_apub(page, context, expected_domain, request_counter).await?;
check_object_for_community_or_site_ban(page, post.community_id, context, request_counter)
.await?;
Ok(post)
}
}
@ -132,20 +142,12 @@ impl FromApubToForm<PageExt> for PostForm {
.as_single_xsd_any_uri()
.context(location_info!())?;
let creator = get_or_fetch_and_upsert_user(creator_actor_id, context, request_counter).await?;
let creator =
get_or_fetch_and_upsert_person(creator_actor_id, context, request_counter).await?;
let community_actor_id = page
.inner
.to()
.as_ref()
.context(location_info!())?
.as_single_xsd_any_uri()
.context(location_info!())?;
let community = get_to_community(page, context, request_counter).await?;
let community =
get_or_fetch_and_upsert_community(community_actor_id, context, request_counter).await?;
let thumbnail_url = match &page.inner.image() {
let thumbnail_url: Option<Url> = match &page.inner.image() {
Some(any_image) => Image::from_any_base(
any_image
.to_owned()
@ -157,7 +159,7 @@ impl FromApubToForm<PageExt> for PostForm {
.url()
.context(location_info!())?
.as_single_xsd_any_uri()
.map(|u| u.to_string()),
.map(|url| url.to_owned()),
None => None,
};
let url = page
@ -165,19 +167,22 @@ impl FromApubToForm<PageExt> for PostForm {
.url()
.map(|u| u.as_single_xsd_any_uri())
.flatten()
.map(|s| s.to_string());
.map(|u| u.to_owned());
let (iframely_title, iframely_description, iframely_html, pictrs_thumbnail) =
if let Some(url) = &url {
fetch_iframely_and_pictrs_data(context.client(), Some(url.to_owned())).await
fetch_iframely_and_pictrs_data(context.client(), Some(url)).await
} else {
(None, None, None, thumbnail_url)
};
let name = page
.inner
.summary()
.as_ref()
.name()
.map(|s| s.map(|s2| s2.to_owned()))
// The following is for compatibility with lemmy v0.9.9 and older
// TODO: remove it after some time (along with the map above)
.or_else(|| page.inner.summary().map(|s| s.to_owned()))
.context(location_info!())?
.as_single_xsd_string()
.context(location_info!())?
@ -188,12 +193,12 @@ impl FromApubToForm<PageExt> for PostForm {
let body_slurs_removed = body.map(|b| remove_slurs(&b));
Ok(PostForm {
name,
url,
url: url.map(|u| u.into()),
body: body_slurs_removed,
creator_id: creator.id,
community_id: community.id,
removed: None,
locked: Some(!ext.comments_enabled),
locked: ext.comments_enabled.map(|e| !e),
published: page
.inner
.published()
@ -205,12 +210,12 @@ impl FromApubToForm<PageExt> for PostForm {
.as_ref()
.map(|u| u.to_owned().naive_local()),
deleted: None,
nsfw: ext.sensitive,
stickied: Some(ext.stickied),
nsfw: ext.sensitive.unwrap_or(false),
stickied: ext.stickied.or(Some(false)),
embed_title: iframely_title,
embed_description: iframely_description,
embed_html: iframely_html,
thumbnail_url: pictrs_thumbnail,
thumbnail_url: pictrs_thumbnail.map(|u| u.into()),
ap_id: Some(check_object_domain(page, expected_domain)?),
local: false,
})

View file

@ -1,7 +1,7 @@
use crate::{
check_is_apub_id_valid,
extensions::context::lemmy_context,
fetcher::user::get_or_fetch_and_upsert_user,
fetcher::person::get_or_fetch_and_upsert_person,
objects::{
check_object_domain,
create_tombstone,
@ -19,12 +19,12 @@ use activitystreams::{
prelude::*,
};
use anyhow::Context;
use lemmy_api_structs::blocking;
use lemmy_db_queries::{Crud, DbPool};
use lemmy_db_schema::source::{
person::Person,
private_message::{PrivateMessage, PrivateMessageForm},
user::User_,
};
use lemmy_structs::blocking;
use lemmy_utils::{location_info, utils::convert_datetime, LemmyError};
use lemmy_websocket::LemmyContext;
use url::Url;
@ -37,17 +37,17 @@ impl ToApub for PrivateMessage {
let mut private_message = ApObject::new(Note::new());
let creator_id = self.creator_id;
let creator = blocking(pool, move |conn| User_::read(conn, creator_id)).await??;
let creator = blocking(pool, move |conn| Person::read(conn, creator_id)).await??;
let recipient_id = self.recipient_id;
let recipient = blocking(pool, move |conn| User_::read(conn, recipient_id)).await??;
let recipient = blocking(pool, move |conn| Person::read(conn, recipient_id)).await??;
private_message
.set_many_contexts(lemmy_context()?)
.set_id(Url::parse(&self.ap_id.to_owned())?)
.set_id(self.ap_id.to_owned().into_inner())
.set_published(convert_datetime(self.published))
.set_to(recipient.actor_id)
.set_attributed_to(creator.actor_id);
.set_to(recipient.actor_id.into_inner())
.set_attributed_to(creator.actor_id.into_inner());
set_content_and_source(&mut private_message, &self.content)?;
@ -59,7 +59,12 @@ impl ToApub for PrivateMessage {
}
fn to_tombstone(&self) -> Result<Tombstone, LemmyError> {
create_tombstone(self.deleted, &self.ap_id, self.updated, NoteType::Note)
create_tombstone(
self.deleted,
self.ap_id.to_owned().into(),
self.updated,
NoteType::Note,
)
}
}
@ -92,7 +97,8 @@ impl FromApubToForm<NoteExt> for PrivateMessageForm {
.single_xsd_any_uri()
.context(location_info!())?;
let creator = get_or_fetch_and_upsert_user(&creator_actor_id, context, request_counter).await?;
let creator =
get_or_fetch_and_upsert_person(&creator_actor_id, context, request_counter).await?;
let recipient_actor_id = note
.to()
.context(location_info!())?
@ -100,7 +106,7 @@ impl FromApubToForm<NoteExt> for PrivateMessageForm {
.single_xsd_any_uri()
.context(location_info!())?;
let recipient =
get_or_fetch_and_upsert_user(&recipient_actor_id, context, request_counter).await?;
get_or_fetch_and_upsert_person(&recipient_actor_id, context, request_counter).await?;
let ap_id = note.id_unchecked().context(location_info!())?.to_string();
check_is_apub_id_valid(&Url::parse(&ap_id)?)?;

View file

@ -1,6 +1,4 @@
use actix_web::*;
use http_signature_normalization_actix::digest::middleware::VerifyDigest;
use lemmy_apub::{
use crate::{
http::{
comment::get_apub_comment,
community::{
@ -10,30 +8,34 @@ use lemmy_apub::{
get_apub_community_outbox,
},
get_activity,
person::{get_apub_person_http, get_apub_person_inbox, get_apub_person_outbox},
post::get_apub_post,
user::{get_apub_user_http, get_apub_user_inbox, get_apub_user_outbox},
},
inbox::{community_inbox::community_inbox, shared_inbox::shared_inbox, user_inbox::user_inbox},
inbox::{
community_inbox::community_inbox,
person_inbox::person_inbox,
shared_inbox::shared_inbox,
},
APUB_JSON_CONTENT_TYPE,
};
use lemmy_utils::settings::Settings;
use actix_web::*;
use http_signature_normalization_actix::digest::middleware::VerifyDigest;
use lemmy_utils::settings::structs::Settings;
use sha2::{Digest, Sha256};
static APUB_JSON_CONTENT_TYPE_LONG: &str =
"application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"";
pub fn config(cfg: &mut web::ServiceConfig) {
if Settings::get().federation.enabled {
println!("federation enabled, host is {}", Settings::get().hostname);
if Settings::get().federation().enabled {
println!("federation enabled, host is {}", Settings::get().hostname());
let digest_verifier = VerifyDigest::new(Sha256::new());
let header_guard_accept = guard::Any(guard::Header("Accept", APUB_JSON_CONTENT_TYPE))
.or(guard::Header("Accept", APUB_JSON_CONTENT_TYPE_LONG));
let header_guard_content_type =
guard::Any(guard::Header("Content-Type", APUB_JSON_CONTENT_TYPE))
.or(guard::Header("Content-Type", APUB_JSON_CONTENT_TYPE_LONG))
// TODO: compatibility with previous lemmy versions, remove this later
.or(guard::Header("Content-Type", "application/json"));
.or(guard::Header("Content-Type", APUB_JSON_CONTENT_TYPE_LONG));
cfg
.service(
@ -55,9 +57,12 @@ pub fn config(cfg: &mut web::ServiceConfig) {
"/c/{community_name}/inbox",
web::get().to(get_apub_community_inbox),
)
.route("/u/{user_name}", web::get().to(get_apub_user_http))
.route("/u/{user_name}/outbox", web::get().to(get_apub_user_outbox))
.route("/u/{user_name}/inbox", web::get().to(get_apub_user_inbox))
.route("/u/{user_name}", web::get().to(get_apub_person_http))
.route(
"/u/{user_name}/outbox",
web::get().to(get_apub_person_outbox),
)
.route("/u/{user_name}/inbox", web::get().to(get_apub_person_inbox))
.route("/post/{post_id}", web::get().to(get_apub_post))
.route("/comment/{comment_id}", web::get().to(get_apub_comment))
.route("/activities/{type_}/{id}", web::get().to(get_activity)),
@ -68,7 +73,7 @@ pub fn config(cfg: &mut web::ServiceConfig) {
.wrap(digest_verifier)
.guard(header_guard_content_type)
.route("/c/{community_name}/inbox", web::post().to(community_inbox))
.route("/u/{user_name}/inbox", web::post().to(user_inbox))
.route("/u/{user_name}/inbox", web::post().to(person_inbox))
.route("/inbox", web::post().to(shared_inbox)),
);
}

View file

@ -6,20 +6,24 @@ edition = "2018"
[lib]
name = "lemmy_db_queries"
path = "src/lib.rs"
doctest = false
[dependencies]
lemmy_utils = { path = "../lemmy_utils" }
lemmy_db_schema = { path = "../lemmy_db_schema" }
lemmy_utils = { path = "../utils" }
lemmy_db_schema = { path = "../db_schema" }
diesel = { version = "1.4.5", features = ["postgres","chrono","r2d2","serde_json"] }
diesel_migrations = "1.4.0"
chrono = { version = "0.4.19", features = ["serde"] }
serde = { version = "1.0.118", features = ["derive"] }
serde_json = { version = "1.0.60", features = ["preserve_order"] }
serde = { version = "1.0.123", features = ["derive"] }
serde_json = { version = "1.0.61", features = ["preserve_order"] }
strum = "0.20.0"
strum_macros = "0.20.1"
log = "0.4.11"
sha2 = "0.9.2"
url = { version = "2.2.0", features = ["serde"] }
log = "0.4.14"
sha2 = "0.9.3"
url = { version = "2.2.1", features = ["serde"] }
lazy_static = "1.4.0"
regex = "1.4.2"
regex = "1.4.3"
bcrypt = "0.9.0"
[dev-dependencies]
serial_test = "0.5.1"

View file

@ -1,12 +1,12 @@
use diesel::{result::Error, *};
use lemmy_db_schema::schema::comment_aggregates;
use lemmy_db_schema::{schema::comment_aggregates, CommentId};
use serde::Serialize;
#[derive(Queryable, Associations, Identifiable, PartialEq, Debug, Serialize, Clone)]
#[table_name = "comment_aggregates"]
pub struct CommentAggregates {
pub id: i32,
pub comment_id: i32,
pub comment_id: CommentId,
pub score: i64,
pub upvotes: i64,
pub downvotes: i64,
@ -14,7 +14,7 @@ pub struct CommentAggregates {
}
impl CommentAggregates {
pub fn read(conn: &PgConnection, comment_id: i32) -> Result<Self, Error> {
pub fn read(conn: &PgConnection, comment_id: CommentId) -> Result<Self, Error> {
comment_aggregates::table
.filter(comment_aggregates::comment_id.eq(comment_id))
.first::<Self>(conn)
@ -28,84 +28,67 @@ mod tests {
establish_unpooled_connection,
Crud,
Likeable,
ListingType,
SortType,
};
use lemmy_db_schema::source::{
comment::{Comment, CommentForm, CommentLike, CommentLikeForm},
community::{Community, CommunityForm},
person::{Person, PersonForm},
post::{Post, PostForm},
user::{UserForm, User_},
};
use serial_test::serial;
#[test]
#[serial]
fn test_crud() {
let conn = establish_unpooled_connection();
let new_user = UserForm {
let new_person = PersonForm {
name: "thommy_comment_agg".into(),
preferred_username: None,
password_encrypted: "nope".into(),
email: None,
matrix_user_id: None,
avatar: None,
banner: None,
admin: false,
banned: Some(false),
banned: None,
deleted: None,
published: None,
updated: None,
show_nsfw: false,
theme: "browser".into(),
default_sort_type: SortType::Hot as i16,
default_listing_type: ListingType::Subscribed as i16,
lang: "browser".into(),
show_avatars: true,
send_notifications_to_email: false,
actor_id: None,
bio: None,
local: true,
local: None,
private_key: None,
public_key: None,
last_refreshed_at: None,
inbox_url: None,
shared_inbox_url: None,
};
let inserted_user = User_::create(&conn, &new_user).unwrap();
let inserted_person = Person::create(&conn, &new_person).unwrap();
let another_user = UserForm {
let another_person = PersonForm {
name: "jerry_comment_agg".into(),
preferred_username: None,
password_encrypted: "nope".into(),
email: None,
matrix_user_id: None,
avatar: None,
banner: None,
admin: false,
banned: Some(false),
banned: None,
deleted: None,
published: None,
updated: None,
show_nsfw: false,
theme: "browser".into(),
default_sort_type: SortType::Hot as i16,
default_listing_type: ListingType::Subscribed as i16,
lang: "browser".into(),
show_avatars: true,
send_notifications_to_email: false,
actor_id: None,
bio: None,
local: true,
local: None,
private_key: None,
public_key: None,
last_refreshed_at: None,
inbox_url: None,
shared_inbox_url: None,
};
let another_inserted_user = User_::create(&conn, &another_user).unwrap();
let another_inserted_person = Person::create(&conn, &another_person).unwrap();
let new_community = CommunityForm {
name: "TIL_comment_agg".into(),
creator_id: inserted_user.id,
creator_id: inserted_person.id,
title: "nada".to_owned(),
description: None,
category_id: 1,
nsfw: false,
removed: None,
deleted: None,
@ -118,6 +101,9 @@ mod tests {
published: None,
icon: None,
banner: None,
followers_url: None,
inbox_url: None,
shared_inbox_url: None,
};
let inserted_community = Community::create(&conn, &new_community).unwrap();
@ -126,7 +112,7 @@ mod tests {
name: "A test post".into(),
url: None,
body: None,
creator_id: inserted_user.id,
creator_id: inserted_person.id,
community_id: inserted_community.id,
removed: None,
deleted: None,
@ -147,7 +133,7 @@ mod tests {
let comment_form = CommentForm {
content: "A test comment".into(),
creator_id: inserted_user.id,
creator_id: inserted_person.id,
post_id: inserted_post.id,
removed: None,
deleted: None,
@ -163,7 +149,7 @@ mod tests {
let child_comment_form = CommentForm {
content: "A test comment".into(),
creator_id: inserted_user.id,
creator_id: inserted_person.id,
post_id: inserted_post.id,
removed: None,
deleted: None,
@ -180,7 +166,7 @@ mod tests {
let comment_like = CommentLikeForm {
comment_id: inserted_comment.id,
post_id: inserted_post.id,
user_id: inserted_user.id,
person_id: inserted_person.id,
score: 1,
};
@ -192,11 +178,11 @@ mod tests {
assert_eq!(1, comment_aggs_before_delete.upvotes);
assert_eq!(0, comment_aggs_before_delete.downvotes);
// Add a post dislike from the other user
// Add a post dislike from the other person
let comment_dislike = CommentLikeForm {
comment_id: inserted_comment.id,
post_id: inserted_post.id,
user_id: another_inserted_user.id,
person_id: another_inserted_person.id,
score: -1,
};
@ -209,7 +195,7 @@ mod tests {
assert_eq!(1, comment_aggs_after_dislike.downvotes);
// Remove the first comment like
CommentLike::remove(&conn, inserted_user.id, inserted_comment.id).unwrap();
CommentLike::remove(&conn, inserted_person.id, inserted_comment.id).unwrap();
let after_like_remove = CommentAggregates::read(&conn, inserted_comment.id).unwrap();
assert_eq!(-1, after_like_remove.score);
assert_eq!(0, after_like_remove.upvotes);
@ -223,8 +209,8 @@ mod tests {
assert!(after_delete.is_err());
// This should delete all the associated rows, and fire triggers
User_::delete(&conn, another_inserted_user.id).unwrap();
let user_num_deleted = User_::delete(&conn, inserted_user.id).unwrap();
assert_eq!(1, user_num_deleted);
Person::delete(&conn, another_inserted_person.id).unwrap();
let person_num_deleted = Person::delete(&conn, inserted_person.id).unwrap();
assert_eq!(1, person_num_deleted);
}
}

View file

@ -1,20 +1,24 @@
use diesel::{result::Error, *};
use lemmy_db_schema::schema::community_aggregates;
use lemmy_db_schema::{schema::community_aggregates, CommunityId};
use serde::Serialize;
#[derive(Queryable, Associations, Identifiable, PartialEq, Debug, Serialize, Clone)]
#[table_name = "community_aggregates"]
pub struct CommunityAggregates {
pub id: i32,
pub community_id: i32,
pub community_id: CommunityId,
pub subscribers: i64,
pub posts: i64,
pub comments: i64,
pub published: chrono::NaiveDateTime,
pub users_active_day: i64,
pub users_active_week: i64,
pub users_active_month: i64,
pub users_active_half_year: i64,
}
impl CommunityAggregates {
pub fn read(conn: &PgConnection, community_id: i32) -> Result<Self, Error> {
pub fn read(conn: &PgConnection, community_id: CommunityId) -> Result<Self, Error> {
community_aggregates::table
.filter(community_aggregates::community_id.eq(community_id))
.first::<Self>(conn)
@ -28,84 +32,67 @@ mod tests {
establish_unpooled_connection,
Crud,
Followable,
ListingType,
SortType,
};
use lemmy_db_schema::source::{
comment::{Comment, CommentForm},
community::{Community, CommunityFollower, CommunityFollowerForm, CommunityForm},
person::{Person, PersonForm},
post::{Post, PostForm},
user::{UserForm, User_},
};
use serial_test::serial;
#[test]
#[serial]
fn test_crud() {
let conn = establish_unpooled_connection();
let new_user = UserForm {
let new_person = PersonForm {
name: "thommy_community_agg".into(),
preferred_username: None,
password_encrypted: "nope".into(),
email: None,
matrix_user_id: None,
avatar: None,
banner: None,
admin: false,
banned: Some(false),
banned: None,
deleted: None,
published: None,
updated: None,
show_nsfw: false,
theme: "browser".into(),
default_sort_type: SortType::Hot as i16,
default_listing_type: ListingType::Subscribed as i16,
lang: "browser".into(),
show_avatars: true,
send_notifications_to_email: false,
actor_id: None,
bio: None,
local: true,
local: None,
private_key: None,
public_key: None,
last_refreshed_at: None,
inbox_url: None,
shared_inbox_url: None,
};
let inserted_user = User_::create(&conn, &new_user).unwrap();
let inserted_person = Person::create(&conn, &new_person).unwrap();
let another_user = UserForm {
let another_person = PersonForm {
name: "jerry_community_agg".into(),
preferred_username: None,
password_encrypted: "nope".into(),
email: None,
matrix_user_id: None,
avatar: None,
banner: None,
admin: false,
banned: Some(false),
banned: None,
deleted: None,
published: None,
updated: None,
show_nsfw: false,
theme: "browser".into(),
default_sort_type: SortType::Hot as i16,
default_listing_type: ListingType::Subscribed as i16,
lang: "browser".into(),
show_avatars: true,
send_notifications_to_email: false,
actor_id: None,
bio: None,
local: true,
local: None,
private_key: None,
public_key: None,
last_refreshed_at: None,
inbox_url: None,
shared_inbox_url: None,
};
let another_inserted_user = User_::create(&conn, &another_user).unwrap();
let another_inserted_person = Person::create(&conn, &another_person).unwrap();
let new_community = CommunityForm {
name: "TIL_community_agg".into(),
creator_id: inserted_user.id,
creator_id: inserted_person.id,
title: "nada".to_owned(),
description: None,
category_id: 1,
nsfw: false,
removed: None,
deleted: None,
@ -118,16 +105,18 @@ mod tests {
published: None,
icon: None,
banner: None,
followers_url: None,
inbox_url: None,
shared_inbox_url: None,
};
let inserted_community = Community::create(&conn, &new_community).unwrap();
let another_community = CommunityForm {
name: "TIL_community_agg_2".into(),
creator_id: inserted_user.id,
creator_id: inserted_person.id,
title: "nada".to_owned(),
description: None,
category_id: 1,
nsfw: false,
removed: None,
deleted: None,
@ -140,29 +129,32 @@ mod tests {
published: None,
icon: None,
banner: None,
followers_url: None,
inbox_url: None,
shared_inbox_url: None,
};
let another_inserted_community = Community::create(&conn, &another_community).unwrap();
let first_user_follow = CommunityFollowerForm {
let first_person_follow = CommunityFollowerForm {
community_id: inserted_community.id,
user_id: inserted_user.id,
person_id: inserted_person.id,
pending: false,
};
CommunityFollower::follow(&conn, &first_user_follow).unwrap();
CommunityFollower::follow(&conn, &first_person_follow).unwrap();
let second_user_follow = CommunityFollowerForm {
let second_person_follow = CommunityFollowerForm {
community_id: inserted_community.id,
user_id: another_inserted_user.id,
person_id: another_inserted_person.id,
pending: false,
};
CommunityFollower::follow(&conn, &second_user_follow).unwrap();
CommunityFollower::follow(&conn, &second_person_follow).unwrap();
let another_community_follow = CommunityFollowerForm {
community_id: another_inserted_community.id,
user_id: inserted_user.id,
person_id: inserted_person.id,
pending: false,
};
@ -172,7 +164,7 @@ mod tests {
name: "A test post".into(),
url: None,
body: None,
creator_id: inserted_user.id,
creator_id: inserted_person.id,
community_id: inserted_community.id,
removed: None,
deleted: None,
@ -193,7 +185,7 @@ mod tests {
let comment_form = CommentForm {
content: "A test comment".into(),
creator_id: inserted_user.id,
creator_id: inserted_person.id,
post_id: inserted_post.id,
removed: None,
deleted: None,
@ -209,7 +201,7 @@ mod tests {
let child_comment_form = CommentForm {
content: "A test comment".into(),
creator_id: inserted_user.id,
creator_id: inserted_person.id,
post_id: inserted_post.id,
removed: None,
deleted: None,
@ -238,12 +230,12 @@ mod tests {
assert_eq!(0, another_community_aggs.comments);
// Unfollow test
CommunityFollower::unfollow(&conn, &second_user_follow).unwrap();
CommunityFollower::unfollow(&conn, &second_person_follow).unwrap();
let after_unfollow = CommunityAggregates::read(&conn, inserted_community.id).unwrap();
assert_eq!(1, after_unfollow.subscribers);
// Follow again just for the later tests
CommunityFollower::follow(&conn, &second_user_follow).unwrap();
CommunityFollower::follow(&conn, &second_person_follow).unwrap();
let after_follow_again = CommunityAggregates::read(&conn, inserted_community.id).unwrap();
assert_eq!(2, after_follow_again.subscribers);
@ -253,14 +245,14 @@ mod tests {
assert_eq!(0, after_parent_post_delete.comments);
assert_eq!(0, after_parent_post_delete.posts);
// Remove the 2nd user
User_::delete(&conn, another_inserted_user.id).unwrap();
let after_user_delete = CommunityAggregates::read(&conn, inserted_community.id).unwrap();
assert_eq!(1, after_user_delete.subscribers);
// Remove the 2nd person
Person::delete(&conn, another_inserted_person.id).unwrap();
let after_person_delete = CommunityAggregates::read(&conn, inserted_community.id).unwrap();
assert_eq!(1, after_person_delete.subscribers);
// This should delete all the associated rows, and fire triggers
let user_num_deleted = User_::delete(&conn, inserted_user.id).unwrap();
assert_eq!(1, user_num_deleted);
let person_num_deleted = Person::delete(&conn, inserted_person.id).unwrap();
assert_eq!(1, person_num_deleted);
// Should be none found, since the creator was deleted
let after_delete = CommunityAggregates::read(&conn, inserted_community.id);

View file

@ -1,5 +1,5 @@
pub mod comment_aggregates;
pub mod community_aggregates;
pub mod person_aggregates;
pub mod post_aggregates;
pub mod site_aggregates;
pub mod user_aggregates;

View file

@ -1,22 +1,22 @@
use diesel::{result::Error, *};
use lemmy_db_schema::schema::user_aggregates;
use lemmy_db_schema::{schema::person_aggregates, PersonId};
use serde::Serialize;
#[derive(Queryable, Associations, Identifiable, PartialEq, Debug, Serialize, Clone)]
#[table_name = "user_aggregates"]
pub struct UserAggregates {
#[table_name = "person_aggregates"]
pub struct PersonAggregates {
pub id: i32,
pub user_id: i32,
pub person_id: PersonId,
pub post_count: i64,
pub post_score: i64,
pub comment_count: i64,
pub comment_score: i64,
}
impl UserAggregates {
pub fn read(conn: &PgConnection, user_id: i32) -> Result<Self, Error> {
user_aggregates::table
.filter(user_aggregates::user_id.eq(user_id))
impl PersonAggregates {
pub fn read(conn: &PgConnection, person_id: PersonId) -> Result<Self, Error> {
person_aggregates::table
.filter(person_aggregates::person_id.eq(person_id))
.first::<Self>(conn)
}
}
@ -24,88 +24,71 @@ impl UserAggregates {
#[cfg(test)]
mod tests {
use crate::{
aggregates::user_aggregates::UserAggregates,
aggregates::person_aggregates::PersonAggregates,
establish_unpooled_connection,
Crud,
Likeable,
ListingType,
SortType,
};
use lemmy_db_schema::source::{
comment::{Comment, CommentForm, CommentLike, CommentLikeForm},
community::{Community, CommunityForm},
person::{Person, PersonForm},
post::{Post, PostForm, PostLike, PostLikeForm},
user::{UserForm, User_},
};
use serial_test::serial;
#[test]
#[serial]
fn test_crud() {
let conn = establish_unpooled_connection();
let new_user = UserForm {
let new_person = PersonForm {
name: "thommy_user_agg".into(),
preferred_username: None,
password_encrypted: "nope".into(),
email: None,
matrix_user_id: None,
avatar: None,
banner: None,
admin: false,
banned: Some(false),
banned: None,
deleted: None,
published: None,
updated: None,
show_nsfw: false,
theme: "browser".into(),
default_sort_type: SortType::Hot as i16,
default_listing_type: ListingType::Subscribed as i16,
lang: "browser".into(),
show_avatars: true,
send_notifications_to_email: false,
actor_id: None,
bio: None,
local: true,
local: None,
private_key: None,
public_key: None,
last_refreshed_at: None,
inbox_url: None,
shared_inbox_url: None,
};
let inserted_user = User_::create(&conn, &new_user).unwrap();
let inserted_person = Person::create(&conn, &new_person).unwrap();
let another_user = UserForm {
let another_person = PersonForm {
name: "jerry_user_agg".into(),
preferred_username: None,
password_encrypted: "nope".into(),
email: None,
matrix_user_id: None,
avatar: None,
banner: None,
admin: false,
banned: Some(false),
banned: None,
deleted: None,
published: None,
updated: None,
show_nsfw: false,
theme: "browser".into(),
default_sort_type: SortType::Hot as i16,
default_listing_type: ListingType::Subscribed as i16,
lang: "browser".into(),
show_avatars: true,
send_notifications_to_email: false,
actor_id: None,
bio: None,
local: true,
local: None,
private_key: None,
public_key: None,
last_refreshed_at: None,
inbox_url: None,
shared_inbox_url: None,
};
let another_inserted_user = User_::create(&conn, &another_user).unwrap();
let another_inserted_person = Person::create(&conn, &another_person).unwrap();
let new_community = CommunityForm {
name: "TIL_site_agg".into(),
creator_id: inserted_user.id,
creator_id: inserted_person.id,
title: "nada".to_owned(),
description: None,
category_id: 1,
nsfw: false,
removed: None,
deleted: None,
@ -118,6 +101,9 @@ mod tests {
published: None,
icon: None,
banner: None,
followers_url: None,
inbox_url: None,
shared_inbox_url: None,
};
let inserted_community = Community::create(&conn, &new_community).unwrap();
@ -126,7 +112,7 @@ mod tests {
name: "A test post".into(),
url: None,
body: None,
creator_id: inserted_user.id,
creator_id: inserted_person.id,
community_id: inserted_community.id,
removed: None,
deleted: None,
@ -147,7 +133,7 @@ mod tests {
let post_like = PostLikeForm {
post_id: inserted_post.id,
user_id: inserted_user.id,
person_id: inserted_person.id,
score: 1,
};
@ -155,7 +141,7 @@ mod tests {
let comment_form = CommentForm {
content: "A test comment".into(),
creator_id: inserted_user.id,
creator_id: inserted_person.id,
post_id: inserted_post.id,
removed: None,
deleted: None,
@ -171,7 +157,7 @@ mod tests {
let mut comment_like = CommentLikeForm {
comment_id: inserted_comment.id,
user_id: inserted_user.id,
person_id: inserted_person.id,
post_id: inserted_post.id,
score: 1,
};
@ -180,7 +166,7 @@ mod tests {
let mut child_comment_form = CommentForm {
content: "A test comment".into(),
creator_id: inserted_user.id,
creator_id: inserted_person.id,
post_id: inserted_post.id,
removed: None,
deleted: None,
@ -196,28 +182,29 @@ mod tests {
let child_comment_like = CommentLikeForm {
comment_id: inserted_child_comment.id,
user_id: another_inserted_user.id,
person_id: another_inserted_person.id,
post_id: inserted_post.id,
score: 1,
};
let _inserted_child_comment_like = CommentLike::like(&conn, &child_comment_like).unwrap();
let user_aggregates_before_delete = UserAggregates::read(&conn, inserted_user.id).unwrap();
let person_aggregates_before_delete =
PersonAggregates::read(&conn, inserted_person.id).unwrap();
assert_eq!(1, user_aggregates_before_delete.post_count);
assert_eq!(1, user_aggregates_before_delete.post_score);
assert_eq!(2, user_aggregates_before_delete.comment_count);
assert_eq!(2, user_aggregates_before_delete.comment_score);
assert_eq!(1, person_aggregates_before_delete.post_count);
assert_eq!(1, person_aggregates_before_delete.post_score);
assert_eq!(2, person_aggregates_before_delete.comment_count);
assert_eq!(2, person_aggregates_before_delete.comment_score);
// Remove a post like
PostLike::remove(&conn, inserted_user.id, inserted_post.id).unwrap();
let after_post_like_remove = UserAggregates::read(&conn, inserted_user.id).unwrap();
PostLike::remove(&conn, inserted_person.id, inserted_post.id).unwrap();
let after_post_like_remove = PersonAggregates::read(&conn, inserted_person.id).unwrap();
assert_eq!(0, after_post_like_remove.post_score);
// Remove a parent comment (the scores should also be removed)
Comment::delete(&conn, inserted_comment.id).unwrap();
let after_parent_comment_delete = UserAggregates::read(&conn, inserted_user.id).unwrap();
let after_parent_comment_delete = PersonAggregates::read(&conn, inserted_person.id).unwrap();
assert_eq!(0, after_parent_comment_delete.comment_count);
assert_eq!(0, after_parent_comment_delete.comment_score);
@ -227,24 +214,24 @@ mod tests {
Comment::create(&conn, &child_comment_form).unwrap();
comment_like.comment_id = new_parent_comment.id;
CommentLike::like(&conn, &comment_like).unwrap();
let after_comment_add = UserAggregates::read(&conn, inserted_user.id).unwrap();
let after_comment_add = PersonAggregates::read(&conn, inserted_person.id).unwrap();
assert_eq!(2, after_comment_add.comment_count);
assert_eq!(1, after_comment_add.comment_score);
Post::delete(&conn, inserted_post.id).unwrap();
let after_post_delete = UserAggregates::read(&conn, inserted_user.id).unwrap();
let after_post_delete = PersonAggregates::read(&conn, inserted_person.id).unwrap();
assert_eq!(0, after_post_delete.comment_score);
assert_eq!(0, after_post_delete.comment_count);
assert_eq!(0, after_post_delete.post_score);
assert_eq!(0, after_post_delete.post_count);
// This should delete all the associated rows, and fire triggers
let user_num_deleted = User_::delete(&conn, inserted_user.id).unwrap();
assert_eq!(1, user_num_deleted);
User_::delete(&conn, another_inserted_user.id).unwrap();
let person_num_deleted = Person::delete(&conn, inserted_person.id).unwrap();
assert_eq!(1, person_num_deleted);
Person::delete(&conn, another_inserted_person.id).unwrap();
// Should be none found
let after_delete = UserAggregates::read(&conn, inserted_user.id);
let after_delete = PersonAggregates::read(&conn, inserted_person.id);
assert!(after_delete.is_err());
}
}

View file

@ -1,23 +1,24 @@
use diesel::{result::Error, *};
use lemmy_db_schema::schema::post_aggregates;
use lemmy_db_schema::{schema::post_aggregates, PostId};
use serde::Serialize;
#[derive(Queryable, Associations, Identifiable, PartialEq, Debug, Serialize, Clone)]
#[table_name = "post_aggregates"]
pub struct PostAggregates {
pub id: i32,
pub post_id: i32,
pub post_id: PostId,
pub comments: i64,
pub score: i64,
pub upvotes: i64,
pub downvotes: i64,
pub stickied: bool,
pub published: chrono::NaiveDateTime,
pub newest_comment_time_necro: chrono::NaiveDateTime, // A newest comment time, limited to 2 days, to prevent necrobumping
pub newest_comment_time: chrono::NaiveDateTime,
}
impl PostAggregates {
pub fn read(conn: &PgConnection, post_id: i32) -> Result<Self, Error> {
pub fn read(conn: &PgConnection, post_id: PostId) -> Result<Self, Error> {
post_aggregates::table
.filter(post_aggregates::post_id.eq(post_id))
.first::<Self>(conn)
@ -31,84 +32,67 @@ mod tests {
establish_unpooled_connection,
Crud,
Likeable,
ListingType,
SortType,
};
use lemmy_db_schema::source::{
comment::{Comment, CommentForm},
community::{Community, CommunityForm},
person::{Person, PersonForm},
post::{Post, PostForm, PostLike, PostLikeForm},
user::{UserForm, User_},
};
use serial_test::serial;
#[test]
#[serial]
fn test_crud() {
let conn = establish_unpooled_connection();
let new_user = UserForm {
let new_person = PersonForm {
name: "thommy_community_agg".into(),
preferred_username: None,
password_encrypted: "nope".into(),
email: None,
matrix_user_id: None,
avatar: None,
banner: None,
admin: false,
banned: Some(false),
banned: None,
deleted: None,
published: None,
updated: None,
show_nsfw: false,
theme: "browser".into(),
default_sort_type: SortType::Hot as i16,
default_listing_type: ListingType::Subscribed as i16,
lang: "browser".into(),
show_avatars: true,
send_notifications_to_email: false,
actor_id: None,
bio: None,
local: true,
local: None,
private_key: None,
public_key: None,
last_refreshed_at: None,
inbox_url: None,
shared_inbox_url: None,
};
let inserted_user = User_::create(&conn, &new_user).unwrap();
let inserted_person = Person::create(&conn, &new_person).unwrap();
let another_user = UserForm {
let another_person = PersonForm {
name: "jerry_community_agg".into(),
preferred_username: None,
password_encrypted: "nope".into(),
email: None,
matrix_user_id: None,
avatar: None,
banner: None,
admin: false,
banned: Some(false),
banned: None,
deleted: None,
published: None,
updated: None,
show_nsfw: false,
theme: "browser".into(),
default_sort_type: SortType::Hot as i16,
default_listing_type: ListingType::Subscribed as i16,
lang: "browser".into(),
show_avatars: true,
send_notifications_to_email: false,
actor_id: None,
bio: None,
local: true,
local: None,
private_key: None,
public_key: None,
last_refreshed_at: None,
inbox_url: None,
shared_inbox_url: None,
};
let another_inserted_user = User_::create(&conn, &another_user).unwrap();
let another_inserted_person = Person::create(&conn, &another_person).unwrap();
let new_community = CommunityForm {
name: "TIL_community_agg".into(),
creator_id: inserted_user.id,
creator_id: inserted_person.id,
title: "nada".to_owned(),
description: None,
category_id: 1,
nsfw: false,
removed: None,
deleted: None,
@ -121,6 +105,9 @@ mod tests {
published: None,
icon: None,
banner: None,
followers_url: None,
inbox_url: None,
shared_inbox_url: None,
};
let inserted_community = Community::create(&conn, &new_community).unwrap();
@ -129,7 +116,7 @@ mod tests {
name: "A test post".into(),
url: None,
body: None,
creator_id: inserted_user.id,
creator_id: inserted_person.id,
community_id: inserted_community.id,
removed: None,
deleted: None,
@ -150,7 +137,7 @@ mod tests {
let comment_form = CommentForm {
content: "A test comment".into(),
creator_id: inserted_user.id,
creator_id: inserted_person.id,
post_id: inserted_post.id,
removed: None,
deleted: None,
@ -166,7 +153,7 @@ mod tests {
let child_comment_form = CommentForm {
content: "A test comment".into(),
creator_id: inserted_user.id,
creator_id: inserted_person.id,
post_id: inserted_post.id,
removed: None,
deleted: None,
@ -182,7 +169,7 @@ mod tests {
let post_like = PostLikeForm {
post_id: inserted_post.id,
user_id: inserted_user.id,
person_id: inserted_person.id,
score: 1,
};
@ -195,10 +182,10 @@ mod tests {
assert_eq!(1, post_aggs_before_delete.upvotes);
assert_eq!(0, post_aggs_before_delete.downvotes);
// Add a post dislike from the other user
// Add a post dislike from the other person
let post_dislike = PostLikeForm {
post_id: inserted_post.id,
user_id: another_inserted_user.id,
person_id: another_inserted_person.id,
score: -1,
};
@ -220,7 +207,7 @@ mod tests {
assert_eq!(1, after_comment_delete.downvotes);
// Remove the first post like
PostLike::remove(&conn, inserted_user.id, inserted_post.id).unwrap();
PostLike::remove(&conn, inserted_person.id, inserted_post.id).unwrap();
let after_like_remove = PostAggregates::read(&conn, inserted_post.id).unwrap();
assert_eq!(0, after_like_remove.comments);
assert_eq!(-1, after_like_remove.score);
@ -228,9 +215,9 @@ mod tests {
assert_eq!(1, after_like_remove.downvotes);
// This should delete all the associated rows, and fire triggers
User_::delete(&conn, another_inserted_user.id).unwrap();
let user_num_deleted = User_::delete(&conn, inserted_user.id).unwrap();
assert_eq!(1, user_num_deleted);
Person::delete(&conn, another_inserted_person.id).unwrap();
let person_num_deleted = Person::delete(&conn, inserted_person.id).unwrap();
assert_eq!(1, person_num_deleted);
// Should be none found, since the creator was deleted
let after_delete = PostAggregates::read(&conn, inserted_post.id);

View file

@ -11,6 +11,10 @@ pub struct SiteAggregates {
pub posts: i64,
pub comments: i64,
pub communities: i64,
pub users_active_day: i64,
pub users_active_week: i64,
pub users_active_month: i64,
pub users_active_half_year: i64,
}
impl SiteAggregates {
@ -21,60 +25,48 @@ impl SiteAggregates {
#[cfg(test)]
mod tests {
use crate::{
aggregates::site_aggregates::SiteAggregates,
establish_unpooled_connection,
Crud,
ListingType,
SortType,
};
use crate::{aggregates::site_aggregates::SiteAggregates, establish_unpooled_connection, Crud};
use lemmy_db_schema::source::{
comment::{Comment, CommentForm},
community::{Community, CommunityForm},
person::{Person, PersonForm},
post::{Post, PostForm},
site::{Site, SiteForm},
user::{UserForm, User_},
};
use serial_test::serial;
#[test]
#[serial]
fn test_crud() {
let conn = establish_unpooled_connection();
let new_user = UserForm {
let new_person = PersonForm {
name: "thommy_site_agg".into(),
preferred_username: None,
password_encrypted: "nope".into(),
email: None,
matrix_user_id: None,
avatar: None,
banner: None,
admin: false,
banned: Some(false),
banned: None,
deleted: None,
published: None,
updated: None,
show_nsfw: false,
theme: "browser".into(),
default_sort_type: SortType::Hot as i16,
default_listing_type: ListingType::Subscribed as i16,
lang: "browser".into(),
show_avatars: true,
send_notifications_to_email: false,
actor_id: None,
bio: None,
local: true,
local: None,
private_key: None,
public_key: None,
last_refreshed_at: None,
inbox_url: None,
shared_inbox_url: None,
};
let inserted_user = User_::create(&conn, &new_user).unwrap();
let inserted_person = Person::create(&conn, &new_person).unwrap();
let site_form = SiteForm {
name: "test_site".into(),
description: None,
icon: None,
banner: None,
creator_id: inserted_user.id,
creator_id: inserted_person.id,
enable_downvotes: true,
open_registration: true,
enable_nsfw: true,
@ -85,10 +77,9 @@ mod tests {
let new_community = CommunityForm {
name: "TIL_site_agg".into(),
creator_id: inserted_user.id,
creator_id: inserted_person.id,
title: "nada".to_owned(),
description: None,
category_id: 1,
nsfw: false,
removed: None,
deleted: None,
@ -101,6 +92,9 @@ mod tests {
published: None,
icon: None,
banner: None,
followers_url: None,
inbox_url: None,
shared_inbox_url: None,
};
let inserted_community = Community::create(&conn, &new_community).unwrap();
@ -109,7 +103,7 @@ mod tests {
name: "A test post".into(),
url: None,
body: None,
creator_id: inserted_user.id,
creator_id: inserted_person.id,
community_id: inserted_community.id,
removed: None,
deleted: None,
@ -132,7 +126,7 @@ mod tests {
let comment_form = CommentForm {
content: "A test comment".into(),
creator_id: inserted_user.id,
creator_id: inserted_person.id,
post_id: inserted_post.id,
removed: None,
deleted: None,
@ -149,7 +143,7 @@ mod tests {
let child_comment_form = CommentForm {
content: "A test comment".into(),
creator_id: inserted_user.id,
creator_id: inserted_person.id,
post_id: inserted_post.id,
removed: None,
deleted: None,
@ -177,8 +171,8 @@ mod tests {
assert_eq!(0, site_aggregates_after_post_delete.comments);
// This shouuld delete all the associated rows, and fire triggers
let user_num_deleted = User_::delete(&conn, inserted_user.id).unwrap();
assert_eq!(1, user_num_deleted);
let person_num_deleted = Person::delete(&conn, inserted_person.id).unwrap();
assert_eq!(1, person_num_deleted);
let after_delete = SiteAggregates::read(&conn);
assert!(after_delete.is_err());

View file

@ -9,27 +9,33 @@ extern crate lazy_static;
#[macro_use]
extern crate diesel_migrations;
#[cfg(test)]
extern crate serial_test;
use diesel::{result::Error, *};
use lemmy_db_schema::{CommunityId, DbUrl, PersonId};
use lemmy_utils::ApiError;
use regex::Regex;
use serde::{Deserialize, Serialize};
use std::{env, env::VarError};
use url::Url;
pub mod aggregates;
pub mod source;
pub type DbPool = diesel::r2d2::Pool<diesel::r2d2::ConnectionManager<diesel::PgConnection>>;
pub trait Crud<T> {
fn create(conn: &PgConnection, form: &T) -> Result<Self, Error>
pub trait Crud<Form, IdType> {
fn create(conn: &PgConnection, form: &Form) -> Result<Self, Error>
where
Self: Sized;
fn read(conn: &PgConnection, id: i32) -> Result<Self, Error>
fn read(conn: &PgConnection, id: IdType) -> Result<Self, Error>
where
Self: Sized;
fn update(conn: &PgConnection, id: i32, form: &T) -> Result<Self, Error>
fn update(conn: &PgConnection, id: IdType, form: &Form) -> Result<Self, Error>
where
Self: Sized;
fn delete(_conn: &PgConnection, _id: i32) -> Result<usize, Error>
fn delete(_conn: &PgConnection, _id: IdType) -> Result<usize, Error>
where
Self: Sized,
{
@ -37,81 +43,85 @@ pub trait Crud<T> {
}
}
pub trait Followable<T> {
fn follow(conn: &PgConnection, form: &T) -> Result<Self, Error>
pub trait Followable<Form> {
fn follow(conn: &PgConnection, form: &Form) -> Result<Self, Error>
where
Self: Sized;
fn follow_accepted(conn: &PgConnection, community_id: i32, user_id: i32) -> Result<Self, Error>
fn follow_accepted(
conn: &PgConnection,
community_id: CommunityId,
person_id: PersonId,
) -> Result<Self, Error>
where
Self: Sized;
fn unfollow(conn: &PgConnection, form: &T) -> Result<usize, Error>
fn unfollow(conn: &PgConnection, form: &Form) -> Result<usize, Error>
where
Self: Sized;
fn has_local_followers(conn: &PgConnection, community_id: i32) -> Result<bool, Error>;
fn has_local_followers(conn: &PgConnection, community_id: CommunityId) -> Result<bool, Error>;
}
pub trait Joinable<T> {
fn join(conn: &PgConnection, form: &T) -> Result<Self, Error>
pub trait Joinable<Form> {
fn join(conn: &PgConnection, form: &Form) -> Result<Self, Error>
where
Self: Sized;
fn leave(conn: &PgConnection, form: &T) -> Result<usize, Error>
fn leave(conn: &PgConnection, form: &Form) -> Result<usize, Error>
where
Self: Sized;
}
pub trait Likeable<T> {
fn like(conn: &PgConnection, form: &T) -> Result<Self, Error>
pub trait Likeable<Form, IdType> {
fn like(conn: &PgConnection, form: &Form) -> Result<Self, Error>
where
Self: Sized;
fn remove(conn: &PgConnection, user_id: i32, item_id: i32) -> Result<usize, Error>
fn remove(conn: &PgConnection, person_id: PersonId, item_id: IdType) -> Result<usize, Error>
where
Self: Sized;
}
pub trait Bannable<T> {
fn ban(conn: &PgConnection, form: &T) -> Result<Self, Error>
pub trait Bannable<Form> {
fn ban(conn: &PgConnection, form: &Form) -> Result<Self, Error>
where
Self: Sized;
fn unban(conn: &PgConnection, form: &T) -> Result<usize, Error>
fn unban(conn: &PgConnection, form: &Form) -> Result<usize, Error>
where
Self: Sized;
}
pub trait Saveable<T> {
fn save(conn: &PgConnection, form: &T) -> Result<Self, Error>
pub trait Saveable<Form> {
fn save(conn: &PgConnection, form: &Form) -> Result<Self, Error>
where
Self: Sized;
fn unsave(conn: &PgConnection, form: &T) -> Result<usize, Error>
fn unsave(conn: &PgConnection, form: &Form) -> Result<usize, Error>
where
Self: Sized;
}
pub trait Readable<T> {
fn mark_as_read(conn: &PgConnection, form: &T) -> Result<Self, Error>
pub trait Readable<Form> {
fn mark_as_read(conn: &PgConnection, form: &Form) -> Result<Self, Error>
where
Self: Sized;
fn mark_as_unread(conn: &PgConnection, form: &T) -> Result<usize, Error>
fn mark_as_unread(conn: &PgConnection, form: &Form) -> Result<usize, Error>
where
Self: Sized;
}
pub trait Reportable<T> {
fn report(conn: &PgConnection, form: &T) -> Result<Self, Error>
pub trait Reportable<Form> {
fn report(conn: &PgConnection, form: &Form) -> Result<Self, Error>
where
Self: Sized;
fn resolve(conn: &PgConnection, report_id: i32, resolver_id: i32) -> Result<usize, Error>
fn resolve(conn: &PgConnection, report_id: i32, resolver_id: PersonId) -> Result<usize, Error>
where
Self: Sized;
fn unresolve(conn: &PgConnection, report_id: i32, resolver_id: i32) -> Result<usize, Error>
fn unresolve(conn: &PgConnection, report_id: i32, resolver_id: PersonId) -> Result<usize, Error>
where
Self: Sized;
}
pub trait ApubObject<T> {
fn read_from_apub_id(conn: &PgConnection, object_id: &str) -> Result<Self, Error>
pub trait ApubObject<Form> {
fn read_from_apub_id(conn: &PgConnection, object_id: &DbUrl) -> Result<Self, Error>
where
Self: Sized;
fn upsert(conn: &PgConnection, user_form: &T) -> Result<Self, Error>
fn upsert(conn: &PgConnection, user_form: &Form) -> Result<Self, Error>
where
Self: Sized;
}
@ -137,6 +147,11 @@ pub trait ToSafe {
fn safe_columns_tuple() -> Self::SafeColumns;
}
pub trait ToSafeSettings {
type SafeSettingsColumns;
fn safe_settings_columns_tuple() -> Self::SafeSettingsColumns;
}
pub trait ViewToVec {
type DbTuple;
fn from_tuple_to_vec(tuple: Vec<Self::DbTuple>) -> Vec<Self>
@ -158,6 +173,8 @@ pub enum SortType {
TopMonth,
TopYear,
TopAll,
MostComments,
NewComments,
}
#[derive(EnumString, ToString, Debug, Serialize, Deserialize, Clone)]
@ -208,6 +225,20 @@ pub fn diesel_option_overwrite(opt: &Option<String>) -> Option<Option<String>> {
}
}
pub fn diesel_option_overwrite_to_url(
opt: &Option<String>,
) -> Result<Option<Option<DbUrl>>, ApiError> {
match opt.as_ref().map(|s| s.as_str()) {
// An empty string is an erase
Some("") => Ok(Some(None)),
Some(str_url) => match Url::parse(str_url) {
Ok(url) => Ok(Some(Some(url.into()))),
Err(_) => Err(ApiError::err("invalid_url")),
},
None => Ok(None),
}
}
embed_migrations!();
pub fn establish_unpooled_connection() -> PgConnection {
@ -220,13 +251,14 @@ pub fn establish_unpooled_connection() -> PgConnection {
};
let conn =
PgConnection::establish(&db_url).unwrap_or_else(|_| panic!("Error connecting to {}", db_url));
embedded_migrations::run(&conn).unwrap();
embedded_migrations::run(&conn).expect("load migrations");
conn
}
lazy_static! {
static ref EMAIL_REGEX: Regex =
Regex::new(r"^[a-zA-Z0-9.!#$%&*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$").unwrap();
Regex::new(r"^[a-zA-Z0-9.!#$%&*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$")
.expect("compile email regex");
}
pub mod functions {
@ -239,7 +271,7 @@ pub mod functions {
#[cfg(test)]
mod tests {
use super::fuzzy_search;
use super::{fuzzy_search, *};
use crate::is_email_regex;
#[test]
@ -253,4 +285,32 @@ mod tests {
assert!(is_email_regex("gush@gmail.com"));
assert!(!is_email_regex("nada_neutho"));
}
#[test]
fn test_diesel_option_overwrite() {
assert_eq!(diesel_option_overwrite(&None), None);
assert_eq!(diesel_option_overwrite(&Some("".to_string())), Some(None));
assert_eq!(
diesel_option_overwrite(&Some("test".to_string())),
Some(Some("test".to_string()))
);
}
#[test]
fn test_diesel_option_overwrite_to_url() {
assert!(matches!(diesel_option_overwrite_to_url(&None), Ok(None)));
assert!(matches!(
diesel_option_overwrite_to_url(&Some("".to_string())),
Ok(Some(None))
));
assert!(matches!(
diesel_option_overwrite_to_url(&Some("invalid_url".to_string())),
Err(_)
));
let example_url = "https://example.com";
assert!(matches!(
diesel_option_overwrite_to_url(&Some(example_url.to_string())),
Ok(Some(Some(url))) if url == Url::parse(&example_url).unwrap().into()
));
}
}

View file

@ -1,14 +1,15 @@
use crate::Crud;
use diesel::{dsl::*, result::Error, *};
use lemmy_db_schema::source::activity::*;
use diesel::{dsl::*, result::Error, sql_types::Text, *};
use lemmy_db_schema::{source::activity::*, DbUrl};
use log::debug;
use serde::Serialize;
use serde_json::Value;
use std::{
fmt::Debug,
io::{Error as IoError, ErrorKind},
};
impl Crud<ActivityForm> for Activity {
impl Crud<ActivityForm, i32> for Activity {
fn read(conn: &PgConnection, activity_id: i32) -> Result<Self, Error> {
use lemmy_db_schema::schema::activity::dsl::*;
activity.find(activity_id).first::<Self>(conn)
@ -40,20 +41,28 @@ impl Crud<ActivityForm> for Activity {
pub trait Activity_ {
fn insert<T>(
conn: &PgConnection,
ap_id: String,
ap_id: DbUrl,
data: &T,
local: bool,
sensitive: bool,
) -> Result<Activity, IoError>
where
T: Serialize + Debug;
fn read_from_apub_id(conn: &PgConnection, object_id: &str) -> Result<Activity, Error>;
fn read_from_apub_id(conn: &PgConnection, object_id: &DbUrl) -> Result<Activity, Error>;
fn delete_olds(conn: &PgConnection) -> Result<usize, Error>;
/// Returns up to 20 activities of type `Announce/Create/Page` from the community
fn read_community_outbox(
conn: &PgConnection,
community_actor_id: &DbUrl,
) -> Result<Vec<Value>, Error>;
}
impl Activity_ for Activity {
fn insert<T>(
conn: &PgConnection,
ap_id: String,
ap_id: DbUrl,
data: &T,
local: bool,
sensitive: bool,
@ -79,62 +88,80 @@ impl Activity_ for Activity {
}
}
fn read_from_apub_id(conn: &PgConnection, object_id: &str) -> Result<Activity, Error> {
fn read_from_apub_id(conn: &PgConnection, object_id: &DbUrl) -> Result<Activity, Error> {
use lemmy_db_schema::schema::activity::dsl::*;
activity.filter(ap_id.eq(object_id)).first::<Self>(conn)
}
fn delete_olds(conn: &PgConnection) -> Result<usize, Error> {
use lemmy_db_schema::schema::activity::dsl::*;
diesel::delete(activity.filter(published.lt(now - 6.months()))).execute(conn)
}
fn read_community_outbox(
conn: &PgConnection,
community_actor_id: &DbUrl,
) -> Result<Vec<Value>, Error> {
use lemmy_db_schema::schema::activity::dsl::*;
let res: Vec<Value> = activity
.select(data)
.filter(
sql("activity.data ->> 'type' = 'Announce'")
.sql(" AND activity.data -> 'object' ->> 'type' = 'Create'")
.sql(" AND activity.data -> 'object' -> 'object' ->> 'type' = 'Page'")
.sql(" AND activity.data ->> 'actor' = ")
.bind::<Text, _>(community_actor_id)
.sql(" ORDER BY activity.published DESC"),
)
.limit(20)
.get_results(conn)?;
Ok(res)
}
}
#[cfg(test)]
mod tests {
use crate::{
establish_unpooled_connection,
source::activity::Activity_,
Crud,
ListingType,
SortType,
};
use super::*;
use crate::{establish_unpooled_connection, source::activity::Activity_};
use lemmy_db_schema::source::{
activity::{Activity, ActivityForm},
user::{UserForm, User_},
person::{Person, PersonForm},
};
use serde_json::Value;
use serial_test::serial;
use url::Url;
#[test]
#[serial]
fn test_crud() {
let conn = establish_unpooled_connection();
let creator_form = UserForm {
let creator_form = PersonForm {
name: "activity_creator_pm".into(),
preferred_username: None,
password_encrypted: "nope".into(),
email: None,
matrix_user_id: None,
avatar: None,
banner: None,
admin: false,
banned: Some(false),
banned: None,
deleted: None,
published: None,
updated: None,
show_nsfw: false,
theme: "browser".into(),
default_sort_type: SortType::Hot as i16,
default_listing_type: ListingType::Subscribed as i16,
lang: "browser".into(),
show_avatars: true,
send_notifications_to_email: false,
actor_id: None,
bio: None,
local: true,
local: None,
private_key: None,
public_key: None,
last_refreshed_at: None,
inbox_url: None,
shared_inbox_url: None,
};
let inserted_creator = User_::create(&conn, &creator_form).unwrap();
let inserted_creator = Person::create(&conn, &creator_form).unwrap();
let ap_id =
"https://enterprise.lemmy.ml/activities/delete/f1b5d57c-80f8-4e03-a615-688d552e946c";
let ap_id: DbUrl = Url::parse(
"https://enterprise.lemmy.ml/activities/delete/f1b5d57c-80f8-4e03-a615-688d552e946c",
)
.unwrap()
.into();
let test_json: Value = serde_json::from_str(
r#"{
"@context": "https://www.w3.org/ns/activitystreams",
@ -150,7 +177,7 @@ mod tests {
)
.unwrap();
let activity_form = ActivityForm {
ap_id: ap_id.to_string(),
ap_id: ap_id.clone(),
data: test_json.to_owned(),
local: true,
sensitive: false,
@ -160,7 +187,7 @@ mod tests {
let inserted_activity = Activity::create(&conn, &activity_form).unwrap();
let expected_activity = Activity {
ap_id: Some(ap_id.to_string()),
ap_id: Some(ap_id.clone()),
id: inserted_activity.id,
data: test_json,
local: true,
@ -170,8 +197,8 @@ mod tests {
};
let read_activity = Activity::read(&conn, inserted_activity.id).unwrap();
let read_activity_by_apub_id = Activity::read_from_apub_id(&conn, ap_id).unwrap();
User_::delete(&conn, inserted_creator.id).unwrap();
let read_activity_by_apub_id = Activity::read_from_apub_id(&conn, &ap_id).unwrap();
Person::delete(&conn, inserted_creator.id).unwrap();
Activity::delete(&conn, inserted_activity.id).unwrap();
assert_eq!(expected_activity, read_activity);

View file

@ -10,39 +10,54 @@ use lemmy_db_schema::{
CommentSaved,
CommentSavedForm,
},
CommentId,
DbUrl,
PersonId,
};
pub trait Comment_ {
fn update_ap_id(conn: &PgConnection, comment_id: i32, apub_id: String) -> Result<Comment, Error>;
fn update_ap_id(
conn: &PgConnection,
comment_id: CommentId,
apub_id: DbUrl,
) -> Result<Comment, Error>;
fn permadelete_for_creator(
conn: &PgConnection,
for_creator_id: i32,
for_creator_id: PersonId,
) -> Result<Vec<Comment>, Error>;
fn update_deleted(
conn: &PgConnection,
comment_id: i32,
comment_id: CommentId,
new_deleted: bool,
) -> Result<Comment, Error>;
fn update_removed(
conn: &PgConnection,
comment_id: i32,
comment_id: CommentId,
new_removed: bool,
) -> Result<Comment, Error>;
fn update_removed_for_creator(
conn: &PgConnection,
for_creator_id: i32,
for_creator_id: PersonId,
new_removed: bool,
) -> Result<Vec<Comment>, Error>;
fn update_read(conn: &PgConnection, comment_id: i32, new_read: bool) -> Result<Comment, Error>;
fn update_read(
conn: &PgConnection,
comment_id: CommentId,
new_read: bool,
) -> Result<Comment, Error>;
fn update_content(
conn: &PgConnection,
comment_id: i32,
comment_id: CommentId,
new_content: &str,
) -> Result<Comment, Error>;
}
impl Comment_ for Comment {
fn update_ap_id(conn: &PgConnection, comment_id: i32, apub_id: String) -> Result<Self, Error> {
fn update_ap_id(
conn: &PgConnection,
comment_id: CommentId,
apub_id: DbUrl,
) -> Result<Self, Error> {
use lemmy_db_schema::schema::comment::dsl::*;
diesel::update(comment.find(comment_id))
@ -50,7 +65,10 @@ impl Comment_ for Comment {
.get_result::<Self>(conn)
}
fn permadelete_for_creator(conn: &PgConnection, for_creator_id: i32) -> Result<Vec<Self>, Error> {
fn permadelete_for_creator(
conn: &PgConnection,
for_creator_id: PersonId,
) -> Result<Vec<Self>, Error> {
use lemmy_db_schema::schema::comment::dsl::*;
diesel::update(comment.filter(creator_id.eq(for_creator_id)))
.set((
@ -63,7 +81,7 @@ impl Comment_ for Comment {
fn update_deleted(
conn: &PgConnection,
comment_id: i32,
comment_id: CommentId,
new_deleted: bool,
) -> Result<Self, Error> {
use lemmy_db_schema::schema::comment::dsl::*;
@ -74,7 +92,7 @@ impl Comment_ for Comment {
fn update_removed(
conn: &PgConnection,
comment_id: i32,
comment_id: CommentId,
new_removed: bool,
) -> Result<Self, Error> {
use lemmy_db_schema::schema::comment::dsl::*;
@ -85,7 +103,7 @@ impl Comment_ for Comment {
fn update_removed_for_creator(
conn: &PgConnection,
for_creator_id: i32,
for_creator_id: PersonId,
new_removed: bool,
) -> Result<Vec<Self>, Error> {
use lemmy_db_schema::schema::comment::dsl::*;
@ -94,7 +112,11 @@ impl Comment_ for Comment {
.get_results::<Self>(conn)
}
fn update_read(conn: &PgConnection, comment_id: i32, new_read: bool) -> Result<Self, Error> {
fn update_read(
conn: &PgConnection,
comment_id: CommentId,
new_read: bool,
) -> Result<Self, Error> {
use lemmy_db_schema::schema::comment::dsl::*;
diesel::update(comment.find(comment_id))
.set(read.eq(new_read))
@ -103,7 +125,7 @@ impl Comment_ for Comment {
fn update_content(
conn: &PgConnection,
comment_id: i32,
comment_id: CommentId,
new_content: &str,
) -> Result<Self, Error> {
use lemmy_db_schema::schema::comment::dsl::*;
@ -113,13 +135,13 @@ impl Comment_ for Comment {
}
}
impl Crud<CommentForm> for Comment {
fn read(conn: &PgConnection, comment_id: i32) -> Result<Self, Error> {
impl Crud<CommentForm, CommentId> for Comment {
fn read(conn: &PgConnection, comment_id: CommentId) -> Result<Self, Error> {
use lemmy_db_schema::schema::comment::dsl::*;
comment.find(comment_id).first::<Self>(conn)
}
fn delete(conn: &PgConnection, comment_id: i32) -> Result<usize, Error> {
fn delete(conn: &PgConnection, comment_id: CommentId) -> Result<usize, Error> {
use lemmy_db_schema::schema::comment::dsl::*;
diesel::delete(comment.find(comment_id)).execute(conn)
}
@ -133,7 +155,7 @@ impl Crud<CommentForm> for Comment {
fn update(
conn: &PgConnection,
comment_id: i32,
comment_id: CommentId,
comment_form: &CommentForm,
) -> Result<Self, Error> {
use lemmy_db_schema::schema::comment::dsl::*;
@ -144,7 +166,7 @@ impl Crud<CommentForm> for Comment {
}
impl ApubObject<CommentForm> for Comment {
fn read_from_apub_id(conn: &PgConnection, object_id: &str) -> Result<Self, Error> {
fn read_from_apub_id(conn: &PgConnection, object_id: &DbUrl) -> Result<Self, Error> {
use lemmy_db_schema::schema::comment::dsl::*;
comment.filter(ap_id.eq(object_id)).first::<Self>(conn)
}
@ -160,22 +182,26 @@ impl ApubObject<CommentForm> for Comment {
}
}
impl Likeable<CommentLikeForm> for CommentLike {
impl Likeable<CommentLikeForm, CommentId> for CommentLike {
fn like(conn: &PgConnection, comment_like_form: &CommentLikeForm) -> Result<Self, Error> {
use lemmy_db_schema::schema::comment_like::dsl::*;
insert_into(comment_like)
.values(comment_like_form)
.on_conflict((comment_id, user_id))
.on_conflict((comment_id, person_id))
.do_update()
.set(comment_like_form)
.get_result::<Self>(conn)
}
fn remove(conn: &PgConnection, user_id: i32, comment_id: i32) -> Result<usize, Error> {
fn remove(
conn: &PgConnection,
person_id: PersonId,
comment_id: CommentId,
) -> Result<usize, Error> {
use lemmy_db_schema::schema::comment_like::dsl;
diesel::delete(
dsl::comment_like
.filter(dsl::comment_id.eq(comment_id))
.filter(dsl::user_id.eq(user_id)),
.filter(dsl::person_id.eq(person_id)),
)
.execute(conn)
}
@ -186,7 +212,7 @@ impl Saveable<CommentSavedForm> for CommentSaved {
use lemmy_db_schema::schema::comment_saved::dsl::*;
insert_into(comment_saved)
.values(comment_saved_form)
.on_conflict((comment_id, user_id))
.on_conflict((comment_id, person_id))
.do_update()
.set(comment_saved_form)
.get_result::<Self>(conn)
@ -196,7 +222,7 @@ impl Saveable<CommentSavedForm> for CommentSaved {
diesel::delete(
comment_saved
.filter(comment_id.eq(comment_saved_form.comment_id))
.filter(user_id.eq(comment_saved_form.user_id)),
.filter(person_id.eq(comment_saved_form.person_id)),
)
.execute(conn)
}
@ -204,53 +230,46 @@ impl Saveable<CommentSavedForm> for CommentSaved {
#[cfg(test)]
mod tests {
use crate::{establish_unpooled_connection, Crud, Likeable, ListingType, Saveable, SortType};
use crate::{establish_unpooled_connection, Crud, Likeable, Saveable};
use lemmy_db_schema::source::{
comment::*,
community::{Community, CommunityForm},
person::{Person, PersonForm},
post::*,
user::{UserForm, User_},
};
use serial_test::serial;
#[test]
#[serial]
fn test_crud() {
let conn = establish_unpooled_connection();
let new_user = UserForm {
let new_person = PersonForm {
name: "terry".into(),
preferred_username: None,
password_encrypted: "nope".into(),
email: None,
matrix_user_id: None,
avatar: None,
banner: None,
admin: false,
banned: Some(false),
banned: None,
deleted: None,
published: None,
updated: None,
show_nsfw: false,
theme: "browser".into(),
default_sort_type: SortType::Hot as i16,
default_listing_type: ListingType::Subscribed as i16,
lang: "browser".into(),
show_avatars: true,
send_notifications_to_email: false,
actor_id: None,
bio: None,
local: true,
local: None,
private_key: None,
public_key: None,
last_refreshed_at: None,
inbox_url: None,
shared_inbox_url: None,
};
let inserted_user = User_::create(&conn, &new_user).unwrap();
let inserted_person = Person::create(&conn, &new_person).unwrap();
let new_community = CommunityForm {
name: "test community".to_string(),
title: "nada".to_owned(),
description: None,
category_id: 1,
creator_id: inserted_user.id,
creator_id: inserted_person.id,
removed: None,
deleted: None,
updated: None,
@ -263,13 +282,16 @@ mod tests {
published: None,
banner: None,
icon: None,
inbox_url: None,
shared_inbox_url: None,
followers_url: None,
};
let inserted_community = Community::create(&conn, &new_community).unwrap();
let new_post = PostForm {
name: "A test post".into(),
creator_id: inserted_user.id,
creator_id: inserted_person.id,
url: None,
body: None,
community_id: inserted_community.id,
@ -292,7 +314,7 @@ mod tests {
let comment_form = CommentForm {
content: "A test comment".into(),
creator_id: inserted_user.id,
creator_id: inserted_person.id,
post_id: inserted_post.id,
removed: None,
deleted: None,
@ -309,7 +331,7 @@ mod tests {
let expected_comment = Comment {
id: inserted_comment.id,
content: "A test comment".into(),
creator_id: inserted_user.id,
creator_id: inserted_person.id,
post_id: inserted_post.id,
removed: false,
deleted: false,
@ -323,7 +345,7 @@ mod tests {
let child_comment_form = CommentForm {
content: "A child comment".into(),
creator_id: inserted_user.id,
creator_id: inserted_person.id,
post_id: inserted_post.id,
parent_id: Some(inserted_comment.id),
removed: None,
@ -341,7 +363,7 @@ mod tests {
let comment_like_form = CommentLikeForm {
comment_id: inserted_comment.id,
post_id: inserted_post.id,
user_id: inserted_user.id,
person_id: inserted_person.id,
score: 1,
};
@ -351,7 +373,7 @@ mod tests {
id: inserted_comment_like.id,
comment_id: inserted_comment.id,
post_id: inserted_post.id,
user_id: inserted_user.id,
person_id: inserted_person.id,
published: inserted_comment_like.published,
score: 1,
};
@ -359,7 +381,7 @@ mod tests {
// Comment Saved
let comment_saved_form = CommentSavedForm {
comment_id: inserted_comment.id,
user_id: inserted_user.id,
person_id: inserted_person.id,
};
let inserted_comment_saved = CommentSaved::save(&conn, &comment_saved_form).unwrap();
@ -367,19 +389,19 @@ mod tests {
let expected_comment_saved = CommentSaved {
id: inserted_comment_saved.id,
comment_id: inserted_comment.id,
user_id: inserted_user.id,
person_id: inserted_person.id,
published: inserted_comment_saved.published,
};
let read_comment = Comment::read(&conn, inserted_comment.id).unwrap();
let updated_comment = Comment::update(&conn, inserted_comment.id, &comment_form).unwrap();
let like_removed = CommentLike::remove(&conn, inserted_user.id, inserted_comment.id).unwrap();
let like_removed = CommentLike::remove(&conn, inserted_person.id, inserted_comment.id).unwrap();
let saved_removed = CommentSaved::unsave(&conn, &comment_saved_form).unwrap();
let num_deleted = Comment::delete(&conn, inserted_comment.id).unwrap();
Comment::delete(&conn, inserted_child_comment.id).unwrap();
Post::delete(&conn, inserted_post.id).unwrap();
Community::delete(&conn, inserted_community.id).unwrap();
User_::delete(&conn, inserted_user.id).unwrap();
Person::delete(&conn, inserted_person.id).unwrap();
assert_eq!(expected_comment, read_comment);
assert_eq!(expected_comment, inserted_comment);

View file

@ -3,6 +3,7 @@ use diesel::{dsl::*, result::Error, *};
use lemmy_db_schema::{
naive_now,
source::comment_report::{CommentReport, CommentReportForm},
PersonId,
};
impl Reportable<CommentReportForm> for CommentReport {
@ -22,7 +23,11 @@ impl Reportable<CommentReportForm> for CommentReport {
/// * `conn` - the postgres connection
/// * `report_id` - the id of the report to resolve
/// * `by_resolver_id` - the id of the user resolving the report
fn resolve(conn: &PgConnection, report_id: i32, by_resolver_id: i32) -> Result<usize, Error> {
fn resolve(
conn: &PgConnection,
report_id: i32,
by_resolver_id: PersonId,
) -> Result<usize, Error> {
use lemmy_db_schema::schema::comment_report::dsl::*;
update(comment_report.find(report_id))
.set((
@ -38,7 +43,11 @@ impl Reportable<CommentReportForm> for CommentReport {
/// * `conn` - the postgres connection
/// * `report_id` - the id of the report to unresolve
/// * `by_resolver_id` - the id of the user unresolving the report
fn unresolve(conn: &PgConnection, report_id: i32, by_resolver_id: i32) -> Result<usize, Error> {
fn unresolve(
conn: &PgConnection,
report_id: i32,
by_resolver_id: PersonId,
) -> Result<usize, Error> {
use lemmy_db_schema::schema::comment_report::dsl::*;
update(comment_report.find(report_id))
.set((

View file

@ -9,9 +9,12 @@ use lemmy_db_schema::{
CommunityForm,
CommunityModerator,
CommunityModeratorForm,
CommunityUserBan,
CommunityUserBanForm,
CommunityPersonBan,
CommunityPersonBanForm,
},
CommunityId,
DbUrl,
PersonId,
};
mod safe_type {
@ -23,7 +26,6 @@ mod safe_type {
name,
title,
description,
category_id,
creator_id,
removed,
published,
@ -44,7 +46,6 @@ mod safe_type {
name,
title,
description,
category_id,
creator_id,
removed,
published,
@ -60,13 +61,13 @@ mod safe_type {
}
}
impl Crud<CommunityForm> for Community {
fn read(conn: &PgConnection, community_id: i32) -> Result<Self, Error> {
impl Crud<CommunityForm, CommunityId> for Community {
fn read(conn: &PgConnection, community_id: CommunityId) -> Result<Self, Error> {
use lemmy_db_schema::schema::community::dsl::*;
community.find(community_id).first::<Self>(conn)
}
fn delete(conn: &PgConnection, community_id: i32) -> Result<usize, Error> {
fn delete(conn: &PgConnection, community_id: CommunityId) -> Result<usize, Error> {
use lemmy_db_schema::schema::community::dsl::*;
diesel::delete(community.find(community_id)).execute(conn)
}
@ -80,7 +81,7 @@ impl Crud<CommunityForm> for Community {
fn update(
conn: &PgConnection,
community_id: i32,
community_id: CommunityId,
new_community: &CommunityForm,
) -> Result<Self, Error> {
use lemmy_db_schema::schema::community::dsl::*;
@ -91,7 +92,7 @@ impl Crud<CommunityForm> for Community {
}
impl ApubObject<CommunityForm> for Community {
fn read_from_apub_id(conn: &PgConnection, for_actor_id: &str) -> Result<Self, Error> {
fn read_from_apub_id(conn: &PgConnection, for_actor_id: &DbUrl) -> Result<Self, Error> {
use lemmy_db_schema::schema::community::dsl::*;
community
.filter(actor_id.eq(for_actor_id))
@ -113,25 +114,29 @@ pub trait Community_ {
fn read_from_name(conn: &PgConnection, community_name: &str) -> Result<Community, Error>;
fn update_deleted(
conn: &PgConnection,
community_id: i32,
community_id: CommunityId,
new_deleted: bool,
) -> Result<Community, Error>;
fn update_removed(
conn: &PgConnection,
community_id: i32,
community_id: CommunityId,
new_removed: bool,
) -> Result<Community, Error>;
fn update_removed_for_creator(
conn: &PgConnection,
for_creator_id: i32,
for_creator_id: PersonId,
new_removed: bool,
) -> Result<Vec<Community>, Error>;
fn update_creator(
conn: &PgConnection,
community_id: i32,
new_creator_id: i32,
community_id: CommunityId,
new_creator_id: PersonId,
) -> Result<Community, Error>;
fn distinct_federated_communities(conn: &PgConnection) -> Result<Vec<String>, Error>;
fn read_from_followers_url(
conn: &PgConnection,
followers_url: &DbUrl,
) -> Result<Community, Error>;
}
impl Community_ for Community {
@ -145,7 +150,7 @@ impl Community_ for Community {
fn update_deleted(
conn: &PgConnection,
community_id: i32,
community_id: CommunityId,
new_deleted: bool,
) -> Result<Community, Error> {
use lemmy_db_schema::schema::community::dsl::*;
@ -156,7 +161,7 @@ impl Community_ for Community {
fn update_removed(
conn: &PgConnection,
community_id: i32,
community_id: CommunityId,
new_removed: bool,
) -> Result<Community, Error> {
use lemmy_db_schema::schema::community::dsl::*;
@ -167,7 +172,7 @@ impl Community_ for Community {
fn update_removed_for_creator(
conn: &PgConnection,
for_creator_id: i32,
for_creator_id: PersonId,
new_removed: bool,
) -> Result<Vec<Community>, Error> {
use lemmy_db_schema::schema::community::dsl::*;
@ -178,8 +183,8 @@ impl Community_ for Community {
fn update_creator(
conn: &PgConnection,
community_id: i32,
new_creator_id: i32,
community_id: CommunityId,
new_creator_id: PersonId,
) -> Result<Community, Error> {
use lemmy_db_schema::schema::community::dsl::*;
diesel::update(community.find(community_id))
@ -191,79 +196,95 @@ impl Community_ for Community {
use lemmy_db_schema::schema::community::dsl::*;
community.select(actor_id).distinct().load::<String>(conn)
}
fn read_from_followers_url(
conn: &PgConnection,
followers_url_: &DbUrl,
) -> Result<Community, Error> {
use lemmy_db_schema::schema::community::dsl::*;
community
.filter(followers_url.eq(followers_url_))
.first::<Self>(conn)
}
}
impl Joinable<CommunityModeratorForm> for CommunityModerator {
fn join(
conn: &PgConnection,
community_user_form: &CommunityModeratorForm,
community_moderator_form: &CommunityModeratorForm,
) -> Result<Self, Error> {
use lemmy_db_schema::schema::community_moderator::dsl::*;
insert_into(community_moderator)
.values(community_user_form)
.values(community_moderator_form)
.get_result::<Self>(conn)
}
fn leave(
conn: &PgConnection,
community_user_form: &CommunityModeratorForm,
community_moderator_form: &CommunityModeratorForm,
) -> Result<usize, Error> {
use lemmy_db_schema::schema::community_moderator::dsl::*;
diesel::delete(
community_moderator
.filter(community_id.eq(community_user_form.community_id))
.filter(user_id.eq(community_user_form.user_id)),
.filter(community_id.eq(community_moderator_form.community_id))
.filter(person_id.eq(community_moderator_form.person_id)),
)
.execute(conn)
}
}
pub trait CommunityModerator_ {
fn delete_for_community(conn: &PgConnection, for_community_id: i32) -> Result<usize, Error>;
fn get_user_moderated_communities(
fn delete_for_community(
conn: &PgConnection,
for_user_id: i32,
) -> Result<Vec<i32>, Error>;
for_community_id: CommunityId,
) -> Result<usize, Error>;
fn get_person_moderated_communities(
conn: &PgConnection,
for_person_id: PersonId,
) -> Result<Vec<CommunityId>, Error>;
}
impl CommunityModerator_ for CommunityModerator {
fn delete_for_community(conn: &PgConnection, for_community_id: i32) -> Result<usize, Error> {
fn delete_for_community(
conn: &PgConnection,
for_community_id: CommunityId,
) -> Result<usize, Error> {
use lemmy_db_schema::schema::community_moderator::dsl::*;
diesel::delete(community_moderator.filter(community_id.eq(for_community_id))).execute(conn)
}
fn get_user_moderated_communities(
fn get_person_moderated_communities(
conn: &PgConnection,
for_user_id: i32,
) -> Result<Vec<i32>, Error> {
for_person_id: PersonId,
) -> Result<Vec<CommunityId>, Error> {
use lemmy_db_schema::schema::community_moderator::dsl::*;
community_moderator
.filter(user_id.eq(for_user_id))
.filter(person_id.eq(for_person_id))
.select(community_id)
.load::<i32>(conn)
.load::<CommunityId>(conn)
}
}
impl Bannable<CommunityUserBanForm> for CommunityUserBan {
impl Bannable<CommunityPersonBanForm> for CommunityPersonBan {
fn ban(
conn: &PgConnection,
community_user_ban_form: &CommunityUserBanForm,
community_person_ban_form: &CommunityPersonBanForm,
) -> Result<Self, Error> {
use lemmy_db_schema::schema::community_user_ban::dsl::*;
insert_into(community_user_ban)
.values(community_user_ban_form)
use lemmy_db_schema::schema::community_person_ban::dsl::*;
insert_into(community_person_ban)
.values(community_person_ban_form)
.get_result::<Self>(conn)
}
fn unban(
conn: &PgConnection,
community_user_ban_form: &CommunityUserBanForm,
community_person_ban_form: &CommunityPersonBanForm,
) -> Result<usize, Error> {
use lemmy_db_schema::schema::community_user_ban::dsl::*;
use lemmy_db_schema::schema::community_person_ban::dsl::*;
diesel::delete(
community_user_ban
.filter(community_id.eq(community_user_ban_form.community_id))
.filter(user_id.eq(community_user_ban_form.user_id)),
community_person_ban
.filter(community_id.eq(community_person_ban_form.community_id))
.filter(person_id.eq(community_person_ban_form.person_id)),
)
.execute(conn)
}
@ -277,12 +298,16 @@ impl Followable<CommunityFollowerForm> for CommunityFollower {
use lemmy_db_schema::schema::community_follower::dsl::*;
insert_into(community_follower)
.values(community_follower_form)
.on_conflict((community_id, user_id))
.on_conflict((community_id, person_id))
.do_update()
.set(community_follower_form)
.get_result::<Self>(conn)
}
fn follow_accepted(conn: &PgConnection, community_id_: i32, user_id_: i32) -> Result<Self, Error>
fn follow_accepted(
conn: &PgConnection,
community_id_: CommunityId,
person_id_: PersonId,
) -> Result<Self, Error>
where
Self: Sized,
{
@ -290,7 +315,7 @@ impl Followable<CommunityFollowerForm> for CommunityFollower {
diesel::update(
community_follower
.filter(community_id.eq(community_id_))
.filter(user_id.eq(user_id_)),
.filter(person_id.eq(person_id_)),
)
.set(pending.eq(true))
.get_result::<Self>(conn)
@ -303,13 +328,13 @@ impl Followable<CommunityFollowerForm> for CommunityFollower {
diesel::delete(
community_follower
.filter(community_id.eq(&community_follower_form.community_id))
.filter(user_id.eq(&community_follower_form.user_id)),
.filter(person_id.eq(&community_follower_form.person_id)),
)
.execute(conn)
}
// TODO: this function name only makes sense if you call it with a remote community. for a local
// community, it will also return true if only remote followers exist
fn has_local_followers(conn: &PgConnection, community_id_: i32) -> Result<bool, Error> {
fn has_local_followers(conn: &PgConnection, community_id_: CommunityId) -> Result<bool, Error> {
use lemmy_db_schema::schema::community_follower::dsl::*;
diesel::select(exists(
community_follower.filter(community_id.eq(community_id_)),
@ -320,56 +345,41 @@ impl Followable<CommunityFollowerForm> for CommunityFollower {
#[cfg(test)]
mod tests {
use crate::{
establish_unpooled_connection,
Bannable,
Crud,
Followable,
Joinable,
ListingType,
SortType,
};
use lemmy_db_schema::source::{community::*, user::*};
use crate::{establish_unpooled_connection, Bannable, Crud, Followable, Joinable};
use lemmy_db_schema::source::{community::*, person::*};
use serial_test::serial;
#[test]
#[serial]
fn test_crud() {
let conn = establish_unpooled_connection();
let new_user = UserForm {
let new_person = PersonForm {
name: "bobbee".into(),
preferred_username: None,
password_encrypted: "nope".into(),
email: None,
matrix_user_id: None,
avatar: None,
banner: None,
admin: false,
banned: Some(false),
banned: None,
deleted: None,
published: None,
updated: None,
show_nsfw: false,
theme: "browser".into(),
default_sort_type: SortType::Hot as i16,
default_listing_type: ListingType::Subscribed as i16,
lang: "browser".into(),
show_avatars: true,
send_notifications_to_email: false,
actor_id: None,
bio: None,
local: true,
local: None,
private_key: None,
public_key: None,
last_refreshed_at: None,
inbox_url: None,
shared_inbox_url: None,
};
let inserted_user = User_::create(&conn, &new_user).unwrap();
let inserted_person = Person::create(&conn, &new_person).unwrap();
let new_community = CommunityForm {
name: "TIL".into(),
creator_id: inserted_user.id,
creator_id: inserted_person.id,
title: "nada".to_owned(),
description: None,
category_id: 1,
nsfw: false,
removed: None,
deleted: None,
@ -382,17 +392,19 @@ mod tests {
published: None,
icon: None,
banner: None,
followers_url: None,
inbox_url: None,
shared_inbox_url: None,
};
let inserted_community = Community::create(&conn, &new_community).unwrap();
let expected_community = Community {
id: inserted_community.id,
creator_id: inserted_user.id,
creator_id: inserted_person.id,
name: "TIL".into(),
title: "nada".to_owned(),
description: None,
category_id: 1,
nsfw: false,
removed: false,
deleted: false,
@ -405,11 +417,14 @@ mod tests {
last_refreshed_at: inserted_community.published,
icon: None,
banner: None,
followers_url: inserted_community.followers_url.to_owned(),
inbox_url: inserted_community.inbox_url.to_owned(),
shared_inbox_url: None,
};
let community_follower_form = CommunityFollowerForm {
community_id: inserted_community.id,
user_id: inserted_user.id,
person_id: inserted_person.id,
pending: false,
};
@ -419,55 +434,56 @@ mod tests {
let expected_community_follower = CommunityFollower {
id: inserted_community_follower.id,
community_id: inserted_community.id,
user_id: inserted_user.id,
person_id: inserted_person.id,
pending: Some(false),
published: inserted_community_follower.published,
};
let community_user_form = CommunityModeratorForm {
let community_moderator_form = CommunityModeratorForm {
community_id: inserted_community.id,
user_id: inserted_user.id,
person_id: inserted_person.id,
};
let inserted_community_user = CommunityModerator::join(&conn, &community_user_form).unwrap();
let inserted_community_moderator =
CommunityModerator::join(&conn, &community_moderator_form).unwrap();
let expected_community_user = CommunityModerator {
id: inserted_community_user.id,
let expected_community_moderator = CommunityModerator {
id: inserted_community_moderator.id,
community_id: inserted_community.id,
user_id: inserted_user.id,
published: inserted_community_user.published,
person_id: inserted_person.id,
published: inserted_community_moderator.published,
};
let community_user_ban_form = CommunityUserBanForm {
let community_person_ban_form = CommunityPersonBanForm {
community_id: inserted_community.id,
user_id: inserted_user.id,
person_id: inserted_person.id,
};
let inserted_community_user_ban =
CommunityUserBan::ban(&conn, &community_user_ban_form).unwrap();
let inserted_community_person_ban =
CommunityPersonBan::ban(&conn, &community_person_ban_form).unwrap();
let expected_community_user_ban = CommunityUserBan {
id: inserted_community_user_ban.id,
let expected_community_person_ban = CommunityPersonBan {
id: inserted_community_person_ban.id,
community_id: inserted_community.id,
user_id: inserted_user.id,
published: inserted_community_user_ban.published,
person_id: inserted_person.id,
published: inserted_community_person_ban.published,
};
let read_community = Community::read(&conn, inserted_community.id).unwrap();
let updated_community =
Community::update(&conn, inserted_community.id, &new_community).unwrap();
let ignored_community = CommunityFollower::unfollow(&conn, &community_follower_form).unwrap();
let left_community = CommunityModerator::leave(&conn, &community_user_form).unwrap();
let unban = CommunityUserBan::unban(&conn, &community_user_ban_form).unwrap();
let left_community = CommunityModerator::leave(&conn, &community_moderator_form).unwrap();
let unban = CommunityPersonBan::unban(&conn, &community_person_ban_form).unwrap();
let num_deleted = Community::delete(&conn, inserted_community.id).unwrap();
User_::delete(&conn, inserted_user.id).unwrap();
Person::delete(&conn, inserted_person.id).unwrap();
assert_eq!(expected_community, read_community);
assert_eq!(expected_community, inserted_community);
assert_eq!(expected_community, updated_community);
assert_eq!(expected_community_follower, inserted_community_follower);
assert_eq!(expected_community_user, inserted_community_user);
assert_eq!(expected_community_user_ban, inserted_community_user_ban);
assert_eq!(expected_community_moderator, inserted_community_moderator);
assert_eq!(expected_community_person_ban, inserted_community_person_ban);
assert_eq!(1, ignored_community);
assert_eq!(1, left_community);
assert_eq!(1, unban);

View file

@ -0,0 +1,119 @@
use crate::Crud;
use bcrypt::{hash, DEFAULT_COST};
use diesel::{dsl::*, result::Error, *};
use lemmy_db_schema::{
naive_now,
schema::local_user::dsl::*,
source::local_user::{LocalUser, LocalUserForm},
LocalUserId,
PersonId,
};
mod safe_settings_type {
use crate::ToSafeSettings;
use lemmy_db_schema::{schema::local_user::columns::*, source::local_user::LocalUser};
type Columns = (
id,
person_id,
email,
admin,
show_nsfw,
theme,
default_sort_type,
default_listing_type,
lang,
show_avatars,
send_notifications_to_email,
matrix_user_id,
validator_time,
);
impl ToSafeSettings for LocalUser {
type SafeSettingsColumns = Columns;
/// Includes everything but the hashed password
fn safe_settings_columns_tuple() -> Self::SafeSettingsColumns {
(
id,
person_id,
email,
admin,
show_nsfw,
theme,
default_sort_type,
default_listing_type,
lang,
show_avatars,
send_notifications_to_email,
matrix_user_id,
validator_time,
)
}
}
}
pub trait LocalUser_ {
fn register(conn: &PgConnection, form: &LocalUserForm) -> Result<LocalUser, Error>;
fn update_password(
conn: &PgConnection,
local_user_id: LocalUserId,
new_password: &str,
) -> Result<LocalUser, Error>;
fn add_admin(conn: &PgConnection, person_id: PersonId, added: bool) -> Result<LocalUser, Error>;
}
impl LocalUser_ for LocalUser {
fn register(conn: &PgConnection, form: &LocalUserForm) -> Result<Self, Error> {
let mut edited_user = form.clone();
let password_hash =
hash(&form.password_encrypted, DEFAULT_COST).expect("Couldn't hash password");
edited_user.password_encrypted = password_hash;
Self::create(&conn, &edited_user)
}
fn update_password(
conn: &PgConnection,
local_user_id: LocalUserId,
new_password: &str,
) -> Result<Self, Error> {
let password_hash = hash(new_password, DEFAULT_COST).expect("Couldn't hash password");
diesel::update(local_user.find(local_user_id))
.set((
password_encrypted.eq(password_hash),
validator_time.eq(naive_now()),
))
.get_result::<Self>(conn)
}
fn add_admin(conn: &PgConnection, for_person_id: PersonId, added: bool) -> Result<Self, Error> {
diesel::update(local_user.filter(person_id.eq(for_person_id)))
.set(admin.eq(added))
.get_result::<Self>(conn)
}
}
impl Crud<LocalUserForm, LocalUserId> for LocalUser {
fn read(conn: &PgConnection, local_user_id: LocalUserId) -> Result<Self, Error> {
local_user.find(local_user_id).first::<Self>(conn)
}
fn delete(conn: &PgConnection, local_user_id: LocalUserId) -> Result<usize, Error> {
diesel::delete(local_user.find(local_user_id)).execute(conn)
}
fn create(conn: &PgConnection, form: &LocalUserForm) -> Result<Self, Error> {
insert_into(local_user)
.values(form)
.get_result::<Self>(conn)
}
fn update(
conn: &PgConnection,
local_user_id: LocalUserId,
form: &LocalUserForm,
) -> Result<Self, Error> {
diesel::update(local_user.find(local_user_id))
.set(form)
.get_result::<Self>(conn)
}
}

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