Compare commits

...

837 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
a10974ed6e Set debug=0 in cargo.toml to speed up builds 2021-01-13 18:10:21 +01: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
eafdf3033f Version v0.9.0-rc.2 2021-01-12 19:29:48 -05:00
d54be4ed7f Trying autotag 2021-01-12 19:26:32 -05:00
a1e5d0fd00 Version v0.9.0-rc.1 2021-01-12 18:59:07 -05:00
d4e800175f Merge branch 'move_views_to_diesel' into remove_travis_and_federation_docker 2021-01-12 11:56:24 -05:00
39001af9a0 Merge remote-tracking branch 'yerba/move_views_to_diesel' into move_views_to_diesel 2021-01-12 11:12:54 -05:00
Dessalines
c6357f3c86
Deletion on fetch (#1345)
* Delete local object on fetch when receiving HTTP 410, split fetcher (fixes #1256)

* Removing submodules

* Trying to re-init submodule

* Trying to re-init submodule 2

* Trying to re-init submodule 3

* Logging line.

* Removing submodules

* Adding again.

* Adding again 2.

* Adding again 3.

* Adding again 4.

* Adding again 5.

* Adding again 6.

* Adding again 7.

* Adding again 8.

* Adding again 9.

* Add more clippy lints, remove dbg!() statement

* Adding again 10.

* Adding again 11.

* Adding again 12.

Co-authored-by: Felix Ableitner <me@nutomic.com>
2021-01-12 11:12:41 -05:00
dessalines
4eedc31893 Merge pull request 'Include fix for mdbook xss vulnerability' (#155) from mdbook-xss into move_views_to_diesel
Reviewed-on: https://yerbamate.ml/LemmyNet/lemmy/pulls/155
2021-01-12 15:57:27 +00: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
fec77d583f Include fix for mdbook xss vulnerability 2021-01-09 17:54:31 +01:00
7a97fc370b Adding stickied to post_aggregates.
- Added more indexes to account for sorting by stickied first.
- Changed all order bys in the diesel views to use post_aggregates.
2021-01-07 16:22:17 -05:00
b9b51c2dfc Fixing some minor websocket things. 2021-01-07 01:17:42 -05:00
ceae7eb47a Private message query debugging. 2021-01-06 16:02:08 -05:00
1c113f915e Logging post query. 2021-01-06 13:23:05 -05:00
514f4011ba Adding docs commit. 2021-01-06 00:49:24 -05:00
a56977f4c5 Trying to get mdbooks to build. 2021-01-06 00:34:26 -05:00
86dfe456fd Merge branch 'main' into move_views_to_diesel 2021-01-06 00:30:29 -05:00
50e7275c3b
Move docs into submodule (fixes #1342) (#1343) 2021-01-06 00:27:58 -05:00
1e0c32f7a3 Merge branch 'main' into move_views_to_diesel 2021-01-05 23:55:02 -05:00
d227000de3 Halfway done with hot rank indexes. 2021-01-05 23:42:48 -05:00
61c6f33103 Trying to fix save user settings. 2021-01-04 14:09:53 -05:00
d300968ee8 Return http status code 410 with apub tombstone (ref #1256) 2021-01-04 19:53:15 +01:00
7bc9434596 Fix integration tests (except one) 2021-01-04 18:32:33 +01:00
dependabot[bot]
632d8f384a
Bump node-notifier from 8.0.0 to 8.0.1 in /api_tests (#1332)
Bumps [node-notifier](https://github.com/mikaelbr/node-notifier) from 8.0.0 to 8.0.1.
- [Release notes](https://github.com/mikaelbr/node-notifier/releases)
- [Changelog](https://github.com/mikaelbr/node-notifier/blob/v8.0.1/CHANGELOG.md)
- [Commits](https://github.com/mikaelbr/node-notifier/compare/v8.0.0...v8.0.1)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-01-04 11:29:04 -05:00
bcb0c381e5 Merge remote-tracking branch 'github/main' into main 2021-01-04 17:23:06 +01:00
D.Loh
dd069de519
add User_ missing field 'deleted' in tests (#1338)
per rust-analyzer tips
2021-01-04 10:26:43 -05:00
418eb8025c Changing default_sort and listing back to numbers. 2020-12-25 22:22:25 -05:00
9ab3a9d072 Add clippy check tests to drone. 2020-12-23 19:42:42 -05:00
4c681eb48b Merge remote-tracking branch 'origin/split-db-workspace2' into move_views_to_diesel_split_2 2020-12-23 19:09:41 -05:00
58281208b9 Adding community_view to PostResponse.
- Changing to_vec function name.
2020-12-23 16:56:20 -05:00
5a16d43fef Run cargo clippy in CI on whole workspace 2020-12-22 13:03:50 +01:00
95e30f0e08 Split up lemmy_db_views, put lemmy_rate_limit into lemmy_utils 2020-12-22 00:34:54 +01:00
d5efebbf47 Split lemmy_db into lemmy_db_queries, lemmy_db_aggregates and lemmy_db_views 2020-12-21 17:39:11 +01:00
e5a65d5807 Upgrading deps. 2020-12-21 09:34:59 -05:00
1a0d1f64f0 Merge remote-tracking branch 'origin/split-db-workspace' into move_views_to_diesel_split 2020-12-21 09:28:20 -05:00
bd06dd53e3 remove timing files added by accident 2020-12-21 14:53:27 +01:00
5231666465 Move remaining structs from lemmy_db::source to lemmy_db_schema 2020-12-21 14:38:34 +01:00
a7e231b35b Move community to lemmy_db_schema 2020-12-21 13:28:12 +01:00
e25436576a Fixing some clippy warnings. 2020-12-20 23:00:33 -05:00
8e1f41f1e4 Removing cargo check from drone, clippy already runs check. 2020-12-20 22:50:43 -05:00
04ce64e9b6 Adding to clippy 2020-12-20 22:42:29 -05:00
2aa8de87b2 Trying other drone checks. 2020-12-20 22:37:01 -05:00
929f1d02b5 Fixing integration tests. 2020-12-20 22:27:27 -05:00
d767dd998e Merge branch 'drone-io-dess' into move_views_to_diesel_drone 2020-12-20 21:48:29 -05:00
5af8257e19 Changing unit tests to api v2. 2020-12-20 16:16:57 -05:00
a27b7f8d1f Fix site aggs. 2020-12-20 00:09:20 -05:00
2d7d9cf7d8 Some API cleanup, adding site_id to site aggregates. 2020-12-19 20:10:47 -05:00
f842bbff8d Move user to lemmy_db_schema, create traits for impls 2020-12-18 19:38:32 +01:00
114f3cbfb5 Move comment, post definitions into lemmy_db_schema 2020-12-18 18:27:25 +01:00
3f36730dba Fixing unit tests. 2020-12-18 12:25:27 -05:00
089d812dc8 Split lemmy_db into separate workspaces 2020-12-18 17:17:44 +01:00
9d0709dfe8 Trying again. 2020-12-17 21:55:15 -05:00
2e5297e337 Trying again. 2020-12-17 21:36:59 -05:00
5c266302c5 Adding unfollows. 2020-12-17 21:10:20 -05:00
6cc148f6a6 Trying again. 2020-12-17 16:02:35 -05:00
1607930d07 Trying again. 2020-12-17 16:00:51 -05:00
1a4e2f4770 Trying again. 2020-12-17 15:59:25 -05:00
caaf6b178b Trying again. 2020-12-17 15:35:28 -05:00
6d96f105c6 Dropping the unecessary views and table triggers. 2020-12-17 15:29:10 -05:00
583808d5e7 Trying again. 2020-12-17 14:59:53 -05:00
5768a4eda7 Trying again. 2020-12-17 14:36:22 -05:00
4997d4b0b5 Trying again. 2020-12-17 14:23:15 -05:00
179709cc09 Fixing drone tests. 2020-12-17 14:01:33 -05:00
dessalines
44b72ccbd6 Merge pull request 'Create empty inbox collections for actors (ref #1322)' (#151) from actor-inbox into main
Reviewed-on: https://yerbamate.ml/LemmyNet/lemmy/pulls/151
2020-12-17 14:51:23 +00:00
dessalines
5b2fb07e44 Merge pull request 'Use correct content-type headers for apub inbox (ref #1220)' (#150) from inbox-headers into main
Reviewed-on: https://yerbamate.ml/LemmyNet/lemmy/pulls/150
2020-12-17 14:44:08 +00:00
4c79e26078 Updating deps. 2020-12-17 09:13:12 -05:00
4bf0ec94c8 Create empty inbox collections for actors (ref #1322) 2020-12-17 14:22:51 +01:00
4f5e51beb5 Removing fast tables and old views. 2020-12-16 22:42:25 -05:00
05c3e471ae Adding report views. 2020-12-16 22:03:03 -05:00
1cf520254d Adding private message view. 2020-12-16 17:16:48 -05:00
313f0467c8 Adding moderator views. 2020-12-16 16:28:18 -05:00
bd6a4a54a9 Merge branch 'main' into move_views_to_diesel 2020-12-16 14:01:16 -05:00
57c2f2ef1c Getting rid of terrible boxedjoin types. 2020-12-16 13:59:43 -05:00
db0a51de2a Handle long activitystreams header in nginx config (ref #1322) 2020-12-16 18:24:14 +01:00
711db4c790 Removing old user_mention_view. 2020-12-16 11:09:21 -05:00
cbd02f2a87 Use correct content-type headers for apub inbox (ref #1220) 2020-12-16 16:30:44 +01:00
eiknat
036161cb38
add on_conflict clauses to common unique constraint failures (#1264)
* add on_conflict clauses to common unique constraint failures

* user mention: change create conflict to do_update
2020-12-16 09:42:57 -05:00
c947539301
Add docs for creating custom lemmy frontend (#1319) 2020-12-16 09:03:21 -05:00
471abf7f29 Removing old comment_view. 2020-12-15 14:39:18 -05:00
e4714627a4 Beginning to add new comment_view. 2020-12-15 10:28:25 -05:00
998e824bd8 Add IP forwarding headers to nginx config (fixes #1318) 2020-12-15 15:01:37 +01:00
e492cce206 Allow running docker-less federation tests locally 2020-12-15 14:58:11 +01:00
d5955b60c0 silence lemmy output in federation logs 2020-12-14 18:01:12 +01:00
f33577b317 send activities sync for tests 2020-12-14 17:44:27 +01:00
a455e8c0ab add pictrs and iframely, read docker hub login from secrets 2020-12-14 17:01:40 +01:00
79a960d8a5 Merge branch 'main' into move_views_to_diesel 2020-12-13 12:07:11 -05:00
f456f5da46 Re-organizing source tables into a different folder. 2020-12-13 12:04:42 -05:00
ccd26bfcd7 Trying to fix post test again. 2020-12-12 10:45:14 -05:00
f6ba6d5590 Trying to fix post test again. 2020-12-12 10:28:28 -05:00
dfe17662df Trying to fix post test again. 2020-12-12 09:39:41 -05:00
28c217eb66 Trying to fix post test again. 2020-12-11 23:20:18 -05:00
d594005d49 Trying to fix post test again. 2020-12-11 20:20:14 -05:00
4410d2e44d Trying to fix post test again. 2020-12-11 19:59:45 -05:00
5dff60adc5 Trying to fix post test again. 2020-12-11 16:26:44 -05:00
9e5824df85 Trying to fix post test. 2020-12-11 15:24:28 -05:00
543be801ab disable debug log 2020-12-11 20:56:14 +01:00
ab6b28ee60 use dash 2020-12-11 20:39:54 +01:00
bdd264cd5e Adding tests for post aggregates. 2020-12-11 13:07:27 -05:00
5ae3f59092 fix warning 2020-12-11 18:43:34 +01:00
0c89e9c2d6 use hosts file 2020-12-11 18:22:16 +01:00
30a1a69850 setup db 2020-12-11 18:09:47 +01:00
e7a5eff061 try debug build again 2020-12-11 17:58:03 +01:00
446ae301f8 syntax 2020-12-11 17:48:57 +01:00
2dd3eee0dd fix paths, try debug 2020-12-11 17:46:50 +01:00
9c7f2cb0c3 fix bin path 2020-12-11 17:33:55 +01:00
b61bfcefa7 find the binary 2020-12-11 17:27:18 +01:00
9cb7680211 fix release build? 2020-12-11 17:20:29 +01:00
20115444b6 syntax 2020-12-11 17:02:24 +01:00
7c12b1026c faster release build 2020-12-11 17:01:05 +01:00
35bf50ab15 Removing old postview. 2020-12-11 10:27:33 -05:00
5b376de5f5 need to use release bin 2020-12-11 16:25:20 +01:00
2259b7ebc2 try release build 2020-12-11 16:15:48 +01:00
d859844fea use apk add, remove diesel migrate from test.sh 2020-12-11 15:56:20 +01:00
a56db9a47c cargo fmt 2020-12-11 15:45:29 +01:00
cbe5cf8ca0 try to run migrations on db connection in tests 2020-12-11 15:41:39 +01:00
6075f7d2f1 try again with full path 2020-12-11 15:31:19 +01:00
55e3f370fd need to use rust image 2020-12-11 15:28:36 +01:00
b11b3f4fad set rustfmt path 2020-12-11 15:22:12 +01:00
135d654999 another try 2020-12-11 15:20:15 +01:00
6d1053b8e5 put full path 2020-12-11 15:15:36 +01:00
71d3457b82 try rustfmt 2020-12-11 15:14:17 +01:00
6fec2e56f6 use nightly image for fmt 2020-12-11 15:05:46 +01:00
d9f0aa223a need nightly for fmt 2020-12-11 15:02:50 +01:00
8b0374cc4e also check formatting, more extensive cargo check 2020-12-11 14:57:43 +01:00
08748bbede try to init db from lemmy in tests 2020-12-11 14:49:10 +01:00
7e7e27a7eb build diesel cli in same step 2020-12-11 14:28:14 +01:00
77b7511235 try tests without diesel migration run 2020-12-11 14:16:14 +01:00
df2ec0aa38 try running federation tests in same alpine version 2020-12-11 14:13:30 +01:00
afc79ce0e3 Adding a viewtovec trait. 2020-12-10 20:39:42 -05:00
eef93440d0 Starting to add post_view. 2020-12-10 15:53:49 -05:00
4fd6b5f5e1 fix script location 2020-12-10 21:36:50 +01:00
405e7eff27 try to fix federation test 2020-12-10 21:14:05 +01:00
606bfa89b6 add sudo 2020-12-10 21:03:32 +01:00
964332db12 build diesel as root 2020-12-10 20:57:28 +01:00
f7cdadc9c2 compile diesel_cli as root (fails for some reason) 2020-12-10 20:51:51 +01:00
e64f196c0d try with chown 2020-12-10 20:45:14 +01:00
ed29e3d934 enable all steps, add comments 2020-12-10 20:30:02 +01:00
a7413723b4 fix 2020-12-10 20:27:38 +01:00
11c9559ef8 check rust version 2020-12-10 20:27:06 +01:00
7af4a60ec4 change dir to read config 2020-12-10 20:19:17 +01:00
7b2c59bd98 use bash, install psql 2020-12-10 20:06:09 +01:00
861e38d157 needs apt-get then 2020-12-10 20:00:27 +01:00
580e397525 dont use alpine for federation test 2020-12-10 19:50:41 +01:00
7717deda0e more debug 2020-12-10 18:57:09 +01:00
cdcbef088d more debugging 2020-12-10 18:50:15 +01:00
fadb2b46f5 try to run as root 2020-12-10 18:44:43 +01:00
a30199d879 ls lemmy bin 2020-12-10 18:38:53 +01:00
6ff7debbdf enable cargo build 2020-12-10 18:32:26 +01:00
f8a196faaf use apk 2020-12-10 18:31:28 +01:00
94d6ceb4df install bash and curl 2020-12-10 18:29:13 +01:00
200913f631 try with sh 2020-12-10 18:26:50 +01:00
53c4aab6af execute with bash 2020-12-10 18:23:04 +01:00
dd6b539119 ls -la 2020-12-10 18:16:44 +01:00
e0bbb58ec5 fix script name 2020-12-10 18:10:29 +01:00
048ada462b fix command 2020-12-10 18:05:53 +01:00
e849b22d3c change image for create tags 2020-12-10 18:04:42 +01:00
f76f742ba7 implement federation test in bash 2020-12-10 18:03:11 +01:00
c939954f84 use node docker image 2020-12-10 17:46:36 +01:00
56bea54536 also install nodejs 2020-12-10 17:37:00 +01:00
9793a0e521 install correct yarn package 2020-12-10 17:33:23 +01:00
a8e0eee7df install yarn 2020-12-10 17:28:09 +01:00
ad75f9de4b fix syntax again 2020-12-10 17:25:17 +01:00
2c60215156 remove comments 2020-12-10 17:23:14 +01:00
def5276f84 fix syntax 2020-12-10 17:22:36 +01:00
6391ec16ed try federation tests 2020-12-10 17:20:44 +01:00
a94fd6aaf6 try without auth 2020-12-10 14:05:03 +01:00
f9bd72e1ee try docker release build 2020-12-10 13:55:38 +01:00
841ad8476c disable broken tests 2020-12-10 13:47:46 +01:00
4eced2518c try caching 2020-12-10 13:33:24 +01:00
331985f0a0 start postgres later 2020-12-10 13:24:16 +01:00
37fc1d721f use volume for diesel cli 2020-12-10 13:21:34 +01:00
82c3778082 change step order for better caching 2020-12-10 13:14:32 +01:00
2d0bf7d40d full diesel path 2020-12-10 13:11:05 +01:00
fc382e20e1 install diesel_cli 2020-12-10 13:06:10 +01:00
a2cd1ff367 set DATABASE_URL, run diesel migration, separate steps 2020-12-10 13:00:31 +01:00
2d88dfdaef run a query 2020-12-10 12:34:56 +01:00
b5b670b8b9 try db connection with psql 2020-12-10 12:31:59 +01:00
e02d0f39ec retry service env 2020-12-10 02:10:17 +01:00
003852f884 try to fix postgres service 2020-12-10 02:03:29 +01:00
69c2fe19e4 remove docker socket mount 2020-12-10 01:44:11 +01:00
b79c10c122 apt-get -y 2020-12-10 01:26:45 +01:00
94ab4f6164 apt update 2020-12-10 01:24:28 +01:00
af2a27935b add espeak and postgres client 2020-12-10 01:23:47 +01:00
5b34d2be6c fix test, run clippy 2020-12-10 00:55:14 +01:00
ec13759ca6 use alt docker image 2020-12-10 00:24:14 +01:00
b7563bfbf5 set toolchain 2020-12-10 00:17:55 +01:00
7aa686b650 build as root 2020-12-10 00:16:50 +01:00
d0e730fed4 perm issues 2 2020-12-10 00:13:53 +01:00
58850a0b0c permission issues 2020-12-10 00:13:21 +01:00
ef22f70e18 syntax again 2020-12-10 00:00:43 +01:00
88cd8b2d74 syntax 2020-12-09 23:59:45 +01:00
84ac188cee cargo test + service 2020-12-09 23:58:50 +01:00
cdc7df8625 fix 2020-12-09 23:18:52 +01:00
2d011468b4 remove comment 2020-12-09 23:18:10 +01:00
4557f2b03d make debug build 2020-12-09 23:17:32 +01:00
b83cafc454 separate steps 2020-12-09 22:43:32 +01:00
dabcfca67b Adding tests for current aggregates. 2020-12-09 11:52:10 -05:00
09212bb6b7 fix docker image 2020-12-09 15:00:31 +01:00
d3a89a2a22 trigger another build 2020-12-09 14:58:58 +01:00
bfd306e9de add volume back in 2020-12-09 14:56:45 +01:00
cb9a06e249 Minor grammar change in docs 2020-12-09 12:34:50 +01:00
f9f95a2b92 try plugins/docker image 2020-12-08 21:30:05 +01:00
91be01fd63 docker in docker 2020-12-08 21:27:03 +01:00
04fe7c29cb remove workspace, fix paths 2020-12-08 21:25:24 +01:00
df0f609cce log dir 2020-12-08 21:23:54 +01:00
e8ffa283f2 fix docker image? 2020-12-08 21:22:36 +01:00
b04944d9a5 try manually without sudo 2020-12-08 21:20:23 +01:00
eb06bbfef7 run test script 2020-12-08 21:17:11 +01:00
0e364a4efe remove volume 2020-12-08 21:15:04 +01:00
d3b2ce7b35 wip: add drone ci, remove travis ci 2020-12-08 21:13:53 +01:00
742b78523a Merge branch 'main' into move_views_to_diesel 2020-12-08 13:31:25 -05:00
a432f939b6 Merge remote-tracking branch 'yerba/main' into main 2020-12-08 13:31:09 -05:00
dessalines
6908feb3ba Merge pull request 'Some more minor docs changes' (#149) from more-docs into main
Reviewed-on: https://yerbamate.ml/LemmyNet/lemmy/pulls/149
2020-12-08 18:31:45 +00:00
46e38bf714 Merge branch 'main' into move_views_to_diesel 2020-12-08 13:17:55 -05:00
5010f693ba Some more minor docs changes 2020-12-08 19:09:11 +01:00
08ab85a9d5 Remove logging to find bug (yerbamate.ml/LemmyNet/lemmy/pulls/146) 2020-12-08 12:48:18 -05:00
16deec7443 Merge remote-tracking branch 'yerba/main' into main 2020-12-08 12:38:21 -05:00
9cc1cfc973
Apub local object handling (#1297)
* Limit visibility of some traits and methods

* WIP: alternative way to handle non-local object parsing

* finish this

* cleanup

* Move check for locked post into Comment::from_apub()

* Mark user as updated after fetching

* Should set last_refreshed_at, not updated

* Add ApubObject trait in DB, with method read_from_apub_id()

* Create shared, generic implementation for `FromApub`, prefer local data

* Check for community ban when parsing post/comment (fixes #1287)

* Fix tests (changes in get_object_from_apub() prevented `Update` from working)

* Support parsing `like.object` either as URL or object

* Send out like.object as URL, instead of full object (fixes #1283)

* add todo
2020-12-08 12:38:48 -05:00
dessalines
860f2f3855 Merge pull request 'Rewrite federation docs' (#145) from federation-docs into main
Reviewed-on: https://yerbamate.ml/LemmyNet/lemmy/pulls/145
2020-12-08 17:36:37 +00:00
55e03c4eb4 Rewrite federation docs 2020-12-08 14:52:54 +01:00
fc1d07fce6 Merge remote-tracking branch 'origin/main' into move_views_to_diesel 2020-12-07 16:22:09 -05:00
e371ec1dc4 Adding user aggregates tests. 2020-12-07 15:58:53 -05:00
5e57f1bcad
Merge pull request #1311 from r0qstr/patch-1
Update lemmy_council.md
2020-12-07 12:17:15 +00:00
9884927b8a Adding site aggregates unit test. 2020-12-06 22:17:52 -05:00
f5bef3980a Adding hot rank function, possibly fixing views. 2020-12-06 16:44:36 -05:00
36f7b20784 Removing old communityviews 2020-12-06 09:12:51 -05:00
5e510e7a67 Adding other community views. 2020-12-05 23:37:16 -05:00
caedb7fcc4 Community view halfway done. 2020-12-05 22:49:15 -05:00
r0qstr
0a12cb0281
Update lemmy_council.md
added myself as a council member
2020-12-05 21:02:19 +01:00
2400a078d7 Merge branch 'main' into move_views_to_diesel 2020-12-05 08:10:25 -06:00
028d1d0efc Userview safe updated. 2020-12-04 23:18:30 -05:00
efdcbc44c4 Starting to work on community view, 2 2020-12-04 16:35:46 -05:00
Dessalines
cf3a98afcc
Merge pull request #1308 from LemmyNet/update_cargo_deps
Updating cargo deps, fixing image if_some deprecation.
2020-12-04 13:07:43 -05:00
88d7b0a83c Starting to work on community view 2020-12-04 11:29:44 -05:00
b92e7eb781 Updating cargo deps, fixing image if_some deprecation. 2020-12-04 09:00:15 -05:00
2d4099577f Removing old user_view. 2020-12-03 19:47:58 -05:00
6d8f93d8a1 More user aggregates. 2020-12-03 13:39:56 -05:00
d66f4e8ac0 Finishing up user aggregates. 2020-12-03 10:18:17 -05:00
5d44dedfda Merge branch 'main' into move_views_to_diesel 2020-12-03 09:34:45 -05:00
eed7eac10b Version v0.8.10 2020-12-03 08:28:58 -06:00
37e7f1a9a8 Starting to work on user aggs. 2020-12-03 08:27:22 -06:00
4a94d9182e
Merge pull request #1304 from LemmyNet/fix_community_local_filter
Fixing community local filter. #1302
2020-12-03 11:55:16 +00:00
7731479607 Adding SiteAggregates. 2020-12-02 22:39:31 -05:00
8a0336c2ff Fixing community local filter. #1302 2020-12-02 16:04:13 -05:00
ca7224c086 Starting on siteview. 2020-12-02 13:32:47 -06:00
df8a696bb5
Merge pull request #1300 from Tafiti/patch-1
Added council member
2020-12-02 12:55:46 +00:00
Tafiti
d06b59c92b
Added a council member. 2020-12-01 18:22:17 -05:00
Tafiti
df913326bd
typo 2020-12-01 18:18:47 -05:00
b587e147b0 Version v0.8.9 2020-12-01 12:54:44 -06:00
e8116f21cd Merge remote-tracking branch 'yerba/main' into main 2020-12-01 13:29:51 -05:00
nutomic
2b5c69d678 Add check to make sure that inbox doesnt receive local activities (ref #1283) (#147)
Fixed comparison

Add check to make sure that inbox doesnt receive local activities (ref #1283)

Co-authored-by: Felix Ableitner <me@nutomic.com>
Reviewed-on: https://yerbamate.ml/LemmyNet/lemmy/pulls/147
2020-12-01 18:30:15 +00:00
Dessalines
4079235b0d
Fix post ranking docs in about guide. Fixes #1294 (#1295)
* Fix post ranking docs in about guide. Fixes #1294

* Shortening lines to refer to not just posts.

* Fixing top line.
2020-12-01 17:54:37 +00:00
Dessalines
45efa94ba4
Making sure image uploads have jwt cookie. Fixes #1291 (#1299) 2020-12-01 17:48:39 +00:00
cc8a6bea65 Merge branch 'patch-1' of https://github.com/Whayme/lemmy into Whayme-patch-1 2020-12-01 11:32:27 -06:00
Porrumentzio
4aa3180027
Update lemmy_council.md (#1290)
Removed line saying that council members are administrators on official instances, as it is unclear that currently only https://lemmy.ml is a official instance.
2020-12-01 16:25:04 +00:00
Scarlett
7f1ab6a5cd
Replaced table 2020-11-30 13:53:08 -06:00
3a90f69efc Revert "Short intro explanation, reformatted as a table"
This reverts commit 961fc9d0ce.
2020-11-30 14:24:09 -05:00
2e9164584b Version v0.8.8 2020-11-30 12:56:05 -06:00
dessalines
9435994405 Merge pull request 'Add logging to find bug (ref #1283)' (#146) from debug-1283 into main
Reviewed-on: https://yerbamate.ml/LemmyNet/lemmy/pulls/146
2020-11-30 18:49:11 +00:00
8d12c77e26 Remove dbg! to avoid spamming logs 2020-11-30 18:28:31 +01:00
8fc4e1ecfe Add logging to find bug (ref #1283)
Also simplify check_object_domain()
2020-11-30 18:24:10 +01:00
Dessalines
7d7fe5962a
Merge pull request #1285 from Whayme/patch-1
Short intro explanation, reformatted as a table
2020-11-28 16:16:43 -05:00
Scarlett
961fc9d0ce
Short intro explanation, reformatted as a table
More explanation from someone who understands the benefits of the log scale would be helpful.
2020-11-28 00:29:56 -06:00
nutomic
3ceeecc63e Better account deletion (fixes #730) (#143)
Also clear bio

Better account deletion (fixes #730)

Co-authored-by: Felix Ableitner <me@nutomic.com>
Reviewed-on: https://yerbamate.ml/LemmyNet/lemmy/pulls/143
2020-11-27 21:00:18 +00:00
5b66e4860c Merge remote-tracking branch 'yerba/main' into main 2020-11-27 16:00:15 -05:00
5e10cf69b7 Merge remote-tracking branch 'yerba/main' into main 2020-11-27 15:58:22 -05:00
dessalines
050ca88085 Merge pull request 'Set valid context for our extra fields (ref #1220)' (#142) from apub-context into main
Reviewed-on: https://yerbamate.ml/LemmyNet/lemmy/pulls/142
2020-11-27 20:28:52 +00:00
Dessalines
ac330a3f7b
Adding a local RSS feed. Fixes #1279 (#1280)
* Adding a local RSS feed. Fixes #1279

* Shorten get_local_feed and get_all_feed functions

* Making the enum params the same.

Co-authored-by: Felix Ableitner <me@nutomic.com>
2020-11-26 17:26:31 +00:00
Dessalines
01a14e3b3c
Change references of dev.lemmy.ml to lemmy.ml (#1281)
* Change references of dev.lemmy.ml to lemmy.ml

* Remove the dev.lemmy.ml refs in RELEASES.md
2020-11-26 11:47:01 -05:00
a30be1ca5d Merge branch 'eiknat-feature/add-reporting-backend' into main 2020-11-26 13:31:25 +01:00
68173914ca Minor cleanup on reports PR 2020-11-26 13:28:58 +01:00
f070b1823d Make changes on content field backwards compatible 2020-11-26 13:24:14 +01:00
Dessalines
2b5feca806 Trying out cargo chef and a travis docker image cache. (#1238)
* Trying out cargo chef and a travis docker image cache.

* trying to change internal target.

* Use latest cargo-chef with --target

* Remove caching for now.

* Adding back in chowns

* Adding back in cache.

* Remove travis caching.

* Switching dev dockerfile to match prod, using cargo-chef and alpine.

* Make travis happy

* Trying a chown rust.

* Caching cargo-chef first.

* Moving the chowns

* Removing many copy commands.

* Go back to rust 1.47.0 due to config-rs breaking.

* Adding the old volume mount version.

* Adding some script comments.

Co-authored-by: Luca Palmieri <lpalmieri@truelayer.com>
2020-11-26 13:24:13 +01:00
d75f621152 Populate content with HTML, and source with markdown (ref #1220) 2020-11-26 13:24:13 +01:00
8bdbda1db9 Remove clap dependency 2020-11-26 13:24:13 +01:00
a2d80d8f2e Generate valid RSS feed (fixes #1274) 2020-11-26 13:24:13 +01:00
8b5289d5c7 Add TODO about populating user outbox 2020-11-26 13:24:13 +01:00
8fe578c958 Version v0.8.7 2020-11-26 13:24:13 +01:00
ac75304a09 Version v0.8.6 2020-11-26 13:24:13 +01:00
7fe4558bee Create empty outbox for user (ref #1220) 2020-11-26 13:24:12 +01:00
f3eebb1dfc Version v0.8.5 2020-11-26 13:24:12 +01:00
250dcc26be For community_name API parameters, only search locally (fixes #1271) 2020-11-26 13:24:12 +01:00
11fdef56db Dont handle activities twice in inbox 2020-11-26 13:24:12 +01:00
d6493f31d9 Reduce visibility of some structs and methods (replaces #1266) 2020-11-26 13:24:04 +01:00
8b8d47f6f5 Version v0.8.4 2020-11-26 13:22:16 +01:00
aaa3fc08af Add user_inbox check that activities are really addressed to local users 2020-11-26 13:22:16 +01:00
dessalines
9707de4d4a Merge pull request 'Populate content with HTML, and source with markdown (ref #1220)' (#141) from apub-media-type2 into main
Reviewed-on: https://yerbamate.ml/LemmyNet/lemmy/pulls/141
2020-11-25 19:54:52 +00:00
a7b72ed5c4 Set valid context for our extra fields (ref #1220) 2020-11-25 18:44:49 +01:00
b2288fcb9a Make changes on content field backwards compatible 2020-11-25 14:07:04 +01:00
e0e23c2f9d Merge branch 'main' into apub-media-type2 2020-11-25 13:15:29 +01:00
cd3f20e49b Populate content with HTML, and source with markdown (ref #1220) 2020-11-24 18:53:43 +01:00
dessalines
1e187ab4f8 Merge pull request 'Remove clap dependency' (#139) from remove-clap2 into main
Reviewed-on: https://yerbamate.ml/LemmyNet/lemmy/pulls/139
2020-11-23 18:01:39 +00:00
aaeb852f23 Remove clap dependency 2020-11-23 18:48:20 +01:00
Dessalines
d26a7ad337
Trying out cargo chef and a travis docker image cache. (#1238)
* Trying out cargo chef and a travis docker image cache.

* trying to change internal target.

* Use latest cargo-chef with --target

* Remove caching for now.

* Adding back in chowns

* Adding back in cache.

* Remove travis caching.

* Switching dev dockerfile to match prod, using cargo-chef and alpine.

* Make travis happy

* Trying a chown rust.

* Caching cargo-chef first.

* Moving the chowns

* Removing many copy commands.

* Go back to rust 1.47.0 due to config-rs breaking.

* Adding the old volume mount version.

* Adding some script comments.

Co-authored-by: Luca Palmieri <lpalmieri@truelayer.com>
2020-11-23 10:59:06 -05:00
dessalines
911b26f163 Merge pull request 'Generate valid RSS feed (fixes #1274)' (#136) from valid-rss into main
Reviewed-on: https://yerbamate.ml/LemmyNet/lemmy/pulls/136
2020-11-20 18:55:11 +00:00
8920f439ce Generate valid RSS feed (fixes #1274) 2020-11-20 15:11:47 +01:00
405ea38959 Add TODO about populating user outbox 2020-11-19 13:50:43 +01:00
bffc82f752 Version v0.8.7 2020-11-18 16:18:53 -06:00
e2693a4192 Version v0.8.6 2020-11-18 13:12:24 -06:00
c7740a7ac5 Merge remote-tracking branch 'yerba/main' into main 2020-11-18 13:12:08 -06:00
dessalines
c038acea5b Merge pull request 'Create empty outbox for user (ref #1220)' (#135) from user-outbox into main
Reviewed-on: https://yerbamate.ml/LemmyNet/lemmy/pulls/135
2020-11-18 19:13:32 +00:00
48f506277a Create empty outbox for user (ref #1220) 2020-11-18 17:04:35 +01:00
b8dc3c11c1 Version v0.8.5 2020-11-17 15:11:58 -06:00
dessalines
f0e3303aeb Merge pull request 'For community_name API parameters, only search locally (fixes #1271)' (#134) from community-name-local into main
Reviewed-on: https://yerbamate.ml/LemmyNet/lemmy/pulls/134
2020-11-17 21:07:00 +00:00
1ba1e466f7 For community_name API parameters, only search locally (fixes #1271) 2020-11-17 18:05:25 +01:00
dessalines
eb690fc39c Merge pull request 'Dont handle activities twice in inbox' (#133) from inbox-deduplicate-handling into main
Reviewed-on: https://yerbamate.ml/LemmyNet/lemmy/pulls/133
2020-11-17 15:43:11 +00:00
14bf45d099 Dont handle activities twice in inbox 2020-11-16 21:43:52 +01:00
dessalines
eee394cf85 Merge pull request 'Reduce visibility of some structs and methods (replaces #1266)' (#132) from reduce-visibility into main
Reviewed-on: https://yerbamate.ml/LemmyNet/lemmy/pulls/132
2020-11-16 16:36:30 +00:00
8675fed49c Reduce visibility of some structs and methods (replaces #1266) 2020-11-16 16:44:04 +01:00
0dcff2e647 Version v0.8.4 2020-11-12 11:54:31 -06:00
nutomic
1b8ce33aa4 Merge pull request 'Add user_inbox check that activities are really addressed to local users' (#131) from user-inbox-check into main
Reviewed-on: https://yerbamate.ml/LemmyNet/lemmy/pulls/131
2020-11-12 12:50:00 +00:00
eiknat
a68cff51b1 add doc 2020-11-11 15:41:40 -05:00
eiknat
9e604b4038 update/fix migration, add some doc
also run cargo fmt/clippy
2020-11-11 15:11:52 -05:00
eiknat
438414a64b add more details to the report views 2020-11-11 15:11:52 -05:00
eiknat
30d784c27c add mod room websocket impl 2020-11-11 15:11:52 -05:00
eiknat
070efe72af add current context for reports 2020-11-11 15:11:52 -05:00
eiknat
2cd2a4df45 reports: split post/comment out again, add some other fixes 2020-11-11 15:11:52 -05:00
eiknat
e8e0890341 db: fix a few comments i missed 2020-11-11 15:11:52 -05:00
eiknat
d6b1c8df2f reports: update db tables, combine api impl 2020-11-11 15:11:52 -05:00
eiknat
6d43202efb reports: initial reports api commit 2020-11-11 15:11:52 -05:00
08b8e9999b Merge remote-tracking branch 'yerba/main' into main 2020-11-11 13:18:42 -06:00
dessalines
15c77f85c1 Merge pull request 'Add pending status for federated follows' (#130) from pending-follow into main
Reviewed-on: https://yerbamate.ml/LemmyNet/lemmy/pulls/130
2020-11-11 19:18:27 +00:00
fb16f47f2f Add user_inbox check that activities are really addressed to local users 2020-11-11 17:40:45 +01:00
964d95de5c Fix unit tests 2020-11-11 17:28:30 +01:00
64ac4e382a Version v0.8.3 2020-11-11 07:57:33 -06:00
Dessalines
4a61758bde
Merge pull request #1261 from LemmyNet/more-tests-2
Adding some more integration tests for locked posts and bans.
2020-11-10 13:39:01 -05:00
ff8acfb8de Adding some more integration tests for locked posts and bans. 2020-11-10 10:53:35 -06:00
06e82fe761 Add pending status for federated follows 2020-11-10 16:45:10 +01:00
dessalines
94dd335fac Merge pull request 'Enforce post lock in federation inbox' (#129) from enforce-post-lock into main
Reviewed-on: https://yerbamate.ml/LemmyNet/lemmy/pulls/129
2020-11-10 13:16:13 +00:00
dessalines
437809d337 Merge pull request 'Separate logic for user and community inbox' (#123) from rewrite-inbox into main
Reviewed-on: https://yerbamate.ml/LemmyNet/lemmy/pulls/123
2020-11-10 13:14:40 +00:00
e1fd614dd1 Fixed bug where comments with mentions were treated as private message 2020-11-09 19:18:23 +01:00
3b4c3ec074 Enforce post lock in federation inbox 2020-11-09 17:06:54 +01:00
8803e7834f Enforce site and community bans for federated users 2020-11-09 15:29:36 +01:00
b469b6d8d3 Separate logic for user and community inbox
more refactoring with tons of changes:

- inbox functions return LemmyError instead of HttpResponse
- announce is done directly in community inbox
- reorganized functions for handling inbox activities
- additional checks for private messages
- moved inbox handler functions for post, comment, vote into separete file
- ensure that posts, comments etc are addressed to public (ref #1220)
- probably more
2020-11-09 13:42:08 +01:00
7e13970a4f Version v0.8.2 2020-11-06 07:06:50 -06:00
nutomic
7c51a36012 In activity table, remove user_id and add sensitive (#127)
Forgot to add migrations

Add `sensitive` column to activities table, so PMs arent served over HTTP

Remove user_id column from actvity table

Co-authored-by: Felix Ableitner <me@nutomic.com>
Reviewed-on: https://yerbamate.ml/LemmyNet/lemmy/pulls/127
2020-11-06 13:06:47 +00:00
405eb15291 Merge remote-tracking branch 'yerba/main' into main 2020-11-06 07:05:22 -06:00
nutomic
b7d2dac9bf Fix federation of community removal/deletion, added docs (#125)
Adding a federation test for community deletes / removes.

Add missing docs for community deletion/removal (fixes #1250)

Fix federation of community deletion/removal (fixes #1253)

Co-authored-by: Dessalines <tyhou13@gmx.com>
Co-authored-by: Felix Ableitner <me@nutomic.com>
Reviewed-on: https://yerbamate.dev/LemmyNet/lemmy/pulls/125
2020-11-05 20:19:06 +00:00
76193d37da Merge remote-tracking branch 'yerba/main' into main 2020-11-05 14:17:35 -06:00
dessalines
60517f8471 Merge pull request 'Dont send email notifications to banned users (fixes #1251)' (#126) from dont-email-banned-user into main
Reviewed-on: https://yerbamate.dev/LemmyNet/lemmy/pulls/126
2020-11-05 19:14:49 +00:00
c6c74ab1e3 Also check for ban when sending private message notification 2020-11-05 16:13:01 +01:00
Dessalines
9aa700fb10
Merge pull request #1252 from LemmyNet/yerbamate-change-tld
Change domain of yerbamate.dev to yerbamate.ml
2020-11-05 08:41:12 -05:00
d2bea09a60 Dont send email notifications to banned users (fixes #1251) 2020-11-05 13:43:46 +01:00
45dced3fcb Change domain of yerbamate.dev to yerbamate.ml 2020-11-03 22:50:59 +01:00
Dessalines
5e2a5c0266
Upgrading pictrs to v0.2.5 (#1249) 2020-11-02 18:12:21 +00:00
Dessalines
18d9811de7
Merge pull request #1246 from knkski/pictrs-url-variable
Fix hardcoded pictrs URL reference
2020-11-01 22:45:03 -05:00
Dessalines
cca0b4c0f9
Merge pull request #1247 from knkski/iframely-url-setting
Add iframely_url setting
2020-11-01 22:44:28 -05:00
eab9123b86 Merge branch 'main' of https://github.com/lemmynet/lemmy into main 2020-11-01 21:26:36 -06:00
0ea4095238 Changing docs formattin, running format. 2020-11-01 21:26:00 -06:00
Kenneth Koski
b3035e21ef
Parameterize docs directory (#1245)
Adds `docs_dir` setting for configurable documentation location
2020-11-01 22:21:15 -05:00
Kenneth Koski
3d872b03e3
Add iframely_url setting 2020-10-31 17:08:03 -05:00
Kenneth Koski
1ae335e058
Fix hardcoded pictrs URL reference
Reads the URL from Settings instead
2020-10-31 16:52:54 -05:00
Kenneth Koski
09498d8bb3
Update configuration documentation 2020-10-31 16:28:09 -05:00
Kenneth Koski
bfd8f52497
Parameterize config file location
Allows `config.hjson` to be located on a configurable path.
2020-10-31 16:25:52 -05:00
eiknat
fc36ae22c9
GetUserDetails doesnt return users own email (#1240)
* user: GetUserDetails doesnt return users own email

* user: rename get_user to get_user_dangerous, apply suggested changes
2020-10-30 18:19:47 -04:00
1fd5486def Fixing missing community doc. 2020-10-30 12:46:11 -05:00
Dessalines
7ef044231f
Update cargo deps, upgrading lettre. #789 (#1234)
* Update cargo deps, upgrading lettre. #789

* Adding a comment

* Adding some better expect messages.

* Fixing lettre email.
2020-10-30 13:19:00 -04:00
Dessalines
9e5e0eb9b7
Merge pull request #1242 from ShadowJonathan/patch-1
Update about_features.md
2020-10-30 11:28:53 -04:00
Jonathan de Jong
d3b07c8131
Update about_features.md
...in accordance with the readme.
2020-10-30 09:46:30 +01:00
dessalines
3bf885329d Merge pull request 'Ignore incoming activities which have been received before, add /activities endpoint' (#118) from activity-checks into main
Reviewed-on: https://yerbamate.dev/LemmyNet/lemmy/pulls/118
2020-10-27 16:26:16 +00:00
dessalines
e95ca66c9f Merge pull request 'In comment create/update, include parent creator in cc (ref #698)' (#122) from comment-parent-cc into main
Reviewed-on: https://yerbamate.dev/LemmyNet/lemmy/pulls/122
2020-10-27 16:15:20 +00:00
9e24eda752 In comment create/update, include parent creator in cc (ref #698) 2020-10-27 16:57:44 +01:00
77b17c6737 Fixing cache dev dockerfile 2020-10-27 10:57:40 -05:00
91d073c2e8 Use docker cache for docker/dev/ 2020-10-27 13:58:52 +01:00
4c0c558945 Merge branch 'main' of https://github.com/lemmynet/lemmy into main 2020-10-26 19:18:28 -05:00
5b871d3dd8 Merge branch 'prod_dockerfile_update' into main 2020-10-26 19:16:33 -05:00
2e922d602d Trying a target fix. 2020-10-26 17:32:50 -05:00
3100e8bf21 Trying a target fix. 2020-10-26 15:50:37 -05:00
b42b461418 Trying a permissions fix. 2020-10-26 13:35:19 -05:00
Dessalines
604d125a55
Simplifying prod build, using musl stable. (#1235) 2020-10-26 17:33:43 +00:00
ba0680f5e6 Simplifying prod build, using musl stable. 2020-10-26 12:18:47 -05:00
6eab24270b Merge remote-tracking branch 'yerba/main' into main 2020-10-26 10:34:40 -05:00
dessalines
53c9094d46 Merge pull request 'Limit amount of HTTP requests to handle activities (fixes #1221)' (#117) from request-limit into main
Reviewed-on: https://yerbamate.dev/LemmyNet/lemmy/pulls/117
2020-10-26 15:35:39 +00:00
dessalines
60bc62932b Merge pull request 'docker/lemmy.hjson is missing field email.use_tls (fixes #1231)' (#120) from lemmy-config-use-tls into main
Reviewed-on: https://yerbamate.dev/LemmyNet/lemmy/pulls/120
2020-10-26 13:49:05 +00:00
99abc49040 Add /activities endpoint (ref #1220) 2020-10-26 12:54:27 +01:00
Dessalines
6337762ec7
Adding image endpoints to docs. Fixes #1226 (#1232) 2020-10-26 11:07:23 +00:00
c33918c71f docker/lemmy.hjson is missing field email.use_tls (fixes #1231) 2020-10-26 11:28:32 +01:00
4f0059198e Updating readme to ref join.lemmy.ml 2020-10-25 10:34:22 -05:00
134d66924e Version v0.8.1 2020-10-24 15:15:48 -05:00
295c209c67 Updating lemmy.hjson config. 2020-10-24 15:14:58 -05:00
5e0562e80e Fix matrix room link. 2020-10-24 15:10:22 -05:00
Dessalines
ad1ba85eb9
Merge pull request #1229 from IronOxidizer/fix-docker-dev
Removed docker root prefix, add pictrs dir
2020-10-23 14:04:30 -04:00
Iron Oxidizer
ac79496036 Removed docker root prefix, add pictrs dir 2020-10-23 13:14:30 -04:00
6d17d5ead2 Ignore incoming activities which have been received before (ref #1220) 2020-10-23 14:29:56 +02:00
Dessalines
cfa60dcb76
Merge pull request #1225 from LemmyNet/fix_remove_icons
Fix invalid url error when removing icons. Fixes #1219
2020-10-22 14:56:55 -04:00
Dessalines
52c679a3cd
Merge pull request #1223 from LemmyNet/remove_cache_headers
Remove cache headers. Fixes #1222
2020-10-22 14:56:43 -04:00
dessalines
73ccbb1bc8 Merge pull request 'Organise activity receive files by object type, not by activity type' (#115) from inbox-refactoring-2 into main
Reviewed-on: https://yerbamate.dev/LemmyNet/lemmy/pulls/115
2020-10-22 18:55:28 +00:00
dessalines
de8d8542b4 Merge pull request 'Dont allow localhost or raw IPs in activitypub IDs (ref #1221)' (#116) from disallow-localhost-urls into main
Reviewed-on: https://yerbamate.dev/LemmyNet/lemmy/pulls/116
2020-10-22 18:55:11 +00:00
a897ec48d2 Merge remote-tracking branch 'yerba/main' into main 2020-10-22 13:55:01 -05:00
3d5647b16f Limit amount of HTTP requests to handle activities (fixes #1221) 2020-10-22 20:27:32 +02:00
18111629fa Fix invalid url error when removing icons. Fixes #1219 2020-10-22 12:23:25 -05:00
e7d3905093 Remove cache headers. Fixes #1222 2020-10-22 11:53:53 -05:00
b08e0a6415 Dont allow localhost or raw IPs in activitypub IDs (ref #1221) 2020-10-22 18:12:43 +02:00
Dessalines
2527c59e55
Fixing ansible for ubuntu 20.04 (#1218)
* Fixing ansible for ubuntu 20.04

* Changing federation to false.
2020-10-22 14:32:56 +00:00
1a3b96b054 Organise activity receive files by object type, not by activity type 2020-10-21 19:37:50 +02:00
dessalines
3e22c99c96 Merge pull request 'Also return json for long accept header with profile link (ref #1216)' (#114) from json-headers into main
Reviewed-on: https://yerbamate.dev/LemmyNet/lemmy/pulls/114
2020-10-21 16:33:31 +00:00
a62c96f85e Merge remote-tracking branch 'yerba/main' into main 2020-10-21 11:32:25 -05:00
Dessalines
8a6bfa3816
Merge pull request #1217 from lo48576/fix/outdated-readme-link
Fix outdated links in README
2020-10-21 09:55:17 -04:00
e8379cb3f7 Also return json for long accept header with profile link (ref #1216) 2020-10-21 15:48:43 +02:00
YOSHIOKA Takuma
8bfd678d49
Fix outdated links in README 2020-10-21 22:23:09 +09:00
Dessalines
1b62e65a5d
Fix IP forwarding headers. Fixes #1214 (#1215) 2020-10-21 09:47:45 +00:00
Dessalines
dd99e77881
Upgrade pictrs to v0.2.4-r0 (#1212) 2020-10-20 10:44:55 +00:00
Dessalines
18b3eab909
Merge pull request #1209 from LemmyNet/fix-actor-name-confusion
Swap name and preferredUsername apub fields
2020-10-19 11:52:35 -04:00
Dessalines
7673e60654
Merge pull request #1210 from LemmyNet/apub-code-docs
Create rustdoc for activitypub code
2020-10-19 11:47:17 -04:00
695272f980 Create rustdoc for activitypub code 2020-10-19 16:29:35 +02:00
e190ecbefb Make lemmy-ui restart: always 2020-10-19 12:18:05 +02:00
Dessalines
37f616bad2
Adding some comments to defaults.hjson (#1207)
* Adding some comments to defaults.hjson

* Link to docs for allow/blocklist.
2020-10-16 20:46:40 +00:00
06a6bab2c1 Swap name and preferredUsername apub fields 2020-10-16 22:44:40 +02:00
cead2a6303 Version v0.8.0 2020-10-16 09:12:54 -05:00
0c4f99b161 Adding to releases.md 2020-10-16 09:10:43 -05:00
815cf60f45 Fixing clippy. 2020-10-16 09:09:37 -05:00
Dessalines
571c71392e
Adding API and APUB URL checks for banners and icons. Fixes #1199 (#1200)
* Adding API and APUB URL checks for banners and icons. Fixes #1199

* Adding a check optional url.

* Missed a few.
2020-10-15 18:23:56 +00:00
Dessalines
24484ed801
Adding references to lemmy types from docs. (#1203)
* Adding references to lemmy types from docs.

* Updating TOC.

* Updating docs.
2020-10-15 17:56:16 +00:00
Dessalines
3e35e4052f
Merge pull request #1206 from LemmyNet/verify-activity-domains2
Verify activity domains 2
2020-10-15 11:18:57 -04:00
Dessalines
a2df2b12e2
Merge pull request #1201 from LemmyNet/verify-activity-domains
Add method verify_activity_domains_valid() (ref #1196)
2020-10-15 11:15:25 -04:00
fe15ff3c51 Also verify activity domains in shared inbox (fixes #1196) 2020-10-15 15:38:49 +02:00
39cbe5f31f Add method verify_activity_domains_valid() (ref #1196) 2020-10-15 15:38:03 +02:00
Dessalines
23793fe096
Merge pull request #1202 from LemmyNet/pict-rs-v2-dess
Pict rs v2 dess
2020-10-14 15:46:02 -04:00
c87a009b37 Altering lemmy pict-rs-v2 forwarding. 2020-10-14 11:48:10 -05:00
9bbacd38f4 Merge branch 'asonix/pict-rs-v2' of https://github.com/asonix/lemmy into asonix-asonix/pict-rs-v2 2020-10-14 10:14:25 -05:00
c9c3d94bf9 Merge remote-tracking branch 'yerba/main' into main 2020-10-13 13:30:32 -05:00
dessalines
d55e38d4a6 Merge pull request 'Dont include full objects with remove/delete activities' (#113) from delete-with-id into main
Reviewed-on: https://yerbamate.dev/LemmyNet/lemmy/pulls/113
2020-10-13 18:31:11 +00:00
3f9ede79ed Add domain checks for private message inbox 2020-10-13 18:06:26 +02:00
ac0cd7bc68 Dont include full objects with remove/delete activities 2020-10-13 17:47:05 +02:00
56d361eeff
Update activitypub outline (#1183)
* Update activitypub outline

* Various adjustments of apub outline
2020-10-13 09:00:35 -04:00
dessalines
9084a56e36 Merge pull request 'ActivityPub refactoring' (#112) from apub-changes into main
Reviewed-on: https://yerbamate.dev/LemmyNet/lemmy/pulls/112
2020-10-12 17:28:48 +00:00
7cfcf0acec Change the way that to is set in apub 2020-10-12 18:02:28 +02:00
2ad60379e4 Add to field for follow, undo follow 2020-10-12 16:45:40 +02:00
0dda2577e1 Refactor apub code, split up large files 2020-10-12 16:10:09 +02:00
cc9bb56d2d
Merge pull request #1192 from LemmyNet/fix_fast_triggers_2
Fixing hot_rank_active fast triggers. Fixes #1190
2020-10-12 10:33:46 +00:00
f44612970a
Merge pull request #1189 from LemmyNet/fix_search_filter
Fixing post community filter. Fixes #1187
2020-10-12 10:31:15 +00:00
70f7dd876f
Merge pull request #1188 from LemmyNet/fix_blocked_creator_outbox
Fixed an issue with blocked post creators in outbox.
2020-10-12 10:30:48 +00:00
b19ac26325
Merge pull request #1191 from LemmyNet/update_deps
Updating deps.
2020-10-12 10:28:32 +00:00
4010a944a4 Bump pict-rs version 2020-10-11 13:57:35 -05:00
863a662ec6 Stable release 2020-10-10 20:54:15 -05:00
08588c873a pict-rs v2 2020-10-10 19:31:56 -05:00
a903cae00b Fixing hot_rank_active fast triggers. Fixes #1190 2020-10-09 23:03:38 -05:00
907f8fff4c Updating deps. 2020-10-09 22:51:47 -05:00
1c8913090d Fixing post community filter. Fixes #1187 2020-10-09 13:09:00 -05:00
Dessalines
e61c3f0de3
Merge pull request #1185 from LemmyNet/federation-disable-downvotes
Respect disable downvotes setting when federating
2020-10-09 13:58:29 -04:00
cb4a3a03a2 Fixed an issue with blocked post creators in outbox.
- Fixes #1186
2020-10-09 12:46:27 -05:00
c90c96fbf6 Respect disable downvotes setting when federating 2020-10-09 15:41:40 +02:00
97fc51b0cd Version v0.7.64 2020-10-08 18:34:30 -05:00
0b4ecdfc05 Fixing a bug with stickied order. 2020-10-08 18:33:54 -05:00
875b0e6f01 Version v0.7.63 2020-10-08 17:50:51 -05:00
03b1821586 Version v0.7.62 2020-10-08 15:52:10 -05:00
Dessalines
8d0580461b
Merge pull request #1180 from LemmyNet/no_conflict_triggers
No send blocked and no conflict triggers
2020-10-08 14:51:04 -04:00
7fbad900d7 Addressing a few comments. 2020-10-08 12:38:44 -05:00
Dessalines
f5184ce749
Fixing local filter, fixes #1181 (#1182) 2020-10-08 11:10:10 -04:00
e9ce14069e Removing some unecessary logging. 2020-10-07 21:55:15 -05:00
c08d891742 Merge branch 'main' into no_conflict_triggers 2020-10-07 20:57:29 -05:00
b4c730e537 Revert "Removing on_conflict as it may not work with table triggers (user_fast, etc)"
This reverts commit db7027a367.
2020-10-07 19:08:06 -05:00
fd257a6d39 Adding no conflict triggers. Fixes #1179 2020-10-07 19:05:46 -05:00
299598f0c4
Remove unused fields on community/user json (#1178) 2020-10-07 14:19:12 -04:00
f00cfa005e Adding submodule update --remote 2020-10-07 11:09:56 -05:00
9dc6294065
Explain how federation works for admins, various doc updates (#1176) 2020-10-07 12:08:03 -04:00
9fe3efcb32 Trimming allowed and blocked instances 2020-10-07 09:40:36 -05:00
30431199cf Merge remote-tracking branch 'origin/no-send-blocked' into no-send-blocked-dess 2020-10-06 12:33:18 -05:00
db7027a367 Removing on_conflict as it may not work with table triggers (user_fast, etc) 2020-10-06 12:31:16 -05:00
26883208cd Create separate SendActivityTask for each destination 2020-10-06 19:19:53 +02:00
60730e81d9 Avoid duplicate comment send, better activity logging 2020-10-06 18:28:31 +02:00
ca4868cefd Adding a boolean check to send_activity_internal 2020-10-06 10:19:01 -05:00
9e84fe20e6 Dont send mentions to inbox of local community
also, dont start SendActivityTask for empty `to`, and remove
useless comment
2020-10-06 14:58:37 +02:00
c6c107cad1
Merge pull request #1173 from LemmyNet/password_length_check
Password length check. Fixes #1087
2020-10-05 20:24:07 +00:00
5e92fb47ed Changing one more darkly to browser. #1163 2020-10-05 11:53:13 -05:00
984f1ae7fb Merge remote-tracking branch 'yerba/main' into main 2020-10-05 11:52:04 -05:00
dessalines
38f4c5f310 Merge pull request 'Update lemmy-ui version in docker-compose files on release (fixes #1164)' (#111) from update-lemmy-ui into main
Reviewed-on: https://yerbamate.dev/LemmyNet/lemmy/pulls/111
2020-10-05 16:49:51 +00:00
dc18ec533e
Merge pull request #1174 from LemmyNet/add_community_name_search
Adding optional community_name field to search. Fixes #1057
2020-10-05 15:55:33 +00:00
07ddeef568
Merge pull request #1170 from LemmyNet/improve_admin_docs
Improving administration page docs. Fixes #1160
2020-10-05 15:49:11 +00:00
14c3d10ba9
Merge pull request #1169 from LemmyNet/new_default_theme
Changing default theme to browser from darkly. Fixes #1163
2020-10-05 15:47:26 +00:00
4a925c9e9f
Merge pull request #1172 from LemmyNet/remove_kubernetes
Remove kubernetes. #842
2020-10-05 15:38:51 +00:00
5a56c08c91 Update lemmy-ui version in docker-compose files on release (fixes #1164) 2020-10-05 17:36:53 +02:00
dessalines
75ace1192a Merge pull request 'Only search locally for Community::read_from_name and similar (ref #698)' (#110) from read-only-local into main
Reviewed-on: https://yerbamate.dev/LemmyNet/lemmy/pulls/110
2020-10-05 15:25:09 +00:00
c348e788e4 Adding optional community_name field to search. Fixes #1057 2020-10-04 19:57:35 -05:00
557d476001 Password length check. Fixes #1087 2020-10-04 19:10:15 -05:00
9b69c446e2 Remove kubernetes. #842 2020-10-04 15:44:26 -05:00
4d0365bca3 Replace gitlab with codeberg. Fixes #1171 2020-10-04 14:01:12 -05:00
990987b174 Improving administration page docs. Fixes #1160 2020-10-04 08:42:30 -05:00
04a4624f14 Changing default theme to browser from darkly. Fixes #1163 2020-10-03 11:44:25 -05:00
7cc5f53cad Add line showing default ports for nginx. Fixes #1167 2020-10-03 09:53:48 -05:00
389e929c8e Fixing readme. 2020-10-03 09:51:15 -05:00
048fe287c2 Running cargo fmt. 2020-10-03 09:50:06 -05:00
9fa2092a21 Adding some logging. 2020-10-03 09:47:06 -05:00
2e2b6eacd7 Fixing pretty print again. 2020-10-02 10:15:26 -05:00
eef0a5c7e8 Adding pretty print for activities. 2020-10-02 09:21:14 -05:00
e596ea6e3a
Add documentation for federation overview (fixes #774) (#1165) 2020-10-02 09:02:16 -04:00
15adc21e1f Only search locally for Community::read_from_name and similar (ref #698) 2020-10-02 14:18:20 +02:00
f5b511ccce Merge branch 'main' into no-send-blocked-dess 2020-10-01 15:57:47 -05:00
2ad137c280 Merge branch 'remove-hardcoded-https-dess' into main 2020-10-01 12:56:04 -05:00
3a24adc57f Renaming to sign_and_send 2020-10-01 12:54:20 -05:00
61f013e4cb Trying to fix travis build. 2020-10-01 09:16:56 -05:00
6c8a5723f8 Trying to add some long delays. 2020-10-01 08:32:01 -05:00
a4cb067130 Dont send to blocked instances, rewrite activity_sender 2020-09-30 20:35:02 +02:00
b074a963fe Version v0.7.61 2020-09-30 20:28:58 +02:00
Dessalines
0ebd830814 More overwriteable fields (#1155)
* Adding more overwriteable fields for user. Fixes #1154

* Adding a note for bio.
2020-09-30 20:28:58 +02:00
Dessalines
4772cbd23f
Merge pull request #1162 from asonix/remove-hardcoded-https
Use http-signature-normalization-reqwest
2020-09-30 09:28:40 -04:00
1fc21aed1c Use http-signature-normalization-reqwest 2020-09-29 20:08:50 -05:00
c1db1042ad Also sign the digest header 2020-09-29 16:46:49 +02:00
0aa0ea19fb Use reqwest to send activities 2020-09-29 15:10:55 +02:00
ada82582ef Version v0.7.61 2020-09-25 16:26:19 -05:00
927ab1f040 Remove hardcoded usage of https (fixes #1126) 2020-09-25 17:33:00 +02:00
Dessalines
8bea13d651
More overwriteable fields (#1155)
* Adding more overwriteable fields for user. Fixes #1154

* Adding a note for bio.
2020-09-25 11:16:49 -04:00
Dessalines
10b3d9005f
Merge pull request #1152 from LemmyNet/upgrade_api_test_deps
Upgrade api test deps
2020-09-24 22:50:32 -04:00
5dbb2b45cb Upgrade api test deps. 2020-09-24 13:22:11 -05:00
nutomic
bfed8a8be4 Dont federate embeds, but refetch them for security (#106)
Dont federate embeds, but refetch them for security (#ref 647)

Co-authored-by: Felix Ableitner <me@nutomic.com>
Reviewed-on: https://yerbamate.dev/LemmyNet/lemmy/pulls/106
2020-09-24 17:43:42 +00:00
4de80dc29d Merge remote-tracking branch 'yerba/main' into main 2020-09-24 09:14:09 -05:00
nutomic
442369a041 Move websocket code into workspace (#107)
Adjust dockerfiles, fix cargo.toml and remove unused deps

Merge branch 'main' into move-websocket-to-workspace

Move api code into workspace

Move apub to separate workspace

Move websocket code into separate workspace

Some code cleanup

Remove websocket dependency on API

Co-authored-by: Felix Ableitner <me@nutomic.com>
Reviewed-on: https://yerbamate.dev/LemmyNet/lemmy/pulls/107
2020-09-24 13:53:21 +00:00
nutomic
e8ea0664ef Fix nginx config for local federation setup (#104)
Fix depends_on

Add note about different port for backend in federation docs

Fix nginx config for local federation setup

Co-authored-by: Felix Ableitner <me@nutomic.com>
Reviewed-on: https://yerbamate.dev/LemmyNet/lemmy/pulls/104
2020-09-24 13:50:38 +00:00
Dessalines
698fb20c12
Fixing ansible install. (#1150)
* Fixing ansible install.

* Fixing LEMMY_EXTERNAL_HOST docs.
2020-09-24 09:47:24 -04:00
Dessalines
c6888472dc
Add linked instances. (#1149)
* Adding linked instances. Fixes #1147

* Removing current instance, checking for federation enabled.

* Cleaning up.

* Switching to iterator.
2020-09-24 09:46:57 -04:00
ab17e0a9f3 Merge branch 'add-view-helper-functions' of https://github.com/eiknat/lemmy into eiknat-add-view-helper-functions 2020-09-23 16:16:18 -05:00
b99b62a211 Version v0.7.59 2020-09-23 08:58:30 -05:00
1dcf14289d Removing weblate translations from deploy. 2020-09-23 08:56:32 -05:00
5732fde4d9 Updating issue template to show lemmy-ui. 2020-09-22 14:32:18 -05:00
440dba4372
Merge pull request #1146 from LemmyNet/fix_nginx_docs
Fix nginx docs, fix test deploy.
2020-09-22 19:02:46 +00:00
2bee3ac33b Fix nginx docs, fix test deploy. 2020-09-22 11:28:16 -05:00
2189d15e5b Merge branch 'main' of https://github.com/lemmynet/lemmy into main 2020-09-22 10:12:36 -05:00
66cf9ff70a Merge remote-tracking branch 'yerba/main' into main 2020-09-22 10:12:26 -05:00
dessalines
8e69801e24 Merge pull request 'Add integration test to ensure that signatures are verified' (#103) from integration-test into main
Reviewed-on: https://yerbamate.dev/LemmyNet/lemmy/pulls/103
2020-09-22 15:10:28 +00:00
b5860a4283
Merge pull request #1145 from LemmyNet/fix_remote_subscribe
Fixing remote subscribe result. Fixes #1144
2020-09-21 19:09:48 +00:00
aece5e67b7 Address review comments 2020-09-21 17:24:42 +02:00
9395312079 Fixing remote subscribe result. Fixes #1144 2020-09-21 09:35:13 -05:00
Dessalines
8a7eb57c24
Add openssl (#1143)
* Adding openssl.

* Openssl didnt work, switching to rustls.
2020-09-21 10:08:47 -04:00
12af0f462f Update federation docs 2020-09-21 14:02:40 +02:00
db3dcc6fdc Add tests for community_inbox and user_inbox 2020-09-18 17:45:50 +02:00
9d4973829b Add integration test to ensure that signatures are verified 2020-09-18 16:37:46 +02:00
Dessalines
7bdb1abbe6
Published user time (#1141)
* Adding published time to UserForm.

- Federates user creation time. Fixes #1140

* Check the user published time.
2020-09-18 11:04:12 +00:00
dessalines
fc525c8144 Merge pull request 'Add tests for avatars, banners and more' (#102) from more-tests into main
Reviewed-on: https://yerbamate.dev/LemmyNet/lemmy/pulls/102
2020-09-17 18:22:07 +00:00
cdb02c992c Add tests for avatars, banners and more 2020-09-17 17:41:51 +02:00
9f4493a0b2 Updating cargo.lock. 2020-09-16 12:37:09 -05:00
397fc74a56 Merge branch 'update-deps' into main 2020-09-16 12:35:37 -05:00
93f681bf7a Turn off email notifications. 2020-09-16 09:24:42 -05:00
96535366c3 Running cargo fmt. 2020-09-16 08:31:30 -05:00
3fbc8b130d Adding site checking. 2020-09-16 08:29:51 -05:00
dd93ac49a3 Merge branch 'fix/add-check-to-create-site-endpoint' of https://github.com/eiknat/lemmy into eiknat-fix/add-check-to-create-site-endpoint 2020-09-16 08:13:19 -05:00
cf4cda2434 Update deps, remove rustls, dont specify patch version 2020-09-16 14:52:20 +02:00
98c086abb9 Move websocket structs into lemmy_structs (ref #1115) 2020-09-16 13:45:31 +02:00
eiknat
d0fefca6f9 api.site: check for existing site before creating 2020-09-15 21:59:59 -04:00
Dessalines
5c6258390c
Isomorphic docker (#1124)
* Adding a way to GetComments for a community given its name only.

* Adding getcomments to api docs.

* A first pass at locally working isomorphic integration.

* Testing out cargo-husky.

* Testing a fail hook.

* Revert "Testing a fail hook."

This reverts commit 0941cf1736.

* Moving server to top level, now that UI is gone.

* Running cargo fmt using old way.

* Adding nginx, fixing up docker-compose files, fixing docs.

* Trying to re-add API tests.

* Fixing prod dockerfile.

* Redoing nightly fmt

* Trying to fix private message api test.

* Adding CommunityJoin, PostJoin instead of joins from GetComments, etc.

- Fixes #1122

* Fixing fmt.

* Fixing up docs.

* Removing translations.

* Adding apps / clients to readme.

* Fixing main image.

* Using new lemmy-isomorphic-ui with better javascript disabled.

* Try to fix image uploads in federation test

* Revert "Try to fix image uploads in federation test"

This reverts commit a2ddf2a90b.

* Fix post url federation

* Adding some more tests, some still broken.

* Don't need gitattributes anymore.

* Update local federation test setup

* Fixing tests.

* Fixing travis build.

* Fixing travis build, again.

* Changing lemmy-isomorphic-ui to lemmy-ui

* Error in travis build again.

Co-authored-by: Felix Ableitner <me@nutomic.com>
2020-09-15 15:26:47 -04:00
eiknat
b69524b498
routes.api: fix get_captcha endpoint (#1135) 2020-09-14 21:22:53 -04:00
nutomic
1870dc8cd9 Split lemmy_utils into multiple files (#96)
Update dependencies

Move send_local_notifs into lemmy_api_structs (ref #1115)

Split lemmy_utils into multiple files

Co-authored-by: Felix Ableitner <me@nutomic.com>
Reviewed-on: https://yerbamate.dev/LemmyNet/lemmy/pulls/96
2020-09-14 15:29:50 +00:00
762c1347ed Merge remote-tracking branch 'yerba/main' into main 2020-09-14 09:12:13 -05:00
dessalines
3bf86e4ca0 Merge pull request 'Remove brotli dependency' (#97) from remove-brotli into main
Reviewed-on: https://yerbamate.dev/LemmyNet/lemmy/pulls/97
2020-09-14 14:12:02 +00:00
29355c749e Remove brotli dependency 2020-09-14 12:23:05 +02:00
eday
26816b9366 db: add utils schema/funcs for view handling qol
Creates a new "utils" db schema with a simple table and two functions under "utils".  The functions take a 'schema' and 'table'- One will get all dependent views and save the ddl, the other will execute stored ddl skipping errors.  Mostly for QOL with testing migrations and making migration files smaller
2020-09-13 22:58:13 -04:00
Marcin Wojnarowski
c239b9af83
Api docs corrections (#1131)
* corrected endpoint names/methods

* small typo fix
2020-09-13 11:20:24 -04:00
934a26f1ef Merge remote-tracking branch 'weblate/main' into main 2020-09-12 17:48:24 -05:00
riccardo
fbc0c28f5e Translated using Weblate (Italian)
Currently translated at 98.9% (281 of 284 strings)

Translation: Lemmy/lemmy
Translate-URL: http://weblate.yerbamate.dev/projects/lemmy/lemmy/it/
2020-09-12 02:58:32 +00:00
Rob Ede
986dc3f52c
update actix-web to v3 stable (#1125) 2020-09-11 21:37:25 -04:00
Lee KiWon
8660caae9b Translated using Weblate (Korean)
Currently translated at 100.0% (284 of 284 strings)

Translation: Lemmy/lemmy
Translate-URL: http://weblate.yerbamate.dev/projects/lemmy/lemmy/ko/
2020-09-04 16:43:00 +00:00
kleeon
1a6dc72898 Translated using Weblate (Russian)
Currently translated at 100.0% (284 of 284 strings)

Translation: Lemmy/lemmy
Translate-URL: http://weblate.yerbamate.dev/projects/lemmy/lemmy/ru/
2020-09-04 16:42:59 +00:00
Filip Bengtsson
87cd4712e0 Translated using Weblate (Swedish)
Currently translated at 100.0% (284 of 284 strings)

Translation: Lemmy/lemmy
Translate-URL: http://weblate.yerbamate.dev/projects/lemmy/lemmy/sv/

Translated using Weblate (Swedish)

Currently translated at 98.5% (280 of 284 strings)

Translation: Lemmy/lemmy
Translate-URL: http://weblate.yerbamate.dev/projects/lemmy/lemmy/sv/
2020-09-04 00:28:35 +00:00
kleeon
361655dd42 Translated using Weblate (Russian)
Currently translated at 98.9% (281 of 284 strings)

Translation: Lemmy/lemmy
Translate-URL: http://weblate.yerbamate.dev/projects/lemmy/lemmy/ru/
2020-09-04 00:28:35 +00:00
Lucy
234411ff9a Translated using Weblate (Irish)
Currently translated at 100.0% (284 of 284 strings)

Translation: Lemmy/lemmy
Translate-URL: http://weblate.yerbamate.dev/projects/lemmy/lemmy/ga/
2020-09-04 00:28:35 +00:00
Lee KiWon
88122e0e76 Translated using Weblate (Korean)
Currently translated at 100.0% (284 of 284 strings)

Translation: Lemmy/lemmy
Translate-URL: http://weblate.yerbamate.dev/projects/lemmy/lemmy/ko/

Translated using Weblate (Korean)

Currently translated at 100.0% (284 of 284 strings)

Translation: Lemmy/lemmy
Translate-URL: http://weblate.yerbamate.dev/projects/lemmy/lemmy/ko/
2020-09-04 00:28:35 +00:00
nutomic
bd0e69b2bb Various things refactored (#95)
Remove unused derive macros

lemmy_rate_limit doesnt depend on lemmy_api_structs anymore

Dont use "pub extern crate"

Co-authored-by: Felix Ableitner <me@nutomic.com>
Reviewed-on: https://yerbamate.dev/LemmyNet/lemmy/pulls/95
2020-09-03 19:45:12 +00:00
af364e7fe0 Version v0.7.57 2020-09-03 09:40:59 -05:00
b6eecfd39d Merge remote-tracking branch 'weblate/main' into main 2020-09-03 09:40:57 -05:00
17df0ee6b3 Merge branch 'structs_separate' into main 2020-09-03 09:02:03 -05:00
730cb6ce67 Merge branch 'main' into structs_separate 2020-09-03 09:01:20 -05:00
Dessalines
2aaf4228ac
Local timeline (#1111)
* Adding a local filter. Fixes #1103

* Not showing local if there are no federated instances.
2020-09-03 09:58:33 -04:00
4a4629763e Fixing user search leaking emails. 2020-09-03 08:48:26 -05:00
7101ac1b4b Merge branch 'main' into fix_docker_caching 2020-09-03 08:06:05 -05:00
3a6982e7b2 Adding rate_limiter and api_structs. 2020-09-02 18:17:35 -05:00
88978077b5 Merge branch 'fix_docker_caching' into structs_separate 2020-09-02 16:53:46 -05:00
8015f560d6 Adding in a more reliable docker dev build. (The other wouldn't use buildkit). 2020-09-02 15:41:49 -05:00
e3140235de Use romacs cargo-build-deps tool. 2020-09-02 10:42:48 -05:00
353e2e027a Move api structs and rate limit into separate workspaces 2020-09-02 13:27:31 +02:00
Filip Bengtsson
b0b93ba1e6 Translated using Weblate (Swedish)
Currently translated at 98.5% (280 of 284 strings)

Translation: Lemmy/lemmy
Translate-URL: http://weblate.yerbamate.dev/projects/lemmy/lemmy/sv/
2020-09-02 03:30:02 +00:00
kleeon
1d156e3854 Translated using Weblate (Russian)
Currently translated at 98.9% (281 of 284 strings)

Translation: Lemmy/lemmy
Translate-URL: http://weblate.yerbamate.dev/projects/lemmy/lemmy/ru/
2020-09-02 03:30:02 +00:00
Lucy
b0af949480 Translated using Weblate (Irish)
Currently translated at 100.0% (284 of 284 strings)

Translation: Lemmy/lemmy
Translate-URL: http://weblate.yerbamate.dev/projects/lemmy/lemmy/ga/
2020-09-02 03:30:02 +00:00
Lee KiWon
09535e294a Translated using Weblate (Korean)
Currently translated at 100.0% (284 of 284 strings)

Translation: Lemmy/lemmy
Translate-URL: http://weblate.yerbamate.dev/projects/lemmy/lemmy/ko/

Translated using Weblate (Korean)

Currently translated at 100.0% (284 of 284 strings)

Translation: Lemmy/lemmy
Translate-URL: http://weblate.yerbamate.dev/projects/lemmy/lemmy/ko/
2020-09-02 03:30:01 +00:00
dfa9cb57aa Running cargo +nightly fmt. 2020-09-01 16:44:56 -05:00
5d64dfd4dc Remove wildcard imports (in particular super::*) 2020-09-01 15:20:22 +02:00
dc1bc741b4 Fixing docker caching. 2020-08-31 16:47:31 -05:00
34e539cdc0 Updating dev docker-compose. 2020-08-31 13:39:01 -05:00
dessalines
ad7dfb0181 Merge pull request 'Simplify docker federation setup' (#92) from update-docker-federation into main
Reviewed-on: https://yerbamate.dev/LemmyNet/lemmy/pulls/92
2020-08-31 16:50:43 +00:00
dessalines
c4dd28e252 Merge pull request 'Refactor websocket to split it into multiple files' (#91) from refactor-websocket into main
Reviewed-on: https://yerbamate.dev/LemmyNet/lemmy/pulls/91
2020-08-31 16:43:16 +00:00
147972273a Simplify docker federation setup 2020-08-31 17:32:21 +02:00
b15c406924 Refactor websocket to split it into multiple files 2020-08-31 17:20:13 +02:00
nutomic
d4dccd17ae implement ActivitySender actor (#89)
Merge pull request 'Adding unique ap_ids. Fixes #1100' (#90) from unique_ap_ids into activity-sender

Reviewed-on: https://yerbamate.dev/LemmyNet/lemmy/pulls/90

Adding back in on_conflict.

Trying to add back in the on_conflict_do_nothing.

Trying to reduce delay time.

Removing createFakes.

Removing some unit tests.

Adding comment jest timeout.

Fixing tests again.

Fixing tests again.

Merge branch 'activity-sender' into unique_ap_ids_2

Replace actix client with reqwest to speed up federation tests

Trying to fix tests again.

Fixing unit tests.

Fixing some broken unit tests, not done yet.

Adding uniques.

Adding unique ap_ids. Fixes #1100

use proper sql functionality for upsert

added logging

in fetcher, replace post/comment::create with upsert

no need to do an actual update in post/comment::upsert

Merge branch 'main' into activity-sender

implement upsert for user/community

reuse http client

got it working

attempt to use background-jobs crate

rewrite with proper error handling and less boilerplate

remove do_send, dont return errors from activity_sender

WIP: implement ActivitySender actor

Co-authored-by: dessalines <dessalines@noreply.yerbamate.dev>
Co-authored-by: Dessalines <tyhou13@gmx.com>
Co-authored-by: Felix Ableitner <me@nutomic.com>
Reviewed-on: https://yerbamate.dev/LemmyNet/lemmy/pulls/89
2020-08-31 13:48:02 +00:00
18002bc837 Version v0.7.56 2020-08-29 17:21:40 -04:00
8184c8021a Merge remote-tracking branch 'weblate/main' into main 2020-08-29 17:21:39 -04:00
d0ad8022d9 Merge branch 'kartikynwa-webmanifest' into test 2020-08-29 17:00:56 -04:00
e8600447ed Merge branch 'webmanifest' of https://github.com/kartikynwa/lemmy into kartikynwa-webmanifest 2020-08-29 17:00:43 -04:00
f587008d61 Merge branch 'add-service-worker' of https://github.com/eiknat/lemmy into eiknat-add-service-worker 2020-08-29 09:36:58 -05:00
b762760e96 Updating korean translation. 2020-08-29 09:29:49 -05:00
Mostafa Ahangarha
36e4ab00da Translated using Weblate (Persian)
Currently translated at 54.9% (156 of 284 strings)

Translation: Lemmy/lemmy
Translate-URL: http://weblate.yerbamate.dev/projects/lemmy/lemmy/fa/
2020-08-29 08:42:08 +00:00
Lee KiWon
1ef3591e17 Translated using Weblate (Korean)
Currently translated at 100.0% (284 of 284 strings)

Translation: Lemmy/lemmy
Translate-URL: http://weblate.yerbamate.dev/projects/lemmy/lemmy/ko/
2020-08-29 08:42:08 +00:00
eiknat
bebdc851c9 Add service worker 2020-08-28 23:47:15 -04:00
Joshua Thomas
0f8372c0fb
Bugfix: Adds case-insensitive SELECT queries to User_::find_by_username() (#1108) 2020-08-28 20:49:31 -04:00
4819bd5608 Version v0.7.55 2020-08-27 10:38:29 -04:00
Sergio Varela
5e78479371 Translated using Weblate (Spanish)
Currently translated at 98.4% (262 of 266 strings)

Translation: Lemmy/lemmy
Translate-URL: http://weblate.yerbamate.dev/projects/lemmy/lemmy/es/

Translated using Weblate (Spanish)

Currently translated at 96.2% (256 of 266 strings)

Translation: Lemmy/lemmy
Translate-URL: http://weblate.yerbamate.dev/projects/lemmy/lemmy/es/
2020-08-27 14:36:45 +00:00
Panos Alevropoulos
d3a00014c9 Translated using Weblate (Greek)
Currently translated at 100.0% (266 of 266 strings)

Translation: Lemmy/lemmy
Translate-URL: http://weblate.yerbamate.dev/projects/lemmy/lemmy/el/
2020-08-27 14:36:45 +00:00
3f0b3d7361 Adding korean language template. 2020-08-27 10:34:53 -04:00
Dessalines
2d5a50e80f
Fixing broken websocket sends. Removing WebSocketInfo (#1098) 2020-08-24 11:58:24 +00:00
Dessalines
2080534744
Switch front end to use lemmy-js-client. Fixes #1090 (#1097)
* Switch front end to use lemmy-js-client. Fixes #1090

* Adding an HTTP client. Cleaning up Websocket client.
2020-08-20 10:50:26 -04:00
Dessalines
dbf231865d
Adding a few more apub tests. (#1096)
* Adding a few more apub tests.

* Fixing travis build, adding a get_post function.
2020-08-20 12:44:22 +00:00
e007006daf Version v0.7.54 2020-08-18 09:43:45 -04:00
nutomic
14f2d190e5 Implement context (#86)
Implement context

Co-authored-by: Felix Ableitner <me@nutomic.com>
Reviewed-on: https://yerbamate.dev/LemmyNet/lemmy/pulls/86
2020-08-18 13:43:50 +00:00
53d449d5d7 Merge branch 'fix-federation-enabled-option' into main 2020-08-18 09:35:27 -04:00
fd2291c004 Dont send out any activities if federation is disabled (fixes #1095) 2020-08-18 15:12:03 +02:00
82faea7f85 Version v0.7.53 2020-08-17 16:53:50 -04:00
ae39ddf031 Fixing some formatting. 2020-08-17 16:52:01 -04:00
3301e0ba50 Merge remote-tracking branch 'yerba/main' into main 2020-08-17 16:50:21 -04:00
nutomic
ed72b096b9 Remove tests for unimplemented Crud::delete functions (#87)
Remove tests for unimplemented Crud::delete functions

Co-authored-by: Felix Ableitner <me@nutomic.com>
Reviewed-on: https://yerbamate.dev/LemmyNet/lemmy/pulls/87
2020-08-17 20:50:44 +00:00
Dessalines
c323ab5275
Added option to remove banned user data (posts, comments, communities) (#1093)
- Works for both a site-ban, and a community ban.
- Fixes #557
2020-08-17 18:12:36 +00:00
725e46da4a Version v0.7.52 2020-08-16 11:31:09 -04:00
abadc79756 Fix community_view clippy 2020-08-16 11:27:50 -04:00
Dessalines
0a27432b65
Encode crosspost url. Fixes #1083 (#1088) 2020-08-16 11:05:45 -04:00
Dessalines
adeba99d38
Allow mods that aren't creators to leave the mod team. Fixes #786 (#1091) 2020-08-16 11:05:32 -04:00
Dessalines
7f97279c48
Fixing community search. Fixes #1084 (#1089) 2020-08-16 11:05:21 -04:00
Kartik Singh
3663cb5ad5 Add webmanifest file, icons. Add manifest to index.html 2020-07-09 19:45:11 +05:30
610 changed files with 37616 additions and 60234 deletions

View file

@ -1,12 +1,8 @@
# build folders and similar which are not needed for the docker build
ui/node_modules
server/target
docker/dev/volumes
docker/federation/volumes
docker/federation-test/volumes
.git
target
docker
api_tests
ansible
# exceptions, needed for federation-test build
!server/target/debug/lemmy_server
tests
.git
*.sh

190
.drone.yml Normal file
View file

@ -0,0 +1,190 @@
---
kind: pipeline
name: amd64
platform:
os: linux
arch: amd64
steps:
- name: chown repo
image: ekidd/rust-musl-builder:1.50.0
user: root
commands:
- chown 1000:1000 . -R
- name: check formatting
image: rustdocker/rust:nightly
commands:
- /root/.cargo/bin/cargo fmt -- --check
- name: cargo clippy
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.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.50.0
environment:
CARGO_HOME: /drone/src/.cargo
commands:
- cargo build
- mv target/x86_64-unknown-linux-musl/debug/lemmy_server target/lemmy_server
- name: run federation tests
image: node:15-alpine3.12
environment:
LEMMY_DATABASE_URL: postgres://lemmy:password@database:5432
DO_WRITE_HOSTS_FILE: 1
commands:
- apk add bash curl postgresql-client
- 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
username:
from_secret: docker_username
password:
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/*
services:
- name: database
image: postgres:12-alpine
environment:
POSTGRES_USER: lemmy
POSTGRES_PASSWORD: password
---
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

2
.gitattributes vendored
View file

@ -1,2 +0,0 @@
* linguist-vendored
*.rs linguist-vendored=false

View file

@ -9,6 +9,8 @@ assignees: ''
Found a bug? Please fill out the sections below. 👍
For front end issues, use [lemmy-ui](https://github.com/LemmyNet/lemmy-ui)
### Issue Summary
A summary of the bug.

View file

@ -7,6 +7,8 @@ assignees: ''
---
For front end issues, use [lemmy-ui](https://github.com/LemmyNet/lemmy-ui)
### Is your proposal related to a problem?
<!--

18
.gitignore vendored
View file

@ -6,16 +6,16 @@ ansible/passwords/
# docker build files
docker/lemmy_mine.hjson
docker/dev/env_deploy.sh
docker/federation/volumes
docker/federation-test/volumes
docker/dev/volumes
# local build files
build/
ui/src/translations
volumes
# ide config
.idea/
.vscode/
.idea
.vscode
# local build files
target
env_setup.sh
query_testing/**/reports/*.json
# API tests
api_tests/node_modules

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,28 +0,0 @@
sudo: required
language: node_js
node_js:
- 14
services:
- docker
env:
matrix:
- DOCKER_COMPOSE_VERSION=1.25.5
global:
- secure: nzmFoTxPn7OT+qcTULezSCT6B44j/q8RxERBQSr1FVXaCcDrBr6q9ewhGy7BHWP74r4qbif4m9r3sNELZCoFYFP3JwLnrZfX/xUwU8p61eFD2PMOJAdOywDxb94SvooOSnjBmxNvRsuqf6Zmnw378mbsSVCi9Xbx9jpoV4Jq8zKgO0M8WIl/lj2dijD95WIMrHcorbzKS3+2zW3LkPiC2bnfDAUmUDfaCj1gh9FCvzZMtrSxu7kxAeFCkR16TJUciIcGgag8rLHfxwG0h2uEJJ+3/62qCWUdgnj171oTE4ZRi0hdvt2HOY5wjHfS2y1ZxWYgo31uws3pyoTNeQZi0o7Q9Xe/4JXYZXvDfuscSZ9RiuhAstCVswtXPJJVVJQ9cdl5eX1TI0bz8eVRvRy4p40OIBjKiobkmRjl8sXjFbpYAIvFr+TgSa/K/bxm3POfI0B8bIHI85zFxUMrWt5i2IJ0dWvDNHrz+CWWKn1vVFYbBNPgDDHtE0P3LWLEioWFf+ULycjW8DefWc+b63Lf9SSaEE7FnX2mc+BaHCgubCDkJy9Au4xP8zQlJjgZwOdTedw5jvmwz3fqMZBpHypVUXzZs7cRhMWtQ7TAoGb8TOqXNgPEVW+BARNXl0wAamTgjt9v20x0wkp+/SLJwMNY+zvwmzxzd5R9TPgDOqyIRTU=
- secure: ALZqC4OYV315P7EZyk+c/PLJdneeU7jMC30TTzMcX3hospIu7naWekZ+HUnziFDQKZxIHWKZsq1R52DWhsERLrPF3SVa+QiXu8vTTPrETBWnu9VgyFzgdEbUKRas1X3qerEAHcNBms1EAl2FOiQM1k5EDygrClv4KWgyzntEtKJbN2UCFKxtoBSdMZA6fcGtCwffcj8uIAIP2NhZixbU+smVgVbpMpe6QEuuEoVlVrfH8iXxb8Gi+qkd0YIYAHkjtTqQ/nHuAUhcuEE0mORTNGPv7CmTwpuQiGCCdtySZc7Qq8z1x2y7RLy0+RVxM0PR8UV6iy4ipyTgZ6wTF30ksLDxOI3GlRaKF3F6kLErOiEiEUOqa+zLgUM0OLGTn+KLATQDx74in5NcKjKUAnkuxdZyuDbifvQb5tqfrGdXd22pzVZbielRJRW59ig0Nr5cxEpRtoRkoFKNk7o3XlD6JmIBjKn1UHkZ4H/oLUKIXT2qOP2fIEzgLjfpSuGwhvJRz1KRP49HYVl7Gkd45/RdZ519W0gnMkIrEaod90iXSFNTgmJTGeH0Mv0jHameN47PIT3c49MOy5Hj0XCHUPfc6qqrdGnliS5hTnrFThCfn5ZuSZxVdgGLJUQvV+D+5KDqjFdGyNGVGoEg0YdrDtGXmpojbyQDJAT7ToL3yIBF7co=
before_install:
# Install docker-compose
- sudo rm /usr/local/bin/docker-compose
- curl -L https://github.com/docker/compose/releases/download/${DOCKER_COMPOSE_VERSION}/docker-compose-`uname
-s`-`uname -m` > docker-compose
- chmod +x docker-compose
- sudo mv docker-compose /usr/local/bin
# Change dir
- cd docker/travis
script:
- "./run-tests.sh"
deploy:
provider: script
script: bash docker_push.sh
on:
tags: 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.dev/LemmyNet/lemmy](https://yerbamate.dev/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://dev.lemmy.ml/docs/contributing.html) for contributing Instructions.
See [here](https://join.lemmy.ml/docs/en/contributing/contributing.html) for contributing Instructions.

File diff suppressed because it is too large Load diff

62
Cargo.toml Normal file
View file

@ -0,0 +1,62 @@
[package]
name = "lemmy_server"
version = "0.0.1"
edition = "2018"
[lib]
doctest = false
[profile.dev]
debug = 0
[workspace]
members = [
"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 = "./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.123", features = ["derive"] }
actix = "0.10.0"
actix-web = { version = "3.3.2", default-features = false, features = ["rustls"] }
log = "0.4.14"
env_logger = "0.8.2"
strum = "0.20.0"
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"
anyhow = "1.0.38"
reqwest = { version = "0.10.10", features = ["json"] }
activitystreams = "0.7.0-alpha.10"
actix-rt = { version = "1.1.1", default-features = false }
serde_json = { version = "1.0.61", features = ["preserve_order"] }
clokwerk = "0.3.4"
[dev-dependencies.cargo-husky]
version = "1.5.0"
default-features = false # Disable features which are enabled by default
features = ["precommit-hook", "run-cargo-fmt", "run-cargo-clippy"]

View file

@ -1,42 +1,45 @@
<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.dev/widgets/lemmy/-/lemmy/svg-badge.svg)](http://weblate.yerbamate.dev/engage/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">
<a href="https://dev.lemmy.ml/" rel="noopener">
<img width=200px height=200px src="ui/assets/favicon.svg"></a>
<a href="https://join.lemmy.ml/" rel="noopener">
<img width=200px height=200px src="https://raw.githubusercontent.com/LemmyNet/lemmy-ui/main/src/assets/icons/favicon.svg"></a>
<h3 align="center"><a href="https://dev.lemmy.ml">Lemmy</a></h3>
<h3 align="center"><a href="https://join.lemmy.ml">Lemmy</a></h3>
<p align="center">
A link aggregator / Reddit clone for the fediverse.
<br />
<br />
<a href="https://dev.lemmy.ml">View Site</a>
<a href="https://join.lemmy.ml">Join Lemmy</a>
·
<a href="https://dev.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), [Raddle](https://raddle.me), 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).
[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).
For a link aggregator, this means a user registered on one server can subscribe to forums on any other server, and can have discussions with users registered elsewhere.
@ -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](#docker), [Ansible](#ansible), [Kubernetes](#kubernetes).
- 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,19 +103,22 @@ Each Lemmy server can set its own moderation policy; appointing site-wide admins
## Installation
- [Docker](https://dev.lemmy.ml/docs/administration_install_docker.html)
- [Ansible](https://dev.lemmy.ml/docs/administration_install_ansible.html)
- [Kubernetes](https://dev.lemmy.ml/docs/administration_install_kubernetes.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-mobile (Android / IOS) - React native ( under development )](https://github.com/koredefashokun/lemmy-mobile)
- [lemmy-ui - The official web app for lemmy](https://github.com/LemmyNet/lemmy-ui)
- [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
- [lemmy-js-client](https://github.com/LemmyNet/lemmy-js-client)
- [Kotlin API ( under development )](https://github.com/eiknat/lemmy-client)
- [Dart API client ( under development )](https://github.com/krawieck/lemmy_api_client)
## Support / Donate
@ -121,7 +127,7 @@ Lemmy is free, open-source software, meaning no advertising, monetizing, or vent
- [Support on Liberapay](https://liberapay.com/Lemmy).
- [Support on Patreon](https://www.patreon.com/dessalines).
- [Support on OpenCollective](https://opencollective.com/lemmy).
- [List of Sponsors](https://dev.lemmy.ml/sponsors).
- [List of Sponsors](https://join.lemmy.ml/sponsors).
### Crypto
@ -131,24 +137,24 @@ Lemmy is free, open-source software, meaning no advertising, monetizing, or vent
## Contributing
- [Contributing instructions](https://dev.lemmy.ml/docs/contributing.html)
- [Docker Development](https://dev.lemmy.ml/docs/contributing_docker_development.html)
- [Local Development](https://dev.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.dev/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
- [Mastodon](https://mastodon.social/@LemmyDev)
- [Matrix](https://riot.im/app/#/room/#rust-reddit-fediverse:matrix.org)
- [Matrix](https://matrix.to/#/#lemmy:matrix.org)
## Code Mirrors
- [GitHub](https://github.com/LemmyNet/lemmy)
- [Gitea](https://yerbamate.dev/LemmyNet/lemmy)
- [GitLab](https://gitlab.com/dessalines/lemmy)
- [Gitea](https://yerbamate.ml/LemmyNet/lemmy)
- [Codeberg](https://codeberg.org/LemmyNet/lemmy)
## Credits

View file

@ -1,3 +1,207 @@
# 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
We've been working at warp speed since our `v0.7.0` release in June, adding over [870 commits](https://github.com/LemmyNet/lemmy/compare/v0.7.0...main) since then. :sweat:
Here are some of the bigger changes:
### LemmyNet projects
- Created [LemmyNet](https://github.com/LemmyNet), where all lemmy-related projects live.
- Split out the frontend into a separete repository, [lemmy-ui](https://github.com/LemmyNet/lemmy-ui)
- Created a [lemmy-js-client](https://github.com/LemmyNet/lemmy-js-client), for any js / typescript developers.
- Split out i18n [lemmy-translations](https://github.com/LemmyNet/lemmy-translations), that any app or site developers can import and use. Lemmy currently supports [~30 languages!](https://weblate.yerbamate.ml/projects/lemmy/lemmy/)
### Lemmy Server
#### Federation
- 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://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
- Many additional security checks
- Lots and lots of refactoring
- Asynchronous sending of outgoing activities
### User Interface
- Separated the UI from the server code, in [lemmy-ui](https://github.com/LemmyNet/lemmy-ui).
- The UI can now read with javascript disabled!
- It's now a fully isomorphic application using [inferno-isomorphic](https://infernojs.org/docs/guides/isomorphic). This means that page loads are now much faster, as the server does the work.
- The UI now also supports open-graph and twitter cards! Linking to lemmy posts (from whatever platform you use) looks pretty now: ![](https://i.imgur.com/6TZ2v7s.png)
- Improved the search page ( more features incoming ).
- The default view is now `Local`, instead of `All`, since all would show all federated posts.
- User settings are now shared across browsers ( a page refresh will pick up changes ).
- A much leaner mobile view.
#### Backend
- Re-organized the rust codebase into separate workspaces for backend and frontend.
- Removed materialized views, making the database **a lot faster**.
- New post sorts `Active` (previously called hot), and `Hot`. Active shows posts with recent comments, hot shows highly ranked posts.
- New sort for `Local` ( meaning from local communities).
- Customizeable site, user, and community icons and banners.
- Added user preferred names / display names, bios, and cakedays.
- Visual / Audio captchas through the lemmy API.
- Lots of API field verifications.
- Upgraded to pictrs-v2 ( thanks to @asonix )
- Wayyy too many bugfixes to count.
## Contributors
We'd also like to thank both the [NLnet foundation](https://nlnet.nl/) for their support in allowing us to work full-time on Lemmy ( as well as their support for [other important open-source projects](https://nlnet.nl/project/current.html) ), [those who sponsor us](https://lemmy.ml/sponsors), and those who [help translate Lemmy](https://weblate.yerbamate.ml/projects/lemmy/). Every little bit does help. We remain committed to never allowing advertisements, monetizing, or venture-capital in Lemmy; software should be communal, and should benefit humanity, not a small group of company owners.
## Upgrading
- [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
Federation is finally ready in Lemmy, pending possible bugs or other issues. So for now we suggest to enable federation only on test servers, or try it on our own test servers ( [enterprise](https://enterprise.lemmy.ml/), [ds9](https://ds9.lemmy.ml/), [voyager](https://voyager.lemmy.ml/) ).
If everything goes well, after a few weeks we will enable federation on lemmy.ml, at first with a limited number of trusted instances. We will also likely change the domain to https://lemmy.ml . Keep in mind that changing domains after turning on federation will break things.
To enable on your instance, edit your [lemmy.hjson](https://github.com/LemmyNet/lemmy/blob/main/config/defaults.hjson#L60) federation section to `enabled: true`, and restart.
### Connecting to another server
The server https://ds9.lemmy.ml has open federation, so after either adding it to the `allowed_instances` list in your `config.hjson`, or if you have open federation, you don't need to add it explicitly.
To federate / connect with a server, type in `!community_name@server.tld`, in your server's search box [like so](https://voyager.lemmy.ml/search/q/!main%40ds9.lemmy.ml/type/All/sort/TopAll/page/1).
To connect with the `main` community on ds9, the search is `!main@ds9.lemmy.ml`.
You can then click the community, and you will see a local version of the community, which you can subscribe to. New posts and comments from `!main@ds9.lemmy.ml` will now show up on your front page, or `/c/All`
# Lemmy v0.7.40 Pre-Release (2020-08-05)
We've [added a lot](https://github.com/LemmyNet/lemmy/compare/v0.7.40...v0.7.0) in this pre-release:
@ -66,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://dev.lemmy.ml/docs/administration_backup_and_restore.html)
[documentation](https://join.lemmy.ml/docs/administration_backup_and_restore.html)
for backup instructions.
**With Ansible:**
@ -122,4 +326,4 @@ This is the biggest release by far:
Another major announcement is that Lemmy now has another lead developer besides me, [@felix@radical.town](https://radical.town/@felix). Theyve created a better documentation system, implemented RSS feeds, simplified docker and project configs, upgraded actix, working on federation, a whole lot else.
https://dev.lemmy.ml
https://lemmy.ml

View file

@ -1 +1 @@
v0.7.50
0.10.0-rc.7

View file

@ -12,7 +12,7 @@
- name: install python for Ansible
# python2-minimal instead of python-minimal for ubuntu 20.04 and up
raw: test -e /usr/bin/python || (apt -y update && apt install -y python-minimal python-setuptools)
raw: test -e /usr/bin/python || (apt -y update && apt install -y python3-minimal python3-setuptools)
args:
executable: /bin/bash
register: output
@ -77,7 +77,9 @@
mode: '0600'
vars:
lemmy_docker_image: "dessalines/lemmy:{{ lookup('file', 'VERSION') }}"
lemmy_docker_ui_image: "dessalines/lemmy-ui:{{ lookup('file', 'VERSION') }}"
lemmy_port: "8536"
lemmy_ui_port: "1235"
pictshare_port: "8537"
iframely_port: "8538"

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://dev.lemmy.ml/docs/administration_configuration.html
# https://join.lemmy.ml/docs/en/administration/configuration.html
# settings related to the postgresql database
database: {
@ -9,12 +9,10 @@
# host where postgres is running
host: "postgres"
}
# the domain name of your instance (eg "dev.lemmy.ml")
# the domain name of your instance (eg "lemmy.ml")
hostname: "{{ domain }}"
# json web token for authorization between server and client
jwt_secret: "{{ jwt_password }}"
# The location of the frontend
front_end_dir: "/app/dist"
# email sending configuration
email: {
# hostname of the smtp server
@ -23,4 +21,17 @@
smtp_from_address: "noreply@{{ domain }}"
use_tls: false
}
# settings related to activitypub federation
federation: {
# whether to enable activitypub federation.
enabled: false
# Allows and blocks are described here:
# https://join.lemmy.ml/docs/en/federation/administration.html#instance-allowlist-and-blocklist
#
# comma separated list of instances with which federation is allowed
# 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: []
}
}

View file

@ -1,4 +1,4 @@
version: '3.3'
version: '2.2'
services:
lemmy:
@ -15,6 +15,18 @@ services:
- pictrs
- iframely
lemmy-ui:
image: {{ lemmy_docker_ui_image }}
ports:
- "127.0.0.1:1235:1234"
restart: always
environment:
- LEMMY_INTERNAL_HOST=lemmy:8536
- LEMMY_EXTERNAL_HOST={{ domain }}
- LEMMY_HTTPS=true
depends_on:
- lemmy
postgres:
image: postgres:12-alpine
environment:
@ -26,13 +38,14 @@ services:
restart: always
pictrs:
image: asonix/pictrs:amd64-v0.1.0-r9
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
@ -41,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

@ -51,37 +51,54 @@ server {
# Upload limit for pictrs
client_max_body_size 20M;
# frontend
location / {
proxy_pass http://0.0.0.0:8536;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# The default ports:
# lemmy_ui_port: 1235
# lemmy_port: 8536
# Cuts off the trailing slash on URLs to make them valid
rewrite ^(.+)/+$ $1 permanent;
set $proxpass "http://0.0.0.0:{{ lemmy_ui_port }}";
if ($http_accept = "application/activity+json") {
set $proxpass "http://0.0.0.0:{{ lemmy_port }}";
}
if ($http_accept = "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"") {
set $proxpass "http://0.0.0.0:{{ lemmy_port }}";
}
if ($request_method = POST) {
set $proxpass "http://0.0.0.0:{{ lemmy_port }}";
}
proxy_pass $proxpass;
# WebSocket support
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
rewrite ^(.+)/+$ $1 permanent;
# Send actual client IP upstream
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
# backend
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;
proxy_set_header Connection "upgrade";
# Rate limit
limit_req zone=lemmy_ratelimit burst=30 nodelay;
# Add IP forwarding headers
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
# Rate limit
limit_req zone=lemmy_ratelimit burst=30 nodelay;
}
# Redirect pictshare images to pictrs
location ~ /pictshare/(.*)$ {
return 301 /pictrs/image/$1;
}
# Separate location block to disable rate limiting for images
location /pictrs {
proxy_pass http://0.0.0.0:8536/pictrs;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
location /iframely/ {
proxy_pass http://0.0.0.0:8061/;
proxy_set_header X-Real-IP $remote_addr;

View file

@ -4,13 +4,11 @@
"browser": true
},
"plugins": [
"jane",
"inferno"
"jane"
],
"extends": [
"plugin:jane/recommended",
"plugin:jane/typescript",
"plugin:inferno/recommended"
"plugin:jane/typescript"
],
"parser": "@typescript-eslint/parser",
"parserOptions": {
@ -32,11 +30,6 @@
"eqeqeq": 0,
"func-style": 0,
"import/no-duplicates": 0,
"inferno/jsx-key": 0,
"inferno/jsx-no-target-blank": 0,
"inferno/jsx-props-class-name": 0,
"inferno/no-direct-mutation-state": 0,
"inferno/no-unknown-property": 0,
"max-statements": 0,
"max-params": 0,
"new-cap": 0,

4
api_tests/jest.config.js Normal file
View file

@ -0,0 +1,4 @@
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
};

25
api_tests/package.json Normal file
View file

@ -0,0 +1,25 @@
{
"name": "api_tests",
"version": "0.0.1",
"description": "API tests for lemmy backend",
"main": "index.js",
"repository": "https://github.com/LemmyNet/lemmy",
"author": "Dessalines",
"license": "AGPL-3.0",
"scripts": {
"lint": "tsc --noEmit && eslint --report-unused-disable-directives --ext .js,.ts,.tsx src",
"fix": "prettier --write src && eslint --fix src",
"api-test": "jest src/ -i --verbose"
},
"devDependencies": {
"@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

@ -0,0 +1,71 @@
#!/bin/bash
set -e
export LEMMY_TEST_SEND_SYNC=1
export RUST_BACKTRACE=1
for INSTANCE in lemmy_alpha lemmy_beta lemmy_gamma lemmy_delta lemmy_epsilon; do
psql "${LEMMY_DATABASE_URL}/lemmy" -c "DROP DATABASE IF EXISTS $INSTANCE"
psql "${LEMMY_DATABASE_URL}/lemmy" -c "CREATE DATABASE $INSTANCE"
done
if [ -z "$DO_WRITE_HOSTS_FILE" ]; then
if ! grep -q lemmy-alpha /etc/hosts; then
echo "Please add the following to your /etc/hosts file, then press enter:
127.0.0.1 lemmy-alpha
127.0.0.1 lemmy-beta
127.0.0.1 lemmy-gamma
127.0.0.1 lemmy-delta
127.0.0.1 lemmy-epsilon"
read -p ""
fi
else
for INSTANCE in lemmy-alpha lemmy-beta lemmy-gamma lemmy-delta lemmy-epsilon; do
echo "127.0.0.1 $INSTANCE" >> /etc/hosts
done
fi
killall lemmy_server || true
echo "$PWD"
echo "start alpha"
LEMMY_HOSTNAME=lemmy-alpha:8541 \
LEMMY_CONFIG_LOCATION=./docker/federation/lemmy_alpha.hjson \
LEMMY_DATABASE_URL="${LEMMY_DATABASE_URL}/lemmy_alpha" \
LEMMY_HOSTNAME="lemmy-alpha:8541" \
target/lemmy_server >/tmp/lemmy_alpha.out 2>&1 &
echo "start beta"
LEMMY_HOSTNAME=lemmy-beta:8551 \
LEMMY_CONFIG_LOCATION=./docker/federation/lemmy_beta.hjson \
LEMMY_DATABASE_URL="${LEMMY_DATABASE_URL}/lemmy_beta" \
target/lemmy_server >/tmp/lemmy_beta.out 2>&1 &
echo "start gamma"
LEMMY_HOSTNAME=lemmy-gamma:8561 \
LEMMY_CONFIG_LOCATION=./docker/federation/lemmy_gamma.hjson \
LEMMY_DATABASE_URL="${LEMMY_DATABASE_URL}/lemmy_gamma" \
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_CONFIG_LOCATION=./docker/federation/lemmy_delta.hjson \
LEMMY_DATABASE_URL="${LEMMY_DATABASE_URL}/lemmy_delta" \
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_CONFIG_LOCATION=./docker/federation/lemmy_epsilon.hjson \
LEMMY_DATABASE_URL="${LEMMY_DATABASE_URL}/lemmy_epsilon" \
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
while [[ "$(curl -s -o /dev/null -w '%{http_code}' 'localhost:8551/api/v2/site')" != "200" ]]; do sleep 1; done
while [[ "$(curl -s -o /dev/null -w '%{http_code}' 'localhost:8561/api/v2/site')" != "200" ]]; do sleep 1; done
while [[ "$(curl -s -o /dev/null -w '%{http_code}' 'localhost:8571/api/v2/site')" != "200" ]]; do sleep 1; done
while [[ "$(curl -s -o /dev/null -w '%{http_code}' 'localhost:8581/api/v2/site')" != "200" ]]; do sleep 1; done

View file

@ -0,0 +1,20 @@
#!/bin/bash
set -e
export LEMMY_DATABASE_URL=postgres://lemmy:password@localhost:5432
pushd ..
cargo build
rm target/lemmy_server || true
cp target/debug/lemmy_server target/lemmy_server
./api_tests/prepare-drone-federation-test.sh
popd
yarn
yarn api-test || true
killall lemmy_server
for INSTANCE in lemmy_alpha lemmy_beta lemmy_gamma lemmy_delta lemmy_epsilon; do
psql "$LEMMY_DATABASE_URL" -c "DROP DATABASE $INSTANCE"
done

View file

@ -0,0 +1,391 @@
jest.setTimeout(180000);
import {
alpha,
beta,
gamma,
setupLogins,
createPost,
getPost,
searchComment,
likeComment,
followBeta,
searchForBetaCommunity,
createComment,
editComment,
deleteComment,
removeComment,
getMentions,
searchPost,
unfollowRemotes,
createCommunity,
registerUser,
API,
} from './shared';
import { CommentView } from 'lemmy-js-client';
import { PostResponse } from 'lemmy-js-client';
let postRes: PostResponse;
beforeAll(async () => {
await setupLogins();
await followBeta(alpha);
await followBeta(gamma);
let search = await searchForBetaCommunity(alpha);
postRes = await createPost(
alpha,
search.communities.find(c => c.community.local == false).community.id
);
});
afterAll(async () => {
await unfollowRemotes(alpha);
await unfollowRemotes(gamma);
});
function assertCommentFederation(
commentOne: CommentView,
commentTwo: CommentView
) {
expect(commentOne.comment.ap_id).toBe(commentOne.comment.ap_id);
expect(commentOne.comment.content).toBe(commentTwo.comment.content);
expect(commentOne.creator.name).toBe(commentTwo.creator.name);
expect(commentOne.community.actor_id).toBe(commentTwo.community.actor_id);
expect(commentOne.comment.published).toBe(commentTwo.comment.published);
expect(commentOne.comment.updated).toBe(commentOne.comment.updated);
expect(commentOne.comment.deleted).toBe(commentOne.comment.deleted);
expect(commentOne.comment.removed).toBe(commentOne.comment.removed);
}
test('Create a comment', async () => {
let commentRes = await createComment(alpha, postRes.post_view.post.id);
expect(commentRes.comment_view.comment.content).toBeDefined();
expect(commentRes.comment_view.community.local).toBe(false);
expect(commentRes.comment_view.creator.local).toBe(true);
expect(commentRes.comment_view.counts.score).toBe(1);
// Make sure that comment is liked on beta
let searchBeta = await searchComment(beta, commentRes.comment_view.comment);
let betaComment = searchBeta.comments[0];
expect(betaComment).toBeDefined();
expect(betaComment.community.local).toBe(true);
expect(betaComment.creator.local).toBe(false);
expect(betaComment.counts.score).toBe(1);
assertCommentFederation(betaComment, commentRes.comment_view);
});
test('Create a comment in a non-existent post', async () => {
let commentRes = await createComment(alpha, -1);
expect(commentRes).toStrictEqual({ error: 'couldnt_find_post' });
});
test('Update a comment', async () => {
let commentRes = await createComment(alpha, postRes.post_view.post.id);
// Federate the comment first
let searchBeta = await searchComment(beta, commentRes.comment_view.comment);
assertCommentFederation(searchBeta.comments[0], commentRes.comment_view);
let updateCommentRes = await editComment(
alpha,
commentRes.comment_view.comment.id
);
expect(updateCommentRes.comment_view.comment.content).toBe(
'A jest test federated comment update'
);
expect(updateCommentRes.comment_view.community.local).toBe(false);
expect(updateCommentRes.comment_view.creator.local).toBe(true);
// Make sure that post is updated on beta
let searchBetaUpdated = await searchComment(
beta,
commentRes.comment_view.comment
);
assertCommentFederation(
searchBetaUpdated.comments[0],
updateCommentRes.comment_view
);
});
test('Delete a comment', async () => {
let commentRes = await createComment(alpha, postRes.post_view.post.id);
let deleteCommentRes = await deleteComment(
alpha,
true,
commentRes.comment_view.comment.id
);
expect(deleteCommentRes.comment_view.comment.deleted).toBe(true);
// Make sure that comment is undefined on beta
let searchBeta = await searchComment(beta, commentRes.comment_view.comment);
let betaComment = searchBeta.comments[0];
expect(betaComment).toBeUndefined();
let undeleteCommentRes = await deleteComment(
alpha,
false,
commentRes.comment_view.comment.id
);
expect(undeleteCommentRes.comment_view.comment.deleted).toBe(false);
// Make sure that comment is undeleted on beta
let searchBeta2 = await searchComment(beta, commentRes.comment_view.comment);
let betaComment2 = searchBeta2.comments[0];
expect(betaComment2.comment.deleted).toBe(false);
assertCommentFederation(
searchBeta2.comments[0],
undeleteCommentRes.comment_view
);
});
test('Remove a comment from admin and community on the same instance', async () => {
let commentRes = await createComment(alpha, postRes.post_view.post.id);
// Get the id for beta
let betaCommentId = (
await searchComment(beta, commentRes.comment_view.comment)
).comments[0].comment.id;
// The beta admin removes it (the community lives on beta)
let removeCommentRes = await removeComment(beta, true, betaCommentId);
expect(removeCommentRes.comment_view.comment.removed).toBe(true);
// Make sure that comment is removed on alpha (it gets pushed since an admin from beta removed it)
let refetchedPost = await getPost(alpha, postRes.post_view.post.id);
expect(refetchedPost.comments[0].comment.removed).toBe(true);
let unremoveCommentRes = await removeComment(beta, false, betaCommentId);
expect(unremoveCommentRes.comment_view.comment.removed).toBe(false);
// Make sure that comment is unremoved on beta
let refetchedPost2 = await getPost(alpha, postRes.post_view.post.id);
expect(refetchedPost2.comments[0].comment.removed).toBe(false);
assertCommentFederation(
refetchedPost2.comments[0],
unremoveCommentRes.comment_view
);
});
test('Remove a comment from admin and community on different instance', async () => {
let alphaUser = await registerUser(alpha);
let newAlphaApi: API = {
client: alpha.client,
auth: alphaUser.jwt,
};
// New alpha user creates a community, post, and comment.
let newCommunity = await createCommunity(newAlphaApi);
let newPost = await createPost(
newAlphaApi,
newCommunity.community_view.community.id
);
let commentRes = await createComment(newAlphaApi, newPost.post_view.post.id);
expect(commentRes.comment_view.comment.content).toBeDefined();
// Beta searches that to cache it, then removes it
let searchBeta = await searchComment(beta, commentRes.comment_view.comment);
let betaComment = searchBeta.comments[0];
let removeCommentRes = await removeComment(
beta,
true,
betaComment.comment.id
);
expect(removeCommentRes.comment_view.comment.removed).toBe(true);
// Make sure its not removed on alpha
let refetchedPost = await getPost(newAlphaApi, newPost.post_view.post.id);
expect(refetchedPost.comments[0].comment.removed).toBe(false);
assertCommentFederation(refetchedPost.comments[0], commentRes.comment_view);
});
test('Unlike a comment', async () => {
let commentRes = await createComment(alpha, postRes.post_view.post.id);
let unlike = await likeComment(alpha, 0, commentRes.comment_view.comment);
expect(unlike.comment_view.counts.score).toBe(0);
// Make sure that post is unliked on beta
let searchBeta = await searchComment(beta, commentRes.comment_view.comment);
let betaComment = searchBeta.comments[0];
expect(betaComment).toBeDefined();
expect(betaComment.community.local).toBe(true);
expect(betaComment.creator.local).toBe(false);
expect(betaComment.counts.score).toBe(0);
});
test('Federated comment like', async () => {
let commentRes = await createComment(alpha, postRes.post_view.post.id);
// Find the comment on beta
let searchBeta = await searchComment(beta, commentRes.comment_view.comment);
let betaComment = searchBeta.comments[0];
let like = await likeComment(beta, 1, betaComment.comment);
expect(like.comment_view.counts.score).toBe(2);
// Get the post from alpha, check the likes
let post = await getPost(alpha, postRes.post_view.post.id);
expect(post.comments[0].counts.score).toBe(2);
});
test('Reply to a comment', async () => {
// Create a comment on alpha, find it on beta
let commentRes = await createComment(alpha, postRes.post_view.post.id);
let searchBeta = await searchComment(beta, commentRes.comment_view.comment);
let betaComment = searchBeta.comments[0];
// find that comment id on beta
// Reply from beta
let replyRes = await createComment(
beta,
betaComment.post.id,
betaComment.comment.id
);
expect(replyRes.comment_view.comment.content).toBeDefined();
expect(replyRes.comment_view.community.local).toBe(true);
expect(replyRes.comment_view.creator.local).toBe(true);
expect(replyRes.comment_view.comment.parent_id).toBe(betaComment.comment.id);
expect(replyRes.comment_view.counts.score).toBe(1);
// Make sure that comment is seen on alpha
// TODO not sure why, but a searchComment back to alpha, for the ap_id of betas
// comment, isn't working.
// let searchAlpha = await searchComment(alpha, replyRes.comment);
let post = await getPost(alpha, postRes.post_view.post.id);
let alphaComment = post.comments[0];
expect(alphaComment.comment.content).toBeDefined();
expect(alphaComment.comment.parent_id).toBe(post.comments[1].comment.id);
expect(alphaComment.community.local).toBe(false);
expect(alphaComment.creator.local).toBe(false);
expect(alphaComment.counts.score).toBe(1);
assertCommentFederation(alphaComment, replyRes.comment_view);
});
test('Mention beta', async () => {
// Create a mention on alpha
let mentionContent = 'A test mention of @lemmy_beta@lemmy-beta:8551';
let commentRes = await createComment(alpha, postRes.post_view.post.id);
let mentionRes = await createComment(
alpha,
postRes.post_view.post.id,
commentRes.comment_view.comment.id,
mentionContent
);
expect(mentionRes.comment_view.comment.content).toBeDefined();
expect(mentionRes.comment_view.community.local).toBe(false);
expect(mentionRes.comment_view.creator.local).toBe(true);
expect(mentionRes.comment_view.counts.score).toBe(1);
let mentionsRes = await getMentions(beta);
expect(mentionsRes.mentions[0].comment.content).toBeDefined();
expect(mentionsRes.mentions[0].community.local).toBe(true);
expect(mentionsRes.mentions[0].creator.local).toBe(false);
expect(mentionsRes.mentions[0].counts.score).toBe(1);
});
test('Comment Search', async () => {
let commentRes = await createComment(alpha, postRes.post_view.post.id);
let searchBeta = await searchComment(beta, commentRes.comment_view.comment);
assertCommentFederation(searchBeta.comments[0], commentRes.comment_view);
});
test('A and G subscribe to B (center) A posts, G mentions B, it gets announced to A', async () => {
// Create a local post
let alphaPost = await createPost(alpha, 2);
expect(alphaPost.post_view.community.local).toBe(true);
// Make sure gamma sees it
let search = await searchPost(gamma, alphaPost.post_view.post);
let gammaPost = search.posts[0];
let commentContent =
'A jest test federated comment announce, lets mention @lemmy_beta@lemmy-beta:8551';
let commentRes = await createComment(
gamma,
gammaPost.post.id,
undefined,
commentContent
);
expect(commentRes.comment_view.comment.content).toBe(commentContent);
expect(commentRes.comment_view.community.local).toBe(false);
expect(commentRes.comment_view.creator.local).toBe(true);
expect(commentRes.comment_view.counts.score).toBe(1);
// Make sure alpha sees it
let alphaPost2 = await getPost(alpha, alphaPost.post_view.post.id);
expect(alphaPost2.comments[0].comment.content).toBe(commentContent);
expect(alphaPost2.comments[0].community.local).toBe(true);
expect(alphaPost2.comments[0].creator.local).toBe(false);
expect(alphaPost2.comments[0].counts.score).toBe(1);
assertCommentFederation(alphaPost2.comments[0], commentRes.comment_view);
// Make sure beta has mentions
let mentionsRes = await getMentions(beta);
expect(mentionsRes.mentions[0].comment.content).toBe(commentContent);
expect(mentionsRes.mentions[0].community.local).toBe(false);
expect(mentionsRes.mentions[0].creator.local).toBe(false);
// TODO this is failing because fetchInReplyTos aren't getting score
// expect(mentionsRes.mentions[0].score).toBe(1);
});
test('Fetch in_reply_tos: A is unsubbed from B, B makes a post, and some embedded comments, A subs to B, B updates the lowest level comment, A fetches both the post and all the inreplyto comments for that post.', async () => {
// Unfollow all remote communities
let followed = await unfollowRemotes(alpha);
expect(
followed.communities.filter(c => c.community.local == false).length
).toBe(0);
// B creates a post, and two comments, should be invisible to A
let postRes = await createPost(beta, 2);
expect(postRes.post_view.post.name).toBeDefined();
let parentCommentContent = 'An invisible top level comment from beta';
let parentCommentRes = await createComment(
beta,
postRes.post_view.post.id,
undefined,
parentCommentContent
);
expect(parentCommentRes.comment_view.comment.content).toBe(
parentCommentContent
);
// B creates a comment, then a child one of that.
let childCommentContent = 'An invisible child comment from beta';
let childCommentRes = await createComment(
beta,
postRes.post_view.post.id,
parentCommentRes.comment_view.comment.id,
childCommentContent
);
expect(childCommentRes.comment_view.comment.content).toBe(
childCommentContent
);
// Follow beta again
let follow = await followBeta(alpha);
expect(follow.community_view.community.local).toBe(false);
expect(follow.community_view.community.name).toBe('main');
// An update to the child comment on beta, should push the post, parent, and child to alpha now
let updatedCommentContent = 'An update child comment from beta';
let updateRes = await editComment(
beta,
childCommentRes.comment_view.comment.id,
updatedCommentContent
);
expect(updateRes.comment_view.comment.content).toBe(updatedCommentContent);
// Get the post from alpha
let search = await searchPost(alpha, postRes.post_view.post);
let alphaPostB = search.posts[0];
let alphaPost = await getPost(alpha, alphaPostB.post.id);
expect(alphaPost.post_view.post.name).toBeDefined();
assertCommentFederation(alphaPost.comments[1], parentCommentRes.comment_view);
assertCommentFederation(alphaPost.comments[0], updateRes.comment_view);
expect(alphaPost.post_view.community.local).toBe(false);
expect(alphaPost.post_view.creator.local).toBe(false);
await unfollowRemotes(alpha);
});

View file

@ -0,0 +1,166 @@
jest.setTimeout(120000);
import {
alpha,
beta,
setupLogins,
searchForCommunity,
createCommunity,
deleteCommunity,
removeCommunity,
getCommunity,
followCommunity,
} from './shared';
import { CommunityView } from 'lemmy-js-client';
beforeAll(async () => {
await setupLogins();
});
function assertCommunityFederation(
communityOne: CommunityView,
communityTwo: CommunityView
) {
expect(communityOne.community.actor_id).toBe(communityTwo.community.actor_id);
expect(communityOne.community.name).toBe(communityTwo.community.name);
expect(communityOne.community.title).toBe(communityTwo.community.title);
expect(communityOne.community.description).toBe(
communityTwo.community.description
);
expect(communityOne.community.icon).toBe(communityTwo.community.icon);
expect(communityOne.community.banner).toBe(communityTwo.community.banner);
expect(communityOne.community.published).toBe(
communityTwo.community.published
);
expect(communityOne.creator.actor_id).toBe(communityTwo.creator.actor_id);
expect(communityOne.community.nsfw).toBe(communityTwo.community.nsfw);
expect(communityOne.community.removed).toBe(communityTwo.community.removed);
expect(communityOne.community.deleted).toBe(communityTwo.community.deleted);
}
test('Create community', async () => {
let communityRes = await createCommunity(alpha);
expect(communityRes.community_view.community.name).toBeDefined();
// A dupe check
let prevName = communityRes.community_view.community.name;
let communityRes2: any = await createCommunity(alpha, prevName);
expect(communityRes2['error']).toBe('community_already_exists');
// Cache the community on beta, make sure it has the other fields
let searchShort = `!${prevName}@lemmy-alpha:8541`;
let search = await searchForCommunity(beta, searchShort);
let communityOnBeta = search.communities[0];
assertCommunityFederation(communityOnBeta, communityRes.community_view);
});
test('Delete community', async () => {
let communityRes = await createCommunity(beta);
// Cache the community on Alpha
let searchShort = `!${communityRes.community_view.community.name}@lemmy-beta:8551`;
let search = await searchForCommunity(alpha, searchShort);
let communityOnAlpha = search.communities[0];
assertCommunityFederation(communityOnAlpha, communityRes.community_view);
// Follow the community from alpha
let follow = await followCommunity(
alpha,
true,
communityOnAlpha.community.id
);
// Make sure the follow response went through
expect(follow.community_view.community.local).toBe(false);
let deleteCommunityRes = await deleteCommunity(
beta,
true,
communityRes.community_view.community.id
);
expect(deleteCommunityRes.community_view.community.deleted).toBe(true);
// Make sure it got deleted on A
let communityOnAlphaDeleted = await getCommunity(
alpha,
communityOnAlpha.community.id
);
expect(communityOnAlphaDeleted.community_view.community.deleted).toBe(true);
// Undelete
let undeleteCommunityRes = await deleteCommunity(
beta,
false,
communityRes.community_view.community.id
);
expect(undeleteCommunityRes.community_view.community.deleted).toBe(false);
// Make sure it got undeleted on A
let communityOnAlphaUnDeleted = await getCommunity(
alpha,
communityOnAlpha.community.id
);
expect(communityOnAlphaUnDeleted.community_view.community.deleted).toBe(
false
);
});
test('Remove community', async () => {
let communityRes = await createCommunity(beta);
// Cache the community on Alpha
let searchShort = `!${communityRes.community_view.community.name}@lemmy-beta:8551`;
let search = await searchForCommunity(alpha, searchShort);
let communityOnAlpha = search.communities[0];
assertCommunityFederation(communityOnAlpha, communityRes.community_view);
// Follow the community from alpha
let follow = await followCommunity(
alpha,
true,
communityOnAlpha.community.id
);
// Make sure the follow response went through
expect(follow.community_view.community.local).toBe(false);
let removeCommunityRes = await removeCommunity(
beta,
true,
communityRes.community_view.community.id
);
expect(removeCommunityRes.community_view.community.removed).toBe(true);
// Make sure it got Removed on A
let communityOnAlphaRemoved = await getCommunity(
alpha,
communityOnAlpha.community.id
);
expect(communityOnAlphaRemoved.community_view.community.removed).toBe(true);
// unremove
let unremoveCommunityRes = await removeCommunity(
beta,
false,
communityRes.community_view.community.id
);
expect(unremoveCommunityRes.community_view.community.removed).toBe(false);
// Make sure it got undeleted on A
let communityOnAlphaUnRemoved = await getCommunity(
alpha,
communityOnAlpha.community.id
);
expect(communityOnAlphaUnRemoved.community_view.community.removed).toBe(
false
);
});
test('Search for beta community', async () => {
let communityRes = await createCommunity(beta);
expect(communityRes.community_view.community.name).toBeDefined();
let searchShort = `!${communityRes.community_view.community.name}@lemmy-beta:8551`;
let search = await searchForCommunity(alpha, searchShort);
let communityOnAlpha = search.communities[0];
assertCommunityFederation(communityOnAlpha, communityRes.community_view);
});

View file

@ -1,3 +1,4 @@
jest.setTimeout(120000);
import {
alpha,
setupLogins,
@ -17,22 +18,26 @@ afterAll(async () => {
test('Follow federated community', async () => {
let search = await searchForBetaCommunity(alpha); // TODO sometimes this is returning null?
let follow = await followCommunity(alpha, true, search.communities[0].id);
let follow = await followCommunity(
alpha,
true,
search.communities[0].community.id
);
// Make sure the follow response went through
expect(follow.community.local).toBe(false);
expect(follow.community.name).toBe('main');
expect(follow.community_view.community.local).toBe(false);
expect(follow.community_view.community.name).toBe('main');
// Check it from local
let followCheck = await checkFollowedCommunities(alpha);
let remoteCommunityId = followCheck.communities.filter(
c => c.community_local == false
)[0].community_id;
let remoteCommunityId = followCheck.communities.find(
c => c.community.local == false
).community.id;
expect(remoteCommunityId).toBeDefined();
// Test an unfollow
let unfollow = await followCommunity(alpha, false, remoteCommunityId);
expect(unfollow.community.local).toBe(false);
expect(unfollow.community_view.community.local).toBe(false);
// Make sure you are unsubbed locally
let unfollowCheck = await checkFollowedCommunities(alpha);

359
api_tests/src/post.spec.ts Normal file
View file

@ -0,0 +1,359 @@
jest.setTimeout(120000);
import {
alpha,
beta,
gamma,
delta,
epsilon,
setupLogins,
createPost,
editPost,
stickyPost,
lockPost,
searchPost,
likePost,
followBeta,
searchForBetaCommunity,
createComment,
deletePost,
removePost,
getPost,
unfollowRemotes,
searchForUser,
banPersonFromSite,
searchPostLocal,
banPersonFromCommunity,
} from './shared';
import { PostView, CommunityView } from 'lemmy-js-client';
let betaCommunity: CommunityView;
beforeAll(async () => {
await setupLogins();
let search = await searchForBetaCommunity(alpha);
betaCommunity = search.communities[0];
await unfollows();
});
afterAll(async () => {
await unfollows();
});
async function unfollows() {
await unfollowRemotes(alpha);
await unfollowRemotes(gamma);
await unfollowRemotes(delta);
await unfollowRemotes(epsilon);
}
function assertPostFederation(postOne: PostView, postTwo: PostView) {
expect(postOne.post.ap_id).toBe(postTwo.post.ap_id);
expect(postOne.post.name).toBe(postTwo.post.name);
expect(postOne.post.body).toBe(postTwo.post.body);
expect(postOne.post.url).toBe(postTwo.post.url);
expect(postOne.post.nsfw).toBe(postTwo.post.nsfw);
expect(postOne.post.embed_title).toBe(postTwo.post.embed_title);
expect(postOne.post.embed_description).toBe(postTwo.post.embed_description);
expect(postOne.post.embed_html).toBe(postTwo.post.embed_html);
expect(postOne.post.published).toBe(postTwo.post.published);
expect(postOne.community.actor_id).toBe(postTwo.community.actor_id);
expect(postOne.post.locked).toBe(postTwo.post.locked);
expect(postOne.post.removed).toBe(postTwo.post.removed);
expect(postOne.post.deleted).toBe(postTwo.post.deleted);
}
test('Create a post', async () => {
let postRes = await createPost(alpha, betaCommunity.community.id);
expect(postRes.post_view.post).toBeDefined();
expect(postRes.post_view.community.local).toBe(false);
expect(postRes.post_view.creator.local).toBe(true);
expect(postRes.post_view.counts.score).toBe(1);
// Make sure that post is liked on beta
let searchBeta = await searchPost(beta, postRes.post_view.post);
let betaPost = searchBeta.posts[0];
expect(betaPost).toBeDefined();
expect(betaPost.community.local).toBe(true);
expect(betaPost.creator.local).toBe(false);
expect(betaPost.counts.score).toBe(1);
assertPostFederation(betaPost, postRes.post_view);
// Delta only follows beta, so it should not see an alpha ap_id
let searchDelta = await searchPost(delta, postRes.post_view.post);
expect(searchDelta.posts[0]).toBeUndefined();
// Epsilon has alpha blocked, it should not see the alpha post
let searchEpsilon = await searchPost(epsilon, postRes.post_view.post);
expect(searchEpsilon.posts[0]).toBeUndefined();
});
test('Create a post in a non-existent community', async () => {
let postRes = await createPost(alpha, -2);
expect(postRes).toStrictEqual({ error: 'couldnt_create_post' });
});
test('Unlike a post', async () => {
let postRes = await createPost(alpha, betaCommunity.community.id);
let unlike = await likePost(alpha, 0, postRes.post_view.post);
expect(unlike.post_view.counts.score).toBe(0);
// Try to unlike it again, make sure it stays at 0
let unlike2 = await likePost(alpha, 0, postRes.post_view.post);
expect(unlike2.post_view.counts.score).toBe(0);
// Make sure that post is unliked on beta
let searchBeta = await searchPost(beta, postRes.post_view.post);
let betaPost = searchBeta.posts[0];
expect(betaPost).toBeDefined();
expect(betaPost.community.local).toBe(true);
expect(betaPost.creator.local).toBe(false);
expect(betaPost.counts.score).toBe(0);
assertPostFederation(betaPost, postRes.post_view);
});
test('Update a post', async () => {
let postRes = await createPost(alpha, betaCommunity.community.id);
let updatedName = 'A jest test federated post, updated';
let updatedPost = await editPost(alpha, postRes.post_view.post);
expect(updatedPost.post_view.post.name).toBe(updatedName);
expect(updatedPost.post_view.community.local).toBe(false);
expect(updatedPost.post_view.creator.local).toBe(true);
// Make sure that post is updated on beta
let searchBeta = await searchPost(beta, postRes.post_view.post);
let betaPost = searchBeta.posts[0];
expect(betaPost.community.local).toBe(true);
expect(betaPost.creator.local).toBe(false);
expect(betaPost.post.name).toBe(updatedName);
assertPostFederation(betaPost, updatedPost.post_view);
// Make sure lemmy beta cannot update the post
let updatedPostBeta = await editPost(beta, betaPost.post);
expect(updatedPostBeta).toStrictEqual({ error: 'no_post_edit_allowed' });
});
test('Sticky a post', async () => {
let postRes = await createPost(alpha, betaCommunity.community.id);
let stickiedPostRes = await stickyPost(alpha, true, postRes.post_view.post);
expect(stickiedPostRes.post_view.post.stickied).toBe(true);
// Make sure that post is stickied on beta
let searchBeta = await searchPost(beta, postRes.post_view.post);
let betaPost = searchBeta.posts[0];
expect(betaPost.community.local).toBe(true);
expect(betaPost.creator.local).toBe(false);
expect(betaPost.post.stickied).toBe(true);
// Unsticky a post
let unstickiedPost = await stickyPost(alpha, false, postRes.post_view.post);
expect(unstickiedPost.post_view.post.stickied).toBe(false);
// Make sure that post is unstickied on beta
let searchBeta2 = await searchPost(beta, postRes.post_view.post);
let betaPost2 = searchBeta2.posts[0];
expect(betaPost2.community.local).toBe(true);
expect(betaPost2.creator.local).toBe(false);
expect(betaPost2.post.stickied).toBe(false);
// Make sure that gamma cannot sticky the post on beta
let searchGamma = await searchPost(gamma, postRes.post_view.post);
let gammaPost = searchGamma.posts[0];
let gammaTrySticky = await stickyPost(gamma, true, gammaPost.post);
let searchBeta3 = await searchPost(beta, postRes.post_view.post);
let betaPost3 = searchBeta3.posts[0];
expect(gammaTrySticky.post_view.post.stickied).toBe(true);
expect(betaPost3.post.stickied).toBe(false);
});
test('Lock a post', async () => {
let postRes = await createPost(alpha, betaCommunity.community.id);
// Lock the post
let lockedPostRes = await lockPost(alpha, true, postRes.post_view.post);
expect(lockedPostRes.post_view.post.locked).toBe(true);
// Make sure that post is locked on beta
let searchBeta = await searchPostLocal(beta, postRes.post_view.post);
let betaPost1 = searchBeta.posts[0];
expect(betaPost1.post.locked).toBe(true);
// Try to make a new comment there, on alpha
let comment: any = await createComment(alpha, postRes.post_view.post.id);
expect(comment['error']).toBe('locked');
// Unlock a post
let unlockedPost = await lockPost(alpha, false, postRes.post_view.post);
expect(unlockedPost.post_view.post.locked).toBe(false);
// Make sure that post is unlocked on beta
let searchBeta2 = await searchPost(beta, postRes.post_view.post);
let betaPost2 = searchBeta2.posts[0];
expect(betaPost2.community.local).toBe(true);
expect(betaPost2.creator.local).toBe(false);
expect(betaPost2.post.locked).toBe(false);
// Try to create a new comment, on beta
let commentBeta = await createComment(beta, betaPost2.post.id);
expect(commentBeta).toBeDefined();
});
test('Delete a post', async () => {
let postRes = await createPost(alpha, betaCommunity.community.id);
expect(postRes.post_view.post).toBeDefined();
let deletedPost = await deletePost(alpha, true, postRes.post_view.post);
expect(deletedPost.post_view.post.deleted).toBe(true);
// Make sure lemmy beta sees post is deleted
let searchBeta = await searchPost(beta, postRes.post_view.post);
let betaPost = searchBeta.posts[0];
// This will be undefined because of the tombstone
expect(betaPost).toBeUndefined();
// Undelete
let undeletedPost = await deletePost(alpha, false, postRes.post_view.post);
expect(undeletedPost.post_view.post.deleted).toBe(false);
// Make sure lemmy beta sees post is undeleted
let searchBeta2 = await searchPost(beta, postRes.post_view.post);
let betaPost2 = searchBeta2.posts[0];
expect(betaPost2.post.deleted).toBe(false);
assertPostFederation(betaPost2, undeletedPost.post_view);
// Make sure lemmy beta cannot delete the post
let deletedPostBeta = await deletePost(beta, true, betaPost2.post);
expect(deletedPostBeta).toStrictEqual({ error: 'no_post_edit_allowed' });
});
test('Remove a post from admin and community on different instance', async () => {
let postRes = await createPost(alpha, betaCommunity.community.id);
let removedPost = await removePost(alpha, true, postRes.post_view.post);
expect(removedPost.post_view.post.removed).toBe(true);
// Make sure lemmy beta sees post is NOT removed
let searchBeta = await searchPost(beta, postRes.post_view.post);
let betaPost = searchBeta.posts[0];
expect(betaPost.post.removed).toBe(false);
// Undelete
let undeletedPost = await removePost(alpha, false, postRes.post_view.post);
expect(undeletedPost.post_view.post.removed).toBe(false);
// Make sure lemmy beta sees post is undeleted
let searchBeta2 = await searchPost(beta, postRes.post_view.post);
let betaPost2 = searchBeta2.posts[0];
expect(betaPost2.post.removed).toBe(false);
assertPostFederation(betaPost2, undeletedPost.post_view);
});
test('Remove a post from admin and community on same instance', async () => {
await followBeta(alpha);
let postRes = await createPost(alpha, betaCommunity.community.id);
expect(postRes.post_view.post).toBeDefined();
// Get the id for beta
let searchBeta = await searchPostLocal(beta, postRes.post_view.post);
let betaPost = searchBeta.posts[0];
expect(betaPost).toBeDefined();
// The beta admin removes it (the community lives on beta)
let removePostRes = await removePost(beta, true, betaPost.post);
expect(removePostRes.post_view.post.removed).toBe(true);
// Make sure lemmy alpha sees post is removed
let alphaPost = await getPost(alpha, postRes.post_view.post.id);
// expect(alphaPost.post_view.post.removed).toBe(true); // TODO this shouldn't be commented
// assertPostFederation(alphaPost.post_view, removePostRes.post_view);
// Undelete
let undeletedPost = await removePost(beta, false, betaPost.post);
expect(undeletedPost.post_view.post.removed).toBe(false);
// Make sure lemmy alpha sees post is undeleted
let alphaPost2 = await getPost(alpha, postRes.post_view.post.id);
expect(alphaPost2.post_view.post.removed).toBe(false);
assertPostFederation(alphaPost2.post_view, undeletedPost.post_view);
await unfollowRemotes(alpha);
});
test('Search for a post', async () => {
await unfollowRemotes(alpha);
let postRes = await createPost(alpha, betaCommunity.community.id);
expect(postRes.post_view.post).toBeDefined();
let searchBeta = await searchPost(beta, postRes.post_view.post);
expect(searchBeta.posts[0].post.name).toBeDefined();
});
test('A and G subscribe to B (center) A posts, it gets announced to G', async () => {
let postRes = await createPost(alpha, betaCommunity.community.id);
expect(postRes.post_view.post).toBeDefined();
let search2 = await searchPost(gamma, postRes.post_view.post);
expect(search2.posts[0].post.name).toBeDefined();
});
test('Enforce site ban for federated user', async () => {
let alphaShortname = `@lemmy_alpha@lemmy-alpha:8541`;
let userSearch = await searchForUser(beta, alphaShortname);
let alphaUser = userSearch.users[0];
expect(alphaUser).toBeDefined();
// ban alpha from beta site
let banAlpha = await banPersonFromSite(beta, alphaUser.person.id, true);
expect(banAlpha.banned).toBe(true);
// Alpha makes post on beta
let postRes = await createPost(alpha, betaCommunity.community.id);
expect(postRes.post_view.post).toBeDefined();
expect(postRes.post_view.community.local).toBe(false);
expect(postRes.post_view.creator.local).toBe(true);
expect(postRes.post_view.counts.score).toBe(1);
// Make sure that post doesn't make it to beta
let searchBeta = await searchPostLocal(beta, postRes.post_view.post);
let betaPost = searchBeta.posts[0];
expect(betaPost).toBeUndefined();
// Unban alpha
let unBanAlpha = await banPersonFromSite(beta, alphaUser.person.id, false);
expect(unBanAlpha.banned).toBe(false);
});
test('Enforce community ban for federated user', async () => {
let alphaShortname = `@lemmy_alpha@lemmy-alpha:8541`;
let userSearch = await searchForUser(beta, alphaShortname);
let alphaUser = userSearch.users[0];
expect(alphaUser).toBeDefined();
// ban alpha from beta site
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
let postRes = await createPost(alpha, betaCommunity.community.id);
expect(postRes.post_view.post).toBeDefined();
expect(postRes.post_view.community.local).toBe(false);
expect(postRes.post_view.creator.local).toBe(true);
expect(postRes.post_view.counts.score).toBe(1);
// Make sure that post doesn't make it to beta community
let searchBeta = await searchPostLocal(beta, postRes.post_view.post);
let betaPost = searchBeta.posts[0];
expect(betaPost).toBeUndefined();
// Unban alpha
let unBanAlpha = await banPersonFromCommunity(
beta,
alphaUser.person.id,
2,
false
);
expect(unBanAlpha.banned).toBe(false);
});

View file

@ -0,0 +1,90 @@
jest.setTimeout(120000);
import {
alpha,
beta,
setupLogins,
followBeta,
createPrivateMessage,
editPrivateMessage,
listPrivateMessages,
deletePrivateMessage,
unfollowRemotes,
} from './shared';
let recipient_id: number;
beforeAll(async () => {
await setupLogins();
let follow = await followBeta(alpha);
recipient_id = follow.community_view.creator.id;
});
afterAll(async () => {
await unfollowRemotes(alpha);
});
test('Create a private message', async () => {
let pmRes = await createPrivateMessage(alpha, recipient_id);
expect(pmRes.private_message_view.private_message.content).toBeDefined();
expect(pmRes.private_message_view.private_message.local).toBe(true);
expect(pmRes.private_message_view.creator.local).toBe(true);
expect(pmRes.private_message_view.recipient.local).toBe(false);
let betaPms = await listPrivateMessages(beta);
expect(betaPms.private_messages[0].private_message.content).toBeDefined();
expect(betaPms.private_messages[0].private_message.local).toBe(false);
expect(betaPms.private_messages[0].creator.local).toBe(false);
expect(betaPms.private_messages[0].recipient.local).toBe(true);
});
test('Update a private message', async () => {
let updatedContent = 'A jest test federated private message edited';
let pmRes = await createPrivateMessage(alpha, recipient_id);
let pmUpdated = await editPrivateMessage(
alpha,
pmRes.private_message_view.private_message.id
);
expect(pmUpdated.private_message_view.private_message.content).toBe(
updatedContent
);
let betaPms = await listPrivateMessages(beta);
expect(betaPms.private_messages[0].private_message.content).toBe(
updatedContent
);
});
test('Delete a private message', async () => {
let pmRes = await createPrivateMessage(alpha, recipient_id);
let betaPms1 = await listPrivateMessages(beta);
let deletedPmRes = await deletePrivateMessage(
alpha,
true,
pmRes.private_message_view.private_message.id
);
expect(deletedPmRes.private_message_view.private_message.deleted).toBe(true);
// The GetPrivateMessages filters out deleted,
// even though they are in the actual database.
// no reason to show them
let betaPms2 = await listPrivateMessages(beta);
expect(betaPms2.private_messages.length).toBe(
betaPms1.private_messages.length - 1
);
// Undelete
let undeletedPmRes = await deletePrivateMessage(
alpha,
false,
pmRes.private_message_view.private_message.id
);
expect(undeletedPmRes.private_message_view.private_message.deleted).toBe(
false
);
let betaPms3 = await listPrivateMessages(beta);
expect(betaPms3.private_messages.length).toBe(
betaPms1.private_messages.length
);
});

636
api_tests/src/shared.ts Normal file
View file

@ -0,0 +1,636 @@
import {
Login,
LoginResponse,
CreatePost,
EditPost,
CreateComment,
DeletePost,
RemovePost,
StickyPost,
LockPost,
PostResponse,
SearchResponse,
FollowCommunity,
CommunityResponse,
GetFollowedCommunitiesResponse,
GetPostResponse,
Register,
Comment,
EditComment,
DeleteComment,
RemoveComment,
Search,
CommentResponse,
GetCommunity,
CreateCommunity,
DeleteCommunity,
RemoveCommunity,
GetPersonMentions,
CreateCommentLike,
CreatePostLike,
EditPrivateMessage,
DeletePrivateMessage,
GetFollowedCommunities,
GetPrivateMessages,
GetSite,
GetPost,
PrivateMessageResponse,
PrivateMessagesResponse,
GetPersonMentionsResponse,
SaveUserSettings,
SortType,
ListingType,
GetSiteResponse,
SearchType,
LemmyHttp,
BanPersonResponse,
BanPerson,
BanFromCommunity,
BanFromCommunityResponse,
Post,
CreatePrivateMessage,
} from 'lemmy-js-client';
export interface API {
client: LemmyHttp;
auth?: string;
}
export let alpha: API = {
client: new LemmyHttp('http://localhost:8541/api/v2'),
};
export let beta: API = {
client: new LemmyHttp('http://localhost:8551/api/v2'),
};
export let gamma: API = {
client: new LemmyHttp('http://localhost:8561/api/v2'),
};
export let delta: API = {
client: new LemmyHttp('http://localhost:8571/api/v2'),
};
export let epsilon: API = {
client: new LemmyHttp('http://localhost:8581/api/v2'),
};
export async function setupLogins() {
let formAlpha: Login = {
username_or_email: 'lemmy_alpha',
password: 'lemmy',
};
let resAlpha = alpha.client.login(formAlpha);
let formBeta = {
username_or_email: 'lemmy_beta',
password: 'lemmy',
};
let resBeta = beta.client.login(formBeta);
let formGamma = {
username_or_email: 'lemmy_gamma',
password: 'lemmy',
};
let resGamma = gamma.client.login(formGamma);
let formDelta = {
username_or_email: 'lemmy_delta',
password: 'lemmy',
};
let resDelta = delta.client.login(formDelta);
let formEpsilon = {
username_or_email: 'lemmy_epsilon',
password: 'lemmy',
};
let resEpsilon = epsilon.client.login(formEpsilon);
let res = await Promise.all([
resAlpha,
resBeta,
resGamma,
resDelta,
resEpsilon,
]);
alpha.auth = res[0].jwt;
beta.auth = res[1].jwt;
gamma.auth = res[2].jwt;
delta.auth = res[3].jwt;
epsilon.auth = res[4].jwt;
}
export async function createPost(
api: API,
community_id: number
): Promise<PostResponse> {
let name = randomString(5);
let body = randomString(10);
let url = 'https://google.com/';
let form: CreatePost = {
name,
url,
body,
auth: api.auth,
community_id,
nsfw: false,
};
return api.client.createPost(form);
}
export async function editPost(api: API, post: Post): Promise<PostResponse> {
let name = 'A jest test federated post, updated';
let form: EditPost = {
name,
post_id: post.id,
auth: api.auth,
nsfw: false,
};
return api.client.editPost(form);
}
export async function deletePost(
api: API,
deleted: boolean,
post: Post
): Promise<PostResponse> {
let form: DeletePost = {
post_id: post.id,
deleted: deleted,
auth: api.auth,
};
return api.client.deletePost(form);
}
export async function removePost(
api: API,
removed: boolean,
post: Post
): Promise<PostResponse> {
let form: RemovePost = {
post_id: post.id,
removed,
auth: api.auth,
};
return api.client.removePost(form);
}
export async function stickyPost(
api: API,
stickied: boolean,
post: Post
): Promise<PostResponse> {
let form: StickyPost = {
post_id: post.id,
stickied,
auth: api.auth,
};
return api.client.stickyPost(form);
}
export async function lockPost(
api: API,
locked: boolean,
post: Post
): Promise<PostResponse> {
let form: LockPost = {
post_id: post.id,
locked,
auth: api.auth,
};
return api.client.lockPost(form);
}
export async function searchPost(
api: API,
post: Post
): Promise<SearchResponse> {
let form: Search = {
q: post.ap_id,
type_: SearchType.Posts,
sort: SortType.TopAll,
};
return api.client.search(form);
}
export async function searchPostLocal(
api: API,
post: Post
): Promise<SearchResponse> {
let form: Search = {
q: post.name,
type_: SearchType.Posts,
sort: SortType.TopAll,
};
return api.client.search(form);
}
export async function getPost(
api: API,
post_id: number
): Promise<GetPostResponse> {
let form: GetPost = {
id: post_id,
};
return api.client.getPost(form);
}
export async function searchComment(
api: API,
comment: Comment
): Promise<SearchResponse> {
let form: Search = {
q: comment.ap_id,
type_: SearchType.Comments,
sort: SortType.TopAll,
};
return api.client.search(form);
}
export async function searchForBetaCommunity(
api: API
): Promise<SearchResponse> {
// Make sure lemmy-beta/c/main is cached on lemmy_alpha
// Use short-hand search url
let form: Search = {
q: '!main@lemmy-beta:8551',
type_: SearchType.Communities,
sort: SortType.TopAll,
};
return api.client.search(form);
}
export async function searchForCommunity(
api: API,
q: string
): Promise<SearchResponse> {
// Use short-hand search url
let form: Search = {
q,
type_: SearchType.Communities,
sort: SortType.TopAll,
};
return api.client.search(form);
}
export async function searchForUser(
api: API,
apShortname: string
): Promise<SearchResponse> {
// Make sure lemmy-beta/c/main is cached on lemmy_alpha
// Use short-hand search url
let form: Search = {
q: apShortname,
type_: SearchType.Users,
sort: SortType.TopAll,
};
return api.client.search(form);
}
export async function banPersonFromSite(
api: API,
person_id: number,
ban: boolean
): Promise<BanPersonResponse> {
// Make sure lemmy-beta/c/main is cached on lemmy_alpha
// Use short-hand search url
let form: BanPerson = {
person_id,
ban,
remove_data: false,
auth: api.auth,
};
return api.client.banPerson(form);
}
export async function banPersonFromCommunity(
api: API,
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 = {
person_id,
community_id,
remove_data: false,
ban,
auth: api.auth,
};
return api.client.banFromCommunity(form);
}
export async function followCommunity(
api: API,
follow: boolean,
community_id: number
): Promise<CommunityResponse> {
let form: FollowCommunity = {
community_id,
follow,
auth: api.auth,
};
return api.client.followCommunity(form);
}
export async function checkFollowedCommunities(
api: API
): Promise<GetFollowedCommunitiesResponse> {
let form: GetFollowedCommunities = {
auth: api.auth,
};
return api.client.getFollowedCommunities(form);
}
export async function likePost(
api: API,
score: number,
post: Post
): Promise<PostResponse> {
let form: CreatePostLike = {
post_id: post.id,
score: score,
auth: api.auth,
};
return api.client.likePost(form);
}
export async function createComment(
api: API,
post_id: number,
parent_id?: number,
content = 'a jest test comment'
): Promise<CommentResponse> {
let form: CreateComment = {
content,
post_id,
parent_id,
auth: api.auth,
};
return api.client.createComment(form);
}
export async function editComment(
api: API,
comment_id: number,
content = 'A jest test federated comment update'
): Promise<CommentResponse> {
let form: EditComment = {
content,
comment_id,
auth: api.auth,
};
return api.client.editComment(form);
}
export async function deleteComment(
api: API,
deleted: boolean,
comment_id: number
): Promise<CommentResponse> {
let form: DeleteComment = {
comment_id,
deleted,
auth: api.auth,
};
return api.client.deleteComment(form);
}
export async function removeComment(
api: API,
removed: boolean,
comment_id: number
): Promise<CommentResponse> {
let form: RemoveComment = {
comment_id,
removed,
auth: api.auth,
};
return api.client.removeComment(form);
}
export async function getMentions(api: API): Promise<GetPersonMentionsResponse> {
let form: GetPersonMentions = {
sort: SortType.New,
unread_only: false,
auth: api.auth,
};
return api.client.getPersonMentions(form);
}
export async function likeComment(
api: API,
score: number,
comment: Comment
): Promise<CommentResponse> {
let form: CreateCommentLike = {
comment_id: comment.id,
score,
auth: api.auth,
};
return api.client.likeComment(form);
}
export async function createCommunity(
api: API,
name_: string = randomString(5)
): Promise<CommunityResponse> {
let description = 'a sample description';
let icon = 'https://image.flaticon.com/icons/png/512/35/35896.png';
let banner = 'https://image.flaticon.com/icons/png/512/35/35896.png';
let form: CreateCommunity = {
name: name_,
title: name_,
description,
icon,
banner,
nsfw: false,
auth: api.auth,
};
return api.client.createCommunity(form);
}
export async function getCommunity(
api: API,
id: number
): Promise<CommunityResponse> {
let form: GetCommunity = {
id,
};
return api.client.getCommunity(form);
}
export async function deleteCommunity(
api: API,
deleted: boolean,
community_id: number
): Promise<CommunityResponse> {
let form: DeleteCommunity = {
community_id,
deleted,
auth: api.auth,
};
return api.client.deleteCommunity(form);
}
export async function removeCommunity(
api: API,
removed: boolean,
community_id: number
): Promise<CommunityResponse> {
let form: RemoveCommunity = {
community_id,
removed,
auth: api.auth,
};
return api.client.removeCommunity(form);
}
export async function createPrivateMessage(
api: API,
recipient_id: number
): Promise<PrivateMessageResponse> {
let content = 'A jest test federated private message';
let form: CreatePrivateMessage = {
content,
recipient_id,
auth: api.auth,
};
return api.client.createPrivateMessage(form);
}
export async function editPrivateMessage(
api: API,
private_message_id: number
): Promise<PrivateMessageResponse> {
let updatedContent = 'A jest test federated private message edited';
let form: EditPrivateMessage = {
content: updatedContent,
private_message_id,
auth: api.auth,
};
return api.client.editPrivateMessage(form);
}
export async function deletePrivateMessage(
api: API,
deleted: boolean,
private_message_id: number
): Promise<PrivateMessageResponse> {
let form: DeletePrivateMessage = {
deleted,
private_message_id,
auth: api.auth,
};
return api.client.deletePrivateMessage(form);
}
export async function registerUser(
api: API,
username: string = randomString(5)
): Promise<LoginResponse> {
let form: Register = {
username,
password: 'test',
password_verify: 'test',
show_nsfw: true,
};
return api.client.register(form);
}
export async function saveUserSettingsBio(
api: API,
auth: string
): Promise<LoginResponse> {
let form: SaveUserSettings = {
show_nsfw: true,
theme: 'darkly',
default_sort_type: Object.keys(SortType).indexOf(SortType.Active),
default_listing_type: Object.keys(ListingType).indexOf(ListingType.All),
lang: 'en',
show_avatars: true,
send_notifications_to_email: false,
bio: 'a changed bio',
auth,
};
return saveUserSettings(api, form);
}
export async function saveUserSettings(
api: API,
form: SaveUserSettings
): Promise<LoginResponse> {
return api.client.saveUserSettings(form);
}
export async function getSite(
api: API,
auth: string
): Promise<GetSiteResponse> {
let form: GetSite = {
auth,
};
return api.client.getSite(form);
}
export async function listPrivateMessages(
api: API
): Promise<PrivateMessagesResponse> {
let form: GetPrivateMessages = {
auth: api.auth,
unread_only: false,
limit: 999,
};
return api.client.getPrivateMessages(form);
}
export async function unfollowRemotes(
api: API
): Promise<GetFollowedCommunitiesResponse> {
// Unfollow all remote communities
let followed = await checkFollowedCommunities(api);
let remoteFollowed = followed.communities.filter(
c => c.community.local == false
);
for (let cu of remoteFollowed) {
await followCommunity(api, false, cu.community.id);
}
let followed2 = await checkFollowedCommunities(api);
return followed2;
}
export async function followBeta(api: API): Promise<CommunityResponse> {
// Cache it
let search = await searchForBetaCommunity(api);
let com = search.communities.find(c => c.community.local == false);
if (com) {
let follow = await followCommunity(api, true, com.community.id);
return follow;
}
}
export function delay(millis: number = 500) {
return new Promise(resolve => setTimeout(resolve, millis));
}
export function longDelay() {
return delay(10000);
}
export function wrapper(form: any): string {
return JSON.stringify(form);
}
function randomString(length: number): string {
var result = '';
var characters = 'abcdefghijklmnopqrstuvwxyz0123456789_';
var charactersLength = characters.length;
for (var i = 0; i < length; i++) {
result += characters.charAt(Math.floor(Math.random() * charactersLength));
}
return result;
}

View file

@ -0,0 +1,65 @@
jest.setTimeout(120000);
import {
alpha,
beta,
registerUser,
searchForUser,
saveUserSettings,
getSite,
} from './shared';
import {
PersonViewSafe,
SaveUserSettings,
SortType,
ListingType,
} from 'lemmy-js-client';
let auth: string;
let apShortname: string;
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 () => {
let userRes = await registerUser(alpha);
expect(userRes.jwt).toBeDefined();
auth = userRes.jwt;
let site = await getSite(alpha, auth);
expect(site.my_user).toBeDefined();
apShortname = `@${site.my_user.person.name}@lemmy-alpha:8541`;
});
test('Set some user settings, check that they are federated', async () => {
let avatar = 'https://image.flaticon.com/icons/png/512/35/35896.png';
let banner = 'https://image.flaticon.com/icons/png/512/36/35896.png';
let bio = 'a changed bio';
let form: SaveUserSettings = {
show_nsfw: false,
theme: '',
default_sort_type: Object.keys(SortType).indexOf(SortType.Hot),
default_listing_type: Object.keys(ListingType).indexOf(ListingType.All),
lang: '',
avatar,
banner,
preferred_username: 'user321',
show_avatars: false,
send_notifications_to_email: false,
bio,
auth,
};
await saveUserSettings(alpha, form);
let searchAlpha = await searchForUser(alpha, apShortname);
let userOnAlpha = searchAlpha.users[0];
let searchBeta = await searchForUser(beta, apShortname);
let userOnBeta = searchBeta.users[0];
assertUserFederation(userOnAlpha, userOnBeta);
});

16
api_tests/tsconfig.json Normal file
View file

@ -0,0 +1,16 @@
{
"compilerOptions": {
"declaration": true,
"declarationDir": "./dist",
"module": "CommonJS",
"noImplicitAny": true,
"lib": ["es2017", "es7", "es6", "dom"],
"outDir": "./dist",
"target": "ES5",
"moduleResolution": "Node"
},
"include": [
"src/**/*"
],
"exclude": ["node_modules", "dist"]
}

4951
api_tests/yarn.lock Normal file

File diff suppressed because it is too large Load diff

View file

@ -1,4 +1,3 @@
{
hostname: "localhost:8536"
federation_enabled: true
}
}

View file

@ -25,18 +25,20 @@
# maximum number of active sql connections
pool_size: 5
}
# the domain name of your instance (eg "dev.lemmy.ml")
# the domain name of your instance (eg "lemmy.ml")
hostname: null
# address where lemmy should listen for incoming requests
bind: "0.0.0.0"
# port where lemmy should listen for incoming requests
port: 8536
# whether tls is required for activitypub. only disable this for debugging, never for producion.
tls_enabled: true
# json web token for authorization between server and client
jwt_secret: "changeme"
# The location of the frontend
front_end_dir: "../ui/dist"
# address where pictrs is available
pictrs_url: "http://pictrs:8080"
# address where iframely is available
iframely_url: "http://iframely"
# rate limits for various user actions, by user ip
rate_limit: {
# maximum number of messages created in interval
@ -58,14 +60,16 @@
}
# settings related to activitypub federation
federation: {
# whether to enable activitypub federation. this feature is in alpha, do not enable in production.
# whether to enable activitypub federation.
enabled: false
# whether tls is required for activitypub. only disable this for debugging, never for producion.
tls_enabled: true
# Allows and blocks are described here:
# 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"] }

876
crates/api/src/comment.rs Normal file
View file

@ -0,0 +1,876 @@
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,
is_mod_or_admin,
Perform,
};
use actix_web::web::Data;
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,
Likeable,
ListingType,
Reportable,
Saveable,
SortType,
};
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_utils::{
utils::{remove_slurs, scrape_text_for_mentions},
ApiError,
ConnectionId,
LemmyError,
};
use lemmy_websocket::{
messages::{SendComment, SendModRoomMessage, SendUserRoomMessage},
LemmyContext,
UserOperation,
};
use std::str::FromStr;
#[async_trait::async_trait(?Send)]
impl Perform for CreateComment {
type Response = CommentResponse;
async fn perform(
&self,
context: &Data<LemmyContext>,
websocket_id: Option<ConnectionId>,
) -> Result<CommentResponse, LemmyError> {
let data: &CreateComment = &self;
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());
// Check for a community ban
let post_id = data.post_id;
let post = get_post(post_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());
}
// 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: local_user_view.person.id,
removed: None,
deleted: None,
read: None,
published: None,
updated: None,
ap_id: None,
local: true,
};
// Create the comment
let comment_form2 = comment_form.clone();
let inserted_comment = match blocking(context.pool(), move |conn| {
Comment::create(&conn, &comment_form2)
})
.await?
{
Ok(comment) => comment,
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| -> Result<Comment, LemmyError> {
let 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()),
};
updated_comment
.send_create(&local_user_view.person, context)
.await?;
// Scan the comment for user mentions, add those rows
let post_id = post.id;
let mentions = scrape_text_for_mentions(&comment_form.content);
let recipient_ids = send_local_notifs(
mentions,
updated_comment.clone(),
local_user_view.person.clone(),
post,
context.pool(),
true,
)
.await?;
// You like your own comment by default
let like_form = CommentLikeForm {
comment_id: inserted_comment.id,
post_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());
}
updated_comment
.send_like(&local_user_view.person, context)
.await?;
let person_id = local_user_view.person.id;
let mut comment_view = blocking(context.pool(), move |conn| {
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 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()),
};
comment_view.comment.read = true;
}
let mut res = CommentResponse {
comment_view,
recipient_ids,
form_id: data.form_id.to_owned(),
};
context.chat_server().do_send(SendComment {
op: UserOperation::CreateComment,
comment: res.clone(),
websocket_id,
});
res.recipient_ids = Vec::new(); // Necessary to avoid doubles
Ok(res)
}
}
#[async_trait::async_trait(?Send)]
impl Perform for EditComment {
type Response = CommentResponse;
async fn perform(
&self,
context: &Data<LemmyContext>,
websocket_id: Option<ConnectionId>,
) -> Result<CommentResponse, LemmyError> {
let data: &EditComment = &self;
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| {
CommentView::read(&conn, comment_id, None)
})
.await??;
check_community_ban(
local_user_view.person.id,
orig_comment.community.id,
context.pool(),
)
.await?;
// Verify that only the creator can edit
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 comment_id = data.comment_id;
let updated_comment = match blocking(context.pool(), move |conn| {
Comment::update_content(conn, comment_id, &content_slurs_removed)
})
.await?
{
Ok(comment) => comment,
Err(_e) => return Err(ApiError::err("couldnt_update_comment").into()),
};
// Send the apub update
updated_comment
.send_update(&local_user_view.person, context)
.await?;
// Do the mentions / recipients
let updated_comment_content = updated_comment.content.to_owned();
let mentions = scrape_text_for_mentions(&updated_comment_content);
let recipient_ids = send_local_notifs(
mentions,
updated_comment,
local_user_view.person.clone(),
orig_comment.post,
context.pool(),
false,
)
.await?;
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, comment_id, Some(person_id))
})
.await??;
let res = CommentResponse {
comment_view,
recipient_ids,
form_id: data.form_id.to_owned(),
};
context.chat_server().do_send(SendComment {
op: UserOperation::EditComment,
comment: res.clone(),
websocket_id,
});
Ok(res)
}
}
#[async_trait::async_trait(?Send)]
impl Perform for DeleteComment {
type Response = CommentResponse;
async fn perform(
&self,
context: &Data<LemmyContext>,
websocket_id: Option<ConnectionId>,
) -> Result<CommentResponse, LemmyError> {
let data: &DeleteComment = &self;
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| {
CommentView::read(&conn, comment_id, None)
})
.await??;
check_community_ban(
local_user_view.person.id,
orig_comment.community.id,
context.pool(),
)
.await?;
// Verify that only the creator can delete
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, comment_id, deleted)
})
.await?
{
Ok(comment) => comment,
Err(_e) => return Err(ApiError::err("couldnt_update_comment").into()),
};
// Send the apub message
if deleted {
updated_comment
.send_delete(&local_user_view.person, context)
.await?;
} else {
updated_comment
.send_undo_delete(&local_user_view.person, context)
.await?;
}
// Refetch it
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, comment_id, Some(person_id))
})
.await??;
// Build the recipients
let comment_view_2 = comment_view.clone();
let mentions = vec![];
let recipient_ids = send_local_notifs(
mentions,
updated_comment,
local_user_view.person.clone(),
comment_view_2.post,
context.pool(),
false,
)
.await?;
let res = CommentResponse {
comment_view,
recipient_ids,
form_id: None, // TODO a comment delete might clear forms?
};
context.chat_server().do_send(SendComment {
op: UserOperation::DeleteComment,
comment: res.clone(),
websocket_id,
});
Ok(res)
}
}
#[async_trait::async_trait(?Send)]
impl Perform for RemoveComment {
type Response = CommentResponse;
async fn perform(
&self,
context: &Data<LemmyContext>,
websocket_id: Option<ConnectionId>,
) -> Result<CommentResponse, LemmyError> {
let data: &RemoveComment = &self;
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| {
CommentView::read(&conn, comment_id, None)
})
.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(),
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, comment_id, removed)
})
.await?
{
Ok(comment) => comment,
Err(_e) => return Err(ApiError::err("couldnt_update_comment").into()),
};
// Mod tables
let form = ModRemoveCommentForm {
mod_person_id: local_user_view.person.id,
comment_id: data.comment_id,
removed: Some(removed),
reason: data.reason.to_owned(),
};
blocking(context.pool(), move |conn| {
ModRemoveComment::create(conn, &form)
})
.await??;
// Send the apub message
if removed {
updated_comment
.send_remove(&local_user_view.person, context)
.await?;
} else {
updated_comment
.send_undo_remove(&local_user_view.person, context)
.await?;
}
// Refetch it
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, comment_id, Some(person_id))
})
.await??;
// Build the recipients
let comment_view_2 = comment_view.clone();
let mentions = vec![];
let recipient_ids = send_local_notifs(
mentions,
updated_comment,
local_user_view.person.clone(),
comment_view_2.post,
context.pool(),
false,
)
.await?;
let res = CommentResponse {
comment_view,
recipient_ids,
form_id: None, // TODO maybe this might clear other forms
};
context.chat_server().do_send(SendComment {
op: UserOperation::RemoveComment,
comment: res.clone(),
websocket_id,
});
Ok(res)
}
}
#[async_trait::async_trait(?Send)]
impl Perform for MarkCommentAsRead {
type Response = CommentResponse;
async fn perform(
&self,
context: &Data<LemmyContext>,
_websocket_id: Option<ConnectionId>,
) -> Result<CommentResponse, LemmyError> {
let data: &MarkCommentAsRead = &self;
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| {
CommentView::read(&conn, comment_id, None)
})
.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 local_user_view.person.id != orig_comment.get_recipient_id() {
return Err(ApiError::err("no_comment_edit_allowed").into());
}
// Do the mark as read
let read = data.read;
match blocking(context.pool(), move |conn| {
Comment::update_read(conn, comment_id, read)
})
.await?
{
Ok(comment) => comment,
Err(_e) => return Err(ApiError::err("couldnt_update_comment").into()),
};
// Refetch it
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, comment_id, Some(person_id))
})
.await??;
let res = CommentResponse {
comment_view,
recipient_ids: Vec::new(),
form_id: None,
};
Ok(res)
}
}
#[async_trait::async_trait(?Send)]
impl Perform for SaveComment {
type Response = CommentResponse;
async fn perform(
&self,
context: &Data<LemmyContext>,
_websocket_id: Option<ConnectionId>,
) -> Result<CommentResponse, LemmyError> {
let data: &SaveComment = &self;
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,
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());
}
} 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());
}
}
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, comment_id, Some(person_id))
})
.await??;
Ok(CommentResponse {
comment_view,
recipient_ids: Vec::new(),
form_id: None,
})
}
}
#[async_trait::async_trait(?Send)]
impl Perform for CreateCommentLike {
type Response = CommentResponse;
async fn perform(
&self,
context: &Data<LemmyContext>,
websocket_id: Option<ConnectionId>,
) -> Result<CommentResponse, LemmyError> {
let data: &CreateCommentLike = &self;
let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
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?;
let comment_id = data.comment_id;
let orig_comment = blocking(context.pool(), move |conn| {
CommentView::read(&conn, comment_id, None)
})
.await??;
check_community_ban(
local_user_view.person.id,
orig_comment.community.id,
context.pool(),
)
.await?;
// Add parent user to recipients
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,
person_id: local_user_view.person.id,
score: data.score,
};
// Remove any likes first
let person_id = local_user_view.person.id;
blocking(context.pool(), move |conn| {
CommentLike::remove(conn, person_id, comment_id)
})
.await??;
// Only add the like if the score isnt 0
let comment = orig_comment.comment;
let do_add = like_form.score != 0 && (like_form.score == 1 || like_form.score == -1);
if do_add {
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());
}
if like_form.score == 1 {
comment.send_like(&local_user_view.person, context).await?;
} else if like_form.score == -1 {
comment
.send_dislike(&local_user_view.person, context)
.await?;
}
} else {
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 person_id = local_user_view.person.id;
let liked_comment = blocking(context.pool(), move |conn| {
CommentView::read(conn, comment_id, Some(person_id))
})
.await??;
let res = CommentResponse {
comment_view: liked_comment,
recipient_ids,
form_id: None,
};
context.chat_server().do_send(SendComment {
op: UserOperation::CreateCommentLike,
comment: res.clone(),
websocket_id,
});
Ok(res)
}
}
#[async_trait::async_trait(?Send)]
impl Perform for GetComments {
type Response = GetCommentsResponse;
async fn perform(
&self,
context: &Data<LemmyContext>,
_websocket_id: Option<ConnectionId>,
) -> Result<GetCommentsResponse, LemmyError> {
let data: &GetComments = &self;
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)?;
let community_id = data.community_id;
let community_name = data.community_name.to_owned();
let page = data.page;
let limit = data.limit;
let comments = blocking(context.pool(), move |conn| {
CommentQueryBuilder::create(conn)
.listing_type(type_)
.sort(&sort)
.community_id(community_id)
.community_name(community_name)
.my_person_id(person_id)
.page(page)
.limit(limit)
.list()
})
.await?;
let comments = match comments {
Ok(comments) => comments,
Err(_) => return Err(ApiError::err("couldnt_get_comments").into()),
};
Ok(GetCommentsResponse { comments })
}
}
/// Creates a comment report and notifies the moderators of the community
#[async_trait::async_trait(?Send)]
impl Perform for CreateCommentReport {
type Response = CreateCommentReportResponse;
async fn perform(
&self,
context: &Data<LemmyContext>,
websocket_id: Option<ConnectionId>,
) -> Result<CreateCommentReportResponse, LemmyError> {
let data: &CreateCommentReport = &self;
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());
}
if reason.chars().count() > 1000 {
return Err(ApiError::err("report_too_long").into());
}
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(person_id, comment_view.community.id, context.pool()).await?;
let report_form = CommentReportForm {
creator_id: person_id,
comment_id,
original_comment_text: comment_view.comment.content,
reason: data.reason.to_owned(),
};
let report = match blocking(context.pool(), move |conn| {
CommentReport::report(conn, &report_form)
})
.await?
{
Ok(report) => report,
Err(_e) => return Err(ApiError::err("couldnt_create_report").into()),
};
let res = CreateCommentReportResponse { success: true };
context.chat_server().do_send(SendUserRoomMessage {
op: UserOperation::CreateCommentReport,
response: res.clone(),
local_recipient_id: local_user_view.local_user.id,
websocket_id,
});
context.chat_server().do_send(SendModRoomMessage {
op: UserOperation::CreateCommentReport,
response: report,
community_id: comment_view.community.id,
websocket_id,
});
Ok(res)
}
}
/// Resolves or unresolves a comment report and notifies the moderators of the community
#[async_trait::async_trait(?Send)]
impl Perform for ResolveCommentReport {
type Response = ResolveCommentReportResponse;
async fn perform(
&self,
context: &Data<LemmyContext>,
websocket_id: Option<ConnectionId>,
) -> Result<ResolveCommentReportResponse, LemmyError> {
let data: &ResolveCommentReport = &self;
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| {
CommentReportView::read(&conn, report_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, person_id)
} else {
CommentReport::unresolve(conn, report_id, person_id)
}
};
if blocking(context.pool(), resolve_fun).await?.is_err() {
return Err(ApiError::err("couldnt_resolve_report").into());
};
let report_id = data.report_id;
let res = ResolveCommentReportResponse {
report_id,
resolved,
};
context.chat_server().do_send(SendModRoomMessage {
op: UserOperation::ResolveCommentReport,
response: res.clone(),
community_id: report.community.id,
websocket_id,
});
Ok(res)
}
}
/// Lists comment reports for a community if an id is supplied
/// or returns all comment reports for communities a user moderates
#[async_trait::async_trait(?Send)]
impl Perform for ListCommentReports {
type Response = ListCommentReportsResponse;
async fn perform(
&self,
context: &Data<LemmyContext>,
websocket_id: Option<ConnectionId>,
) -> Result<ListCommentReportsResponse, LemmyError> {
let data: &ListCommentReports = &self;
let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
let person_id = local_user_view.person.id;
let community_id = data.community;
let community_ids =
collect_moderated_communities(person_id, community_id, context.pool()).await?;
let page = data.page;
let limit = data.limit;
let comments = blocking(context.pool(), move |conn| {
CommentReportQueryBuilder::create(conn)
.community_ids(community_ids)
.page(page)
.limit(limit)
.list()
})
.await??;
let res = ListCommentReportsResponse { comments };
context.chat_server().do_send(SendUserRoomMessage {
op: UserOperation::ListCommentReports,
response: res.clone(),
local_recipient_id: local_user_view.local_user.id,
websocket_id,
});
Ok(res)
}
}

903
crates/api/src/community.rs Normal file
View file

@ -0,0 +1,903 @@
use crate::{
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_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_to_url,
source::{
comment::Comment_,
community::{CommunityModerator_, Community_},
post::Post_,
},
ApubObject,
Bannable,
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},
person_view::PersonViewSafe,
};
use lemmy_utils::{
apub::generate_actor_keypair,
location_info,
utils::{check_slurs, check_slurs_opt, is_valid_community_name, naive_from_unix},
ApiError,
ConnectionId,
LemmyError,
};
use lemmy_websocket::{
messages::{GetCommunityUsersOnline, SendCommunityRoomMessage},
LemmyContext,
UserOperation,
};
use std::str::FromStr;
#[async_trait::async_trait(?Send)]
impl Perform for GetCommunity {
type Response = GetCommunityResponse;
async fn perform(
&self,
context: &Data<LemmyContext>,
_websocket_id: Option<ConnectionId>,
) -> Result<GetCommunityResponse, LemmyError> {
let data: &GetCommunity = &self;
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,
None => {
let name = data.name.to_owned().unwrap_or_else(|| "main".to_string());
match blocking(context.pool(), move |conn| {
Community::read_from_name(conn, &name)
})
.await?
{
Ok(community) => community,
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, person_id)
})
.await?
{
Ok(community) => community,
Err(_e) => return Err(ApiError::err("couldnt_find_community").into()),
};
let moderators: Vec<CommunityModeratorView> = match blocking(context.pool(), move |conn| {
CommunityModeratorView::for_community(conn, community_id)
})
.await?
{
Ok(moderators) => moderators,
Err(_e) => return Err(ApiError::err("couldnt_find_community").into()),
};
let online = context
.chat_server()
.send(GetCommunityUsersOnline { community_id })
.await
.unwrap_or(1);
let res = GetCommunityResponse {
community_view,
moderators,
online,
};
// Return the jwt
Ok(res)
}
}
#[async_trait::async_trait(?Send)]
impl Perform for CreateCommunity {
type Response = CommunityResponse;
async fn perform(
&self,
context: &Data<LemmyContext>,
_websocket_id: Option<ConnectionId>,
) -> Result<CommunityResponse, LemmyError> {
let data: &CreateCommunity = &self;
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());
}
// Double check for duplicate community actor_ids
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());
}
// Check to make sure the icon and banners are urls
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()?;
let community_form = CommunityForm {
name: data.name.to_owned(),
title: data.title.to_owned(),
description: data.description.to_owned(),
icon,
banner,
creator_id: local_user_view.person.id,
removed: None,
deleted: None,
nsfw: data.nsfw,
updated: None,
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| {
Community::create(conn, &community_form)
})
.await?
{
Ok(community) => community,
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,
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());
}
// Follow your own community
let community_follower_form = CommunityFollowerForm {
community_id: inserted_community.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());
}
let person_id = local_user_view.person.id;
let community_view = blocking(context.pool(), move |conn| {
CommunityView::read(conn, inserted_community.id, Some(person_id))
})
.await??;
Ok(CommunityResponse { community_view })
}
}
#[async_trait::async_trait(?Send)]
impl Perform for EditCommunity {
type Response = CommunityResponse;
async fn perform(
&self,
context: &Data<LemmyContext>,
websocket_id: Option<ConnectionId>,
) -> Result<CommunityResponse, LemmyError> {
let data: &EditCommunity = &self;
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 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(&local_user_view.person.id) {
return Err(ApiError::err("not_a_moderator").into());
}
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_to_url(&data.icon)?;
let banner = diesel_option_overwrite_to_url(&data.banner)?;
let community_form = CommunityForm {
name: read_community.name,
title: data.title.to_owned(),
description: data.description.to_owned(),
icon,
banner,
creator_id: read_community.creator_id,
removed: Some(read_community.removed),
deleted: Some(read_community.deleted),
nsfw: data.nsfw,
updated: Some(naive_now()),
actor_id: Some(read_community.actor_id),
local: read_community.local,
private_key: read_community.private_key,
public_key: read_community.public_key,
last_refreshed_at: None,
published: None,
followers_url: None,
inbox_url: None,
shared_inbox_url: None,
};
let community_id = data.community_id;
match blocking(context.pool(), move |conn| {
Community::update(conn, community_id, &community_form)
})
.await?
{
Ok(community) => community,
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 community_id = data.community_id;
let person_id = local_user_view.person.id;
let community_view = blocking(context.pool(), move |conn| {
CommunityView::read(conn, community_id, Some(person_id))
})
.await??;
let res = CommunityResponse { community_view };
send_community_websocket(&res, context, websocket_id, UserOperation::EditCommunity);
Ok(res)
}
}
#[async_trait::async_trait(?Send)]
impl Perform for DeleteCommunity {
type Response = CommunityResponse;
async fn perform(
&self,
context: &Data<LemmyContext>,
websocket_id: Option<ConnectionId>,
) -> Result<CommunityResponse, LemmyError> {
let data: &DeleteCommunity = &self;
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 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 community_id = data.community_id;
let deleted = data.deleted;
let updated_community = match blocking(context.pool(), move |conn| {
Community::update_deleted(conn, community_id, deleted)
})
.await?
{
Ok(community) => community,
Err(_e) => return Err(ApiError::err("couldnt_update_community").into()),
};
// Send apub messages
if deleted {
updated_community.send_delete(context).await?;
} else {
updated_community.send_undo_delete(context).await?;
}
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, community_id, Some(person_id))
})
.await??;
let res = CommunityResponse { community_view };
send_community_websocket(&res, context, websocket_id, UserOperation::DeleteCommunity);
Ok(res)
}
}
#[async_trait::async_trait(?Send)]
impl Perform for RemoveCommunity {
type Response = CommunityResponse;
async fn perform(
&self,
context: &Data<LemmyContext>,
websocket_id: Option<ConnectionId>,
) -> Result<CommunityResponse, LemmyError> {
let data: &RemoveCommunity = &self;
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(&local_user_view)?;
// Do the remove
let community_id = data.community_id;
let removed = data.removed;
let updated_community = match blocking(context.pool(), move |conn| {
Community::update_removed(conn, community_id, removed)
})
.await?
{
Ok(community) => community,
Err(_e) => return Err(ApiError::err("couldnt_update_community").into()),
};
// Mod tables
let expires = match data.expires {
Some(time) => Some(naive_from_unix(time)),
None => None,
};
let form = ModRemoveCommunityForm {
mod_person_id: local_user_view.person.id,
community_id: data.community_id,
removed: Some(removed),
reason: data.reason.to_owned(),
expires,
};
blocking(context.pool(), move |conn| {
ModRemoveCommunity::create(conn, &form)
})
.await??;
// Apub messages
if removed {
updated_community.send_remove(context).await?;
} else {
updated_community.send_undo_remove(context).await?;
}
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, community_id, Some(person_id))
})
.await??;
let res = CommunityResponse { community_view };
send_community_websocket(&res, context, websocket_id, UserOperation::RemoveCommunity);
Ok(res)
}
}
#[async_trait::async_trait(?Send)]
impl Perform for ListCommunities {
type Response = ListCommunitiesResponse;
async fn perform(
&self,
context: &Data<LemmyContext>,
_websocket_id: Option<ConnectionId>,
) -> Result<ListCommunitiesResponse, LemmyError> {
let data: &ListCommunities = &self;
let local_user_view = get_local_user_view_from_jwt_opt(&data.auth, context.pool()).await?;
let person_id = match &local_user_view {
Some(uv) => Some(uv.person.id),
None => None,
};
// 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_person_id(person_id)
.page(page)
.limit(limit)
.list()
})
.await??;
// Return the jwt
Ok(ListCommunitiesResponse { communities })
}
}
#[async_trait::async_trait(?Send)]
impl Perform for FollowCommunity {
type Response = CommunityResponse;
async fn perform(
&self,
context: &Data<LemmyContext>,
_websocket_id: Option<ConnectionId>,
) -> Result<CommunityResponse, LemmyError> {
let data: &FollowCommunity = &self;
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| {
Community::read(conn, community_id)
})
.await??;
let community_follower_form = CommunityFollowerForm {
community_id: data.community_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());
}
} 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());
}
}
} else if data.follow {
// Dont actually add to the community followers here, because you need
// to wait for the accept
local_user_view
.person
.send_follow(&community.actor_id(), context)
.await?;
} else {
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());
}
}
let community_id = data.community_id;
let person_id = local_user_view.person.id;
let mut community_view = blocking(context.pool(), move |conn| {
CommunityView::read(conn, community_id, Some(person_id))
})
.await??;
// TODO: this needs to return a "pending" state, until Accept is received from the remote server
// For now, just assume that remote follows are accepted.
// Otherwise, the subscribed will be null
if !community.local {
community_view.subscribed = data.follow;
}
Ok(CommunityResponse { community_view })
}
}
#[async_trait::async_trait(?Send)]
impl Perform for GetFollowedCommunities {
type Response = GetFollowedCommunitiesResponse;
async fn perform(
&self,
context: &Data<LemmyContext>,
_websocket_id: Option<ConnectionId>,
) -> Result<GetFollowedCommunitiesResponse, LemmyError> {
let data: &GetFollowedCommunities = &self;
let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
let person_id = local_user_view.person.id;
let communities = match blocking(context.pool(), move |conn| {
CommunityFollowerView::for_person(conn, person_id)
})
.await?
{
Ok(communities) => communities,
_ => return Err(ApiError::err("system_err_login").into()),
};
// Return the jwt
Ok(GetFollowedCommunitiesResponse { communities })
}
}
#[async_trait::async_trait(?Send)]
impl Perform for BanFromCommunity {
type Response = BanFromCommunityResponse;
async fn perform(
&self,
context: &Data<LemmyContext>,
websocket_id: Option<ConnectionId>,
) -> Result<BanFromCommunityResponse, LemmyError> {
let data: &BanFromCommunity = &self;
let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
let community_id = data.community_id;
let banned_person_id = data.person_id;
// Verify that only mods or admins can ban
is_mod_or_admin(context.pool(), local_user_view.person.id, community_id).await?;
let community_user_ban_form = CommunityPersonBanForm {
community_id: data.community_id,
person_id: data.person_id,
};
if data.ban {
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());
}
// 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: &'_ _| CommunityPersonBan::unban(conn, &community_user_ban_form);
if blocking(context.pool(), unban).await?.is_err() {
return Err(ApiError::err("community_user_already_banned").into());
}
}
// Remove/Restore their data if that's desired
if data.remove_data {
// Posts
blocking(context.pool(), move |conn: &'_ _| {
Post::update_removed_for_creator(conn, banned_person_id, Some(community_id), true)
})
.await??;
// Comments
// 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_person_id)
.community_id(community_id)
.limit(std::i64::MAX)
.list()
})
.await??;
for comment_view in &comments {
let comment_id = comment_view.comment.id;
blocking(context.pool(), move |conn: &'_ _| {
Comment::update_removed(conn, comment_id, true)
})
.await??;
}
}
// Mod tables
// TODO eventually do correct expires
let expires = match data.expires {
Some(time) => Some(naive_from_unix(time)),
None => None,
};
let form = ModBanFromCommunityForm {
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),
expires,
};
blocking(context.pool(), move |conn| {
ModBanFromCommunity::create(conn, &form)
})
.await??;
let person_id = data.person_id;
let person_view = blocking(context.pool(), move |conn| {
PersonViewSafe::read(conn, person_id)
})
.await??;
let res = BanFromCommunityResponse {
person_view,
banned: data.ban,
};
context.chat_server().do_send(SendCommunityRoomMessage {
op: UserOperation::BanFromCommunity,
response: res.clone(),
community_id,
websocket_id,
});
Ok(res)
}
}
#[async_trait::async_trait(?Send)]
impl Perform for AddModToCommunity {
type Response = AddModToCommunityResponse;
async fn perform(
&self,
context: &Data<LemmyContext>,
websocket_id: Option<ConnectionId>,
) -> Result<AddModToCommunityResponse, LemmyError> {
let data: &AddModToCommunity = &self;
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,
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(), 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());
}
} 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());
}
}
// Mod tables
let form = ModAddCommunityForm {
mod_person_id: local_user_view.person.id,
other_person_id: data.person_id,
community_id: data.community_id,
removed: Some(!data.added),
};
blocking(context.pool(), move |conn| {
ModAddCommunity::create(conn, &form)
})
.await??;
let community_id = data.community_id;
let moderators = blocking(context.pool(), move |conn| {
CommunityModeratorView::for_community(conn, community_id)
})
.await??;
let res = AddModToCommunityResponse { moderators };
context.chat_server().do_send(SendCommunityRoomMessage {
op: UserOperation::AddModToCommunity,
response: res.clone(),
community_id,
websocket_id,
});
Ok(res)
}
}
#[async_trait::async_trait(?Send)]
impl Perform for TransferCommunity {
type Response = GetCommunityResponse;
async fn perform(
&self,
context: &Data<LemmyContext>,
_websocket_id: Option<ConnectionId>,
) -> Result<GetCommunityResponse, LemmyError> {
let data: &TransferCommunity = &self;
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| {
Community::read(conn, community_id)
})
.await??;
let site_creator_id = blocking(context.pool(), move |conn| {
Site::read(conn, 1).map(|s| s.creator_id)
})
.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.person.id == site_creator_id)
.context(location_info!())?;
let creator_person = admins.remove(creator_index);
admins.insert(0, creator_person);
// Make sure user is the creator, or an admin
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());
}
let community_id = data.community_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());
};
// You also have to re-do the community_moderator table, reordering it.
let community_id = data.community_id;
let mut community_mods = blocking(context.pool(), move |conn| {
CommunityModeratorView::for_community(conn, community_id)
})
.await??;
let creator_index = community_mods
.iter()
.position(|r| r.moderator.id == data.person_id)
.context(location_info!())?;
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| {
CommunityModerator::delete_for_community(conn, community_id)
})
.await??;
// TODO: this should probably be a bulk operation
for cmod in &community_mods {
let community_moderator_form = CommunityModeratorForm {
community_id: cmod.community.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());
}
}
// Mod tables
let form = ModAddCommunityForm {
mod_person_id: local_user_view.person.id,
other_person_id: data.person_id,
community_id: data.community_id,
removed: Some(false),
};
blocking(context.pool(), move |conn| {
ModAddCommunity::create(conn, &form)
})
.await??;
let community_id = data.community_id;
let person_id = local_user_view.person.id;
let community_view = match blocking(context.pool(), move |conn| {
CommunityView::read(conn, community_id, Some(person_id))
})
.await?
{
Ok(community) => community,
Err(_e) => return Err(ApiError::err("couldnt_find_community").into()),
};
let community_id = data.community_id;
let moderators = match blocking(context.pool(), move |conn| {
CommunityModeratorView::for_community(conn, community_id)
})
.await?
{
Ok(moderators) => moderators,
Err(_e) => return Err(ApiError::err("couldnt_find_community").into()),
};
// Return the jwt
Ok(GetCommunityResponse {
community_view,
moderators,
online: 0,
})
}
}
fn send_community_websocket(
res: &CommunityResponse,
context: &Data<LemmyContext>,
websocket_id: Option<ConnectionId>,
op: UserOperation,
) {
// Strip out the person id and subscribed when sending to others
let mut res_sent = res.clone();
res_sent.community_view.subscribed = false;
context.chat_server().do_send(SendCommunityRoomMessage {
op,
response: res_sent,
community_id: res.community_view.community.id,
websocket_id,
});
}

586
crates/api/src/lib.rs Normal file
View file

@ -0,0 +1,586 @@
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_},
site::Site_,
},
Crud,
DbPool,
};
use lemmy_db_schema::{
source::{
community::{Community, CommunityModerator},
post::Post,
site::Site,
},
CommunityId,
LocalUserId,
PersonId,
PostId,
};
use lemmy_db_views::local_user_view::{LocalUserSettingsView, LocalUserView};
use lemmy_db_views_actor::{
community_person_ban_view::CommunityPersonBanView,
community_view::CommunityView,
};
use lemmy_utils::{
claims::Claims,
settings::structs::Settings,
ApiError,
ConnectionId,
LemmyError,
};
use lemmy_websocket::{serialize_websocket_message, LemmyContext, UserOperation};
use serde::Deserialize;
use std::{env, process::Command};
use url::Url;
pub mod comment;
pub mod community;
pub mod local_user;
pub mod post;
pub mod routes;
pub mod site;
pub mod websocket;
#[async_trait::async_trait(?Send)]
pub trait Perform {
type Response: serde::ser::Serialize + Send;
async fn perform(
&self,
context: &Data<LemmyContext>,
websocket_id: Option<ConnectionId>,
) -> Result<Self::Response, LemmyError>;
}
pub(crate) async fn is_mod_or_admin(
pool: &DbPool,
person_id: PersonId,
community_id: CommunityId,
) -> Result<(), LemmyError> {
let is_mod_or_admin = blocking(pool, move |conn| {
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 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()),
}
}
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()),
};
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 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)
}
/// 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<LocalUserView>, LemmyError> {
match jwt {
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(
person_id: PersonId,
community_id: CommunityId,
pool: &DbPool,
) -> Result<(), LemmyError> {
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())
} else {
Ok(())
}
}
pub(crate) async fn check_downvotes_enabled(score: i16, pool: &DbPool) -> Result<(), LemmyError> {
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());
}
}
Ok(())
}
/// Returns a list of communities that the user moderates
/// or if a community_id is supplied validates the user is a moderator
/// of that community and returns the community id in a vec
///
/// * `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(
person_id: PersonId,
community_id: Option<CommunityId>,
pool: &DbPool,
) -> 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, person_id, community_id).await?;
Ok(vec![community_id])
} else {
let ids = blocking(pool, move |conn: &'_ _| {
CommunityModerator::get_person_moderated_communities(conn, person_id)
})
.await??;
Ok(ids)
}
}
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??;
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>>()?;
if let Some(allowed) = allowed.as_ref() {
linked.extend_from_slice(allowed);
}
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(
context: LemmyContext,
id: ConnectionId,
op: UserOperation,
data: &str,
) -> Result<String, LemmyError> {
match op {
// User ops
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::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::BanPerson => do_websocket_operation::<BanPerson>(context, id, op, data).await,
UserOperation::GetPersonMentions => {
do_websocket_operation::<GetPersonMentions>(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
}
UserOperation::DeleteAccount => {
do_websocket_operation::<DeleteAccount>(context, id, op, data).await
}
UserOperation::PasswordReset => {
do_websocket_operation::<PasswordReset>(context, id, op, data).await
}
UserOperation::PasswordChange => {
do_websocket_operation::<PasswordChange>(context, id, op, data).await
}
UserOperation::UserJoin => do_websocket_operation::<UserJoin>(context, id, op, data).await,
UserOperation::PostJoin => do_websocket_operation::<PostJoin>(context, id, op, data).await,
UserOperation::CommunityJoin => {
do_websocket_operation::<CommunityJoin>(context, id, op, data).await
}
UserOperation::ModJoin => do_websocket_operation::<ModJoin>(context, id, op, data).await,
UserOperation::SaveUserSettings => {
do_websocket_operation::<SaveUserSettings>(context, id, op, data).await
}
UserOperation::GetReportCount => {
do_websocket_operation::<GetReportCount>(context, id, op, data).await
}
// Private Message ops
UserOperation::CreatePrivateMessage => {
do_websocket_operation::<CreatePrivateMessage>(context, id, op, data).await
}
UserOperation::EditPrivateMessage => {
do_websocket_operation::<EditPrivateMessage>(context, id, op, data).await
}
UserOperation::DeletePrivateMessage => {
do_websocket_operation::<DeletePrivateMessage>(context, id, op, data).await
}
UserOperation::MarkPrivateMessageAsRead => {
do_websocket_operation::<MarkPrivateMessageAsRead>(context, id, op, data).await
}
UserOperation::GetPrivateMessages => {
do_websocket_operation::<GetPrivateMessages>(context, id, op, data).await
}
// Site ops
UserOperation::GetModlog => do_websocket_operation::<GetModlog>(context, id, op, data).await,
UserOperation::CreateSite => do_websocket_operation::<CreateSite>(context, id, op, data).await,
UserOperation::EditSite => do_websocket_operation::<EditSite>(context, id, op, data).await,
UserOperation::GetSite => do_websocket_operation::<GetSite>(context, id, op, data).await,
UserOperation::GetSiteConfig => {
do_websocket_operation::<GetSiteConfig>(context, id, op, data).await
}
UserOperation::SaveSiteConfig => {
do_websocket_operation::<SaveSiteConfig>(context, id, op, data).await
}
UserOperation::Search => do_websocket_operation::<Search>(context, id, op, data).await,
UserOperation::TransferCommunity => {
do_websocket_operation::<TransferCommunity>(context, id, op, data).await
}
UserOperation::TransferSite => {
do_websocket_operation::<TransferSite>(context, id, op, data).await
}
// Community ops
UserOperation::GetCommunity => {
do_websocket_operation::<GetCommunity>(context, id, op, data).await
}
UserOperation::ListCommunities => {
do_websocket_operation::<ListCommunities>(context, id, op, data).await
}
UserOperation::CreateCommunity => {
do_websocket_operation::<CreateCommunity>(context, id, op, data).await
}
UserOperation::EditCommunity => {
do_websocket_operation::<EditCommunity>(context, id, op, data).await
}
UserOperation::DeleteCommunity => {
do_websocket_operation::<DeleteCommunity>(context, id, op, data).await
}
UserOperation::RemoveCommunity => {
do_websocket_operation::<RemoveCommunity>(context, id, op, data).await
}
UserOperation::FollowCommunity => {
do_websocket_operation::<FollowCommunity>(context, id, op, data).await
}
UserOperation::GetFollowedCommunities => {
do_websocket_operation::<GetFollowedCommunities>(context, id, op, data).await
}
UserOperation::BanFromCommunity => {
do_websocket_operation::<BanFromCommunity>(context, id, op, data).await
}
UserOperation::AddModToCommunity => {
do_websocket_operation::<AddModToCommunity>(context, id, op, data).await
}
// Post ops
UserOperation::CreatePost => do_websocket_operation::<CreatePost>(context, id, op, data).await,
UserOperation::GetPost => do_websocket_operation::<GetPost>(context, id, op, data).await,
UserOperation::GetPosts => do_websocket_operation::<GetPosts>(context, id, op, data).await,
UserOperation::EditPost => do_websocket_operation::<EditPost>(context, id, op, data).await,
UserOperation::DeletePost => do_websocket_operation::<DeletePost>(context, id, op, data).await,
UserOperation::RemovePost => do_websocket_operation::<RemovePost>(context, id, op, data).await,
UserOperation::LockPost => do_websocket_operation::<LockPost>(context, id, op, data).await,
UserOperation::StickyPost => do_websocket_operation::<StickyPost>(context, id, op, data).await,
UserOperation::CreatePostLike => {
do_websocket_operation::<CreatePostLike>(context, id, op, data).await
}
UserOperation::SavePost => do_websocket_operation::<SavePost>(context, id, op, data).await,
UserOperation::CreatePostReport => {
do_websocket_operation::<CreatePostReport>(context, id, op, data).await
}
UserOperation::ListPostReports => {
do_websocket_operation::<ListPostReports>(context, id, op, data).await
}
UserOperation::ResolvePostReport => {
do_websocket_operation::<ResolvePostReport>(context, id, op, data).await
}
// Comment ops
UserOperation::CreateComment => {
do_websocket_operation::<CreateComment>(context, id, op, data).await
}
UserOperation::EditComment => {
do_websocket_operation::<EditComment>(context, id, op, data).await
}
UserOperation::DeleteComment => {
do_websocket_operation::<DeleteComment>(context, id, op, data).await
}
UserOperation::RemoveComment => {
do_websocket_operation::<RemoveComment>(context, id, op, data).await
}
UserOperation::MarkCommentAsRead => {
do_websocket_operation::<MarkCommentAsRead>(context, id, op, data).await
}
UserOperation::SaveComment => {
do_websocket_operation::<SaveComment>(context, id, op, data).await
}
UserOperation::GetComments => {
do_websocket_operation::<GetComments>(context, id, op, data).await
}
UserOperation::CreateCommentLike => {
do_websocket_operation::<CreateCommentLike>(context, id, op, data).await
}
UserOperation::CreateCommentReport => {
do_websocket_operation::<CreateCommentReport>(context, id, op, data).await
}
UserOperation::ListCommentReports => {
do_websocket_operation::<ListCommentReports>(context, id, op, data).await
}
UserOperation::ResolveCommentReport => {
do_websocket_operation::<ResolveCommentReport>(context, id, op, data).await
}
}
}
async fn do_websocket_operation<'a, 'b, Data>(
context: LemmyContext,
id: ConnectionId,
op: UserOperation,
data: &str,
) -> Result<String, LemmyError>
where
for<'de> Data: Deserialize<'de> + 'a,
Data: Perform,
{
let parsed_data: Data = serde_json::from_str(&data)?;
let res = parsed_data
.perform(&web::Data::new(context), Some(id))
.await?;
serialize_websocket_message(&op, &res)
}
pub(crate) fn captcha_espeak_wav_base64(captcha: &str) -> Result<String, LemmyError> {
let mut built_text = String::new();
// Building proper speech text for espeak
for mut c in captcha.chars() {
let new_str = if c.is_alphabetic() {
if c.is_lowercase() {
c.make_ascii_uppercase();
format!("lower case {} ... ", c)
} else {
c.make_ascii_uppercase();
format!("capital {} ... ", c)
}
} else {
format!("{} ...", c)
};
built_text.push_str(&new_str);
}
espeak_wav_base64(&built_text)
}
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!(
"{}/lemmy_espeak_{}.wav",
env::temp_dir().to_string_lossy(),
&uuid
);
// Write the wav file
Command::new("espeak")
.arg("-w")
.arg(&file_path)
.arg(text)
.status()?;
// Read the wav file bytes
let bytes = std::fs::read(&file_path)?;
// Delete the file
std::fs::remove_file(file_path)?;
// Convert to base64
let base64 = base64::encode(bytes);
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, 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() {
assert!(captcha_espeak_wav_base64("WxRt2l").is_ok())
}
}

1478
crates/api/src/local_user.rs Normal file

File diff suppressed because it is too large Load diff

954
crates/api/src/post.rs Normal file
View file

@ -0,0 +1,954 @@
use crate::{
check_community_ban,
check_downvotes_enabled,
collect_moderated_communities,
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_api_structs::{blocking, post::*};
use lemmy_apub::{generate_apub_endpoint, ApubLikeableType, ApubObjectType, EndpointType};
use lemmy_db_queries::{
source::post::Post_,
Crud,
Likeable,
ListingType,
Reportable,
Saveable,
SortType,
};
use lemmy_db_schema::{
naive_now,
source::{
moderator::*,
post::*,
post_report::{PostReport, PostReportForm},
},
};
use lemmy_db_views::{
comment_view::CommentQueryBuilder,
post_report_view::{PostReportQueryBuilder, PostReportView},
post_view::{PostQueryBuilder, PostView},
};
use lemmy_db_views_actor::{
community_moderator_view::CommunityModeratorView,
community_view::CommunityView,
};
use lemmy_utils::{
request::fetch_iframely_and_pictrs_data,
utils::{check_slurs, check_slurs_opt, is_valid_post_title},
ApiError,
ConnectionId,
LemmyError,
};
use lemmy_websocket::{
messages::{GetPostUsersOnline, SendModRoomMessage, SendPost, SendUserRoomMessage},
LemmyContext,
UserOperation,
};
use std::str::FromStr;
#[async_trait::async_trait(?Send)]
impl Perform for CreatePost {
type Response = PostResponse;
async fn perform(
&self,
context: &Data<LemmyContext>,
websocket_id: Option<ConnectionId>,
) -> Result<PostResponse, LemmyError> {
let data: &CreatePost = &self;
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());
}
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).await;
let post_form = PostForm {
name: data.name.trim().to_owned(),
url: data_url.map(|u| u.to_owned().into()),
body: data.body.to_owned(),
community_id: data.community_id,
creator_id: local_user_view.person.id,
removed: None,
deleted: None,
nsfw: data.nsfw,
locked: None,
stickied: None,
updated: None,
embed_title: iframely_title,
embed_description: iframely_description,
embed_html: iframely_html,
thumbnail_url: pictrs_thumbnail.map(|u| u.into()),
ap_id: None,
local: true,
published: None,
};
let inserted_post =
match blocking(context.pool(), move |conn| Post::create(conn, &post_form)).await? {
Ok(post) => post,
Err(e) => {
let err_type = if e.to_string() == "value too long for type character varying(200)" {
"post_title_too_long"
} else {
"couldnt_create_post"
};
return Err(ApiError::err(err_type).into());
}
};
let inserted_post_id = inserted_post.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()),
};
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,
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());
}
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(local_user_view.person.id))
})
.await?
{
Ok(post) => post,
Err(_e) => return Err(ApiError::err("couldnt_find_post").into()),
};
let res = PostResponse { post_view };
context.chat_server().do_send(SendPost {
op: UserOperation::CreatePost,
post: res.clone(),
websocket_id,
});
Ok(res)
}
}
#[async_trait::async_trait(?Send)]
impl Perform for GetPost {
type Response = GetPostResponse;
async fn perform(
&self,
context: &Data<LemmyContext>,
_websocket_id: Option<ConnectionId>,
) -> Result<GetPostResponse, LemmyError> {
let data: &GetPost = &self;
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, person_id)
})
.await?
{
Ok(post) => post,
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_person_id(person_id)
.post_id(id)
.limit(9999)
.list()
})
.await??;
let community_id = post_view.community.id;
let moderators = blocking(context.pool(), move |conn| {
CommunityModeratorView::for_community(conn, community_id)
})
.await??;
// Necessary for the sidebar
let community_view = match blocking(context.pool(), move |conn| {
CommunityView::read(conn, community_id, person_id)
})
.await?
{
Ok(community) => community,
Err(_e) => return Err(ApiError::err("couldnt_find_community").into()),
};
let online = context
.chat_server()
.send(GetPostUsersOnline { post_id: data.id })
.await
.unwrap_or(1);
// Return the jwt
Ok(GetPostResponse {
post_view,
community_view,
comments,
moderators,
online,
})
}
}
#[async_trait::async_trait(?Send)]
impl Perform for GetPosts {
type Response = GetPostsResponse;
async fn perform(
&self,
context: &Data<LemmyContext>,
_websocket_id: Option<ConnectionId>,
) -> Result<GetPostsResponse, LemmyError> {
let data: &GetPosts = &self;
let local_user_view = get_local_user_view_from_jwt_opt(&data.auth, context.pool()).await?;
let person_id = match &local_user_view {
Some(uv) => Some(uv.person.id),
None => None,
};
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 community_id = data.community_id;
let community_name = data.community_name.to_owned();
let posts = match blocking(context.pool(), move |conn| {
PostQueryBuilder::create(conn)
.listing_type(&type_)
.sort(&sort)
.show_nsfw(show_nsfw)
.community_id(community_id)
.community_name(community_name)
.my_person_id(person_id)
.page(page)
.limit(limit)
.list()
})
.await?
{
Ok(posts) => posts,
Err(_e) => return Err(ApiError::err("couldnt_get_posts").into()),
};
Ok(GetPostsResponse { posts })
}
}
#[async_trait::async_trait(?Send)]
impl Perform for CreatePostLike {
type Response = PostResponse;
async fn perform(
&self,
context: &Data<LemmyContext>,
websocket_id: Option<ConnectionId>,
) -> Result<PostResponse, LemmyError> {
let data: &CreatePostLike = &self;
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?;
// Check for a community ban
let post_id = data.post_id;
let post = blocking(context.pool(), move |conn| Post::read(conn, post_id)).await??;
check_community_ban(local_user_view.person.id, post.community_id, context.pool()).await?;
let like_form = PostLikeForm {
post_id: data.post_id,
person_id: local_user_view.person.id,
score: data.score,
};
// Remove any likes first
let person_id = local_user_view.person.id;
blocking(context.pool(), move |conn| {
PostLike::remove(conn, person_id, post_id)
})
.await??;
// Only add the like if the score isnt 0
let do_add = like_form.score != 0 && (like_form.score == 1 || like_form.score == -1);
if do_add {
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());
}
if like_form.score == 1 {
post.send_like(&local_user_view.person, context).await?;
} else if like_form.score == -1 {
post.send_dislike(&local_user_view.person, context).await?;
}
} else {
post
.send_undo_like(&local_user_view.person, context)
.await?;
}
let post_id = data.post_id;
let person_id = local_user_view.person.id;
let post_view = match blocking(context.pool(), move |conn| {
PostView::read(conn, post_id, Some(person_id))
})
.await?
{
Ok(post) => post,
Err(_e) => return Err(ApiError::err("couldnt_find_post").into()),
};
let res = PostResponse { post_view };
context.chat_server().do_send(SendPost {
op: UserOperation::CreatePostLike,
post: res.clone(),
websocket_id,
});
Ok(res)
}
}
#[async_trait::async_trait(?Send)]
impl Perform for EditPost {
type Response = PostResponse;
async fn perform(
&self,
context: &Data<LemmyContext>,
websocket_id: Option<ConnectionId>,
) -> Result<PostResponse, LemmyError> {
let data: &EditPost = &self;
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());
}
let post_id = data.post_id;
let orig_post = blocking(context.pool(), move |conn| Post::read(conn, post_id)).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(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).await;
let post_form = PostForm {
name: data.name.trim().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(),
community_id: orig_post.community_id,
removed: Some(orig_post.removed),
deleted: Some(orig_post.deleted),
locked: Some(orig_post.locked),
stickied: Some(orig_post.stickied),
updated: Some(naive_now()),
embed_title: iframely_title,
embed_description: iframely_description,
embed_html: iframely_html,
thumbnail_url: pictrs_thumbnail.map(|u| u.into()),
ap_id: Some(orig_post.ap_id),
local: orig_post.local,
published: None,
};
let post_id = data.post_id;
let res = blocking(context.pool(), move |conn| {
Post::update(conn, post_id, &post_form)
})
.await?;
let updated_post: Post = match res {
Ok(post) => post,
Err(e) => {
let err_type = if e.to_string() == "value too long for type character varying(200)" {
"post_title_too_long"
} else {
"couldnt_update_post"
};
return Err(ApiError::err(err_type).into());
}
};
// Send apub update
updated_post
.send_update(&local_user_view.person, context)
.await?;
let post_id = data.post_id;
let post_view = blocking(context.pool(), move |conn| {
PostView::read(conn, post_id, Some(local_user_view.person.id))
})
.await??;
let res = PostResponse { post_view };
context.chat_server().do_send(SendPost {
op: UserOperation::EditPost,
post: res.clone(),
websocket_id,
});
Ok(res)
}
}
#[async_trait::async_trait(?Send)]
impl Perform for DeletePost {
type Response = PostResponse;
async fn perform(
&self,
context: &Data<LemmyContext>,
websocket_id: Option<ConnectionId>,
) -> Result<PostResponse, LemmyError> {
let data: &DeletePost = &self;
let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
let post_id = data.post_id;
let orig_post = blocking(context.pool(), move |conn| Post::read(conn, post_id)).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(local_user_view.person.id, orig_post.creator_id) {
return Err(ApiError::err("no_post_edit_allowed").into());
}
// Update the post
let post_id = data.post_id;
let deleted = data.deleted;
let updated_post = blocking(context.pool(), move |conn| {
Post::update_deleted(conn, post_id, deleted)
})
.await??;
// apub updates
if deleted {
updated_post
.send_delete(&local_user_view.person, context)
.await?;
} else {
updated_post
.send_undo_delete(&local_user_view.person, context)
.await?;
}
// Refetch the post
let post_id = data.post_id;
let post_view = blocking(context.pool(), move |conn| {
PostView::read(conn, post_id, Some(local_user_view.person.id))
})
.await??;
let res = PostResponse { post_view };
context.chat_server().do_send(SendPost {
op: UserOperation::DeletePost,
post: res.clone(),
websocket_id,
});
Ok(res)
}
}
#[async_trait::async_trait(?Send)]
impl Perform for RemovePost {
type Response = PostResponse;
async fn perform(
&self,
context: &Data<LemmyContext>,
websocket_id: Option<ConnectionId>,
) -> Result<PostResponse, LemmyError> {
let data: &RemovePost = &self;
let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
let post_id = data.post_id;
let orig_post = blocking(context.pool(), move |conn| Post::read(conn, post_id)).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(),
local_user_view.person.id,
orig_post.community_id,
)
.await?;
// Update the post
let post_id = data.post_id;
let removed = data.removed;
let updated_post = blocking(context.pool(), move |conn| {
Post::update_removed(conn, post_id, removed)
})
.await??;
// Mod tables
let form = ModRemovePostForm {
mod_person_id: local_user_view.person.id,
post_id: data.post_id,
removed: Some(removed),
reason: data.reason.to_owned(),
};
blocking(context.pool(), move |conn| {
ModRemovePost::create(conn, &form)
})
.await??;
// apub updates
if removed {
updated_post
.send_remove(&local_user_view.person, context)
.await?;
} else {
updated_post
.send_undo_remove(&local_user_view.person, context)
.await?;
}
// Refetch the post
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, post_id, Some(person_id))
})
.await??;
let res = PostResponse { post_view };
context.chat_server().do_send(SendPost {
op: UserOperation::RemovePost,
post: res.clone(),
websocket_id,
});
Ok(res)
}
}
#[async_trait::async_trait(?Send)]
impl Perform for LockPost {
type Response = PostResponse;
async fn perform(
&self,
context: &Data<LemmyContext>,
websocket_id: Option<ConnectionId>,
) -> Result<PostResponse, LemmyError> {
let data: &LockPost = &self;
let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
let post_id = data.post_id;
let orig_post = blocking(context.pool(), move |conn| Post::read(conn, post_id)).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(),
local_user_view.person.id,
orig_post.community_id,
)
.await?;
// Update the post
let post_id = data.post_id;
let locked = data.locked;
let updated_post = blocking(context.pool(), move |conn| {
Post::update_locked(conn, post_id, locked)
})
.await??;
// Mod tables
let form = ModLockPostForm {
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(&local_user_view.person, context)
.await?;
// Refetch the post
let post_id = data.post_id;
let post_view = blocking(context.pool(), move |conn| {
PostView::read(conn, post_id, Some(local_user_view.person.id))
})
.await??;
let res = PostResponse { post_view };
context.chat_server().do_send(SendPost {
op: UserOperation::LockPost,
post: res.clone(),
websocket_id,
});
Ok(res)
}
}
#[async_trait::async_trait(?Send)]
impl Perform for StickyPost {
type Response = PostResponse;
async fn perform(
&self,
context: &Data<LemmyContext>,
websocket_id: Option<ConnectionId>,
) -> Result<PostResponse, LemmyError> {
let data: &StickyPost = &self;
let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
let post_id = data.post_id;
let orig_post = blocking(context.pool(), move |conn| Post::read(conn, post_id)).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(),
local_user_view.person.id,
orig_post.community_id,
)
.await?;
// Update the post
let post_id = data.post_id;
let stickied = data.stickied;
let updated_post = blocking(context.pool(), move |conn| {
Post::update_stickied(conn, post_id, stickied)
})
.await??;
// Mod tables
let form = ModStickyPostForm {
mod_person_id: local_user_view.person.id,
post_id: data.post_id,
stickied: Some(stickied),
};
blocking(context.pool(), move |conn| {
ModStickyPost::create(conn, &form)
})
.await??;
// Apub updates
// TODO stickied should pry work like locked for ease of use
updated_post
.send_update(&local_user_view.person, context)
.await?;
// Refetch the post
let post_id = data.post_id;
let post_view = blocking(context.pool(), move |conn| {
PostView::read(conn, post_id, Some(local_user_view.person.id))
})
.await??;
let res = PostResponse { post_view };
context.chat_server().do_send(SendPost {
op: UserOperation::StickyPost,
post: res.clone(),
websocket_id,
});
Ok(res)
}
}
#[async_trait::async_trait(?Send)]
impl Perform for SavePost {
type Response = PostResponse;
async fn perform(
&self,
context: &Data<LemmyContext>,
_websocket_id: Option<ConnectionId>,
) -> Result<PostResponse, LemmyError> {
let data: &SavePost = &self;
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,
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());
}
} 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());
}
}
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, post_id, Some(person_id))
})
.await??;
Ok(PostResponse { post_view })
}
}
/// Creates a post report and notifies the moderators of the community
#[async_trait::async_trait(?Send)]
impl Perform for CreatePostReport {
type Response = CreatePostReportResponse;
async fn perform(
&self,
context: &Data<LemmyContext>,
websocket_id: Option<ConnectionId>,
) -> Result<CreatePostReportResponse, LemmyError> {
let data: &CreatePostReport = &self;
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());
}
if reason.chars().count() > 1000 {
return Err(ApiError::err("report_too_long").into());
}
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(person_id, post_view.community.id, context.pool()).await?;
let report_form = PostReportForm {
creator_id: person_id,
post_id,
original_post_name: post_view.post.name,
original_post_url: post_view.post.url,
original_post_body: post_view.post.body,
reason: data.reason.to_owned(),
};
let report = match blocking(context.pool(), move |conn| {
PostReport::report(conn, &report_form)
})
.await?
{
Ok(report) => report,
Err(_e) => return Err(ApiError::err("couldnt_create_report").into()),
};
let res = CreatePostReportResponse { success: true };
context.chat_server().do_send(SendUserRoomMessage {
op: UserOperation::CreatePostReport,
response: res.clone(),
local_recipient_id: local_user_view.local_user.id,
websocket_id,
});
context.chat_server().do_send(SendModRoomMessage {
op: UserOperation::CreatePostReport,
response: report,
community_id: post_view.community.id,
websocket_id,
});
Ok(res)
}
}
/// Resolves or unresolves a post report and notifies the moderators of the community
#[async_trait::async_trait(?Send)]
impl Perform for ResolvePostReport {
type Response = ResolvePostReportResponse;
async fn perform(
&self,
context: &Data<LemmyContext>,
websocket_id: Option<ConnectionId>,
) -> Result<ResolvePostReportResponse, LemmyError> {
let data: &ResolvePostReport = &self;
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| {
PostReportView::read(&conn, report_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, person_id)
} else {
PostReport::unresolve(conn, report_id, person_id)
}
};
let res = ResolvePostReportResponse {
report_id,
resolved: true,
};
if blocking(context.pool(), resolve_fun).await?.is_err() {
return Err(ApiError::err("couldnt_resolve_report").into());
};
context.chat_server().do_send(SendModRoomMessage {
op: UserOperation::ResolvePostReport,
response: res.clone(),
community_id: report.community.id,
websocket_id,
});
Ok(res)
}
}
/// Lists post reports for a community if an id is supplied
/// or returns all post reports for communities a user moderates
#[async_trait::async_trait(?Send)]
impl Perform for ListPostReports {
type Response = ListPostReportsResponse;
async fn perform(
&self,
context: &Data<LemmyContext>,
websocket_id: Option<ConnectionId>,
) -> Result<ListPostReportsResponse, LemmyError> {
let data: &ListPostReports = &self;
let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
let person_id = local_user_view.person.id;
let community_id = data.community;
let community_ids =
collect_moderated_communities(person_id, community_id, context.pool()).await?;
let page = data.page;
let limit = data.limit;
let posts = blocking(context.pool(), move |conn| {
PostReportQueryBuilder::create(conn)
.community_ids(community_ids)
.page(page)
.limit(limit)
.list()
})
.await??;
let res = ListPostReportsResponse { posts };
context.chat_server().do_send(SendUserRoomMessage {
op: UserOperation::ListPostReports,
response: res.clone(),
local_recipient_id: local_user_view.local_user.id,
websocket_id,
});
Ok(res)
}
}

View file

@ -1,17 +1,15 @@
use crate::{
api::{comment::*, community::*, post::*, site::*, user::*, Perform},
rate_limit::RateLimit,
routes::{ChatServerParam, DbPoolParam},
websocket::WebsocketInfo,
};
use actix_web::{client::Client, error::ErrorBadRequest, *};
use serde::Serialize;
use crate::Perform;
use actix_web::{error::ErrorBadRequest, *};
use lemmy_api_structs::{comment::*, community::*, person::*, post::*, site::*, websocket::*};
use lemmy_utils::rate_limit::RateLimit;
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/v1")
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")
@ -24,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())
@ -58,7 +51,9 @@ pub fn config(cfg: &mut web::ServiceConfig, rate_limit: &RateLimit) {
.route("/remove", web::post().to(route_post::<RemoveCommunity>))
.route("/transfer", web::post().to(route_post::<TransferCommunity>))
.route("/ban_user", web::post().to(route_post::<BanFromCommunity>))
.route("/mod", web::post().to(route_post::<AddModToCommunity>)),
.route("/mod", web::post().to(route_post::<AddModToCommunity>))
.route("/join", web::post().to(route_post::<CommunityJoin>))
.route("/mod/join", web::post().to(route_post::<ModJoin>)),
)
// Post
.service(
@ -79,7 +74,14 @@ pub fn config(cfg: &mut web::ServiceConfig, rate_limit: &RateLimit) {
.route("/sticky", web::post().to(route_post::<StickyPost>))
.route("/list", web::get().to(route_get::<GetPosts>))
.route("/like", web::post().to(route_post::<CreatePostLike>))
.route("/save", web::put().to(route_post::<SavePost>)),
.route("/save", web::put().to(route_post::<SavePost>))
.route("/join", web::post().to(route_post::<PostJoin>))
.route("/report", web::post().to(route_post::<CreatePostReport>))
.route(
"/report/resolve",
web::put().to(route_post::<ResolvePostReport>),
)
.route("/report/list", web::get().to(route_get::<ListPostReports>)),
)
// Comment
.service(
@ -94,7 +96,17 @@ pub fn config(cfg: &mut web::ServiceConfig, rate_limit: &RateLimit) {
web::post().to(route_post::<MarkCommentAsRead>),
)
.route("/like", web::post().to(route_post::<CreateCommentLike>))
.route("/save", web::put().to(route_post::<SaveComment>)),
.route("/save", web::put().to(route_post::<SaveComment>))
.route("/list", web::get().to(route_get::<GetComments>))
.route("/report", web::post().to(route_post::<CreateCommentReport>))
.route(
"/report/resolve",
web::put().to(route_post::<ResolveCommentReport>),
)
.route(
"/report/list",
web::get().to(route_get::<ListCommentReports>),
),
)
// Private Message
.service(
@ -125,22 +137,23 @@ 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(
"/followed_communities",
web::get().to(route_get::<GetFollowedCommunities>),
)
.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_post::<GetCaptcha>))
.route("/get_captcha", web::get().to(route_get::<GetCaptcha>))
.route(
"/delete_account",
web::post().to(route_post::<DeleteAccount>),
@ -161,7 +174,8 @@ pub fn config(cfg: &mut web::ServiceConfig, rate_limit: &RateLimit) {
.route(
"/save_user_settings",
web::put().to(route_post::<SaveUserSettings>),
),
)
.route("/report_count", web::get().to(route_get::<GetReportCount>)),
)
// Admin Actions
.service(
@ -174,47 +188,36 @@ pub fn config(cfg: &mut web::ServiceConfig, rate_limit: &RateLimit) {
async fn perform<Request>(
data: Request,
client: &Client,
db: DbPoolParam,
chat_server: ChatServerParam,
context: web::Data<LemmyContext>,
) -> Result<HttpResponse, Error>
where
Request: Perform,
Request: Send + 'static,
{
let ws_info = WebsocketInfo {
chatserver: chat_server.get_ref().to_owned(),
id: None,
};
let res = data
.perform(&db, Some(ws_info), client.clone())
.perform(&context, None)
.await
.map(|json| HttpResponse::Ok().json(json))
.map_err(ErrorBadRequest)?;
Ok(res)
}
async fn route_get<Data>(
async fn route_get<'a, Data>(
data: web::Query<Data>,
client: web::Data<Client>,
db: DbPoolParam,
chat_server: ChatServerParam,
context: web::Data<LemmyContext>,
) -> Result<HttpResponse, Error>
where
Data: Serialize + Send + 'static + Perform,
Data: Deserialize<'a> + Send + 'static + Perform,
{
perform::<Data>(data.0, &client, db, chat_server).await
perform::<Data>(data.0, context).await
}
async fn route_post<Data>(
async fn route_post<'a, Data>(
data: web::Json<Data>,
client: web::Data<Client>,
db: DbPoolParam,
chat_server: ChatServerParam,
context: web::Data<LemmyContext>,
) -> Result<HttpResponse, Error>
where
Data: Serialize + Send + 'static + Perform,
Data: Deserialize<'a> + Send + 'static + Perform,
{
perform::<Data>(data.0, &client, db, chat_server).await
perform::<Data>(data.0, context).await
}

598
crates/api/src/site.rs Normal file
View file

@ -0,0 +1,598 @@
use crate::{
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,
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_to_url,
source::site::Site_,
Crud,
SearchType,
SortType,
};
use lemmy_db_schema::{
naive_now,
source::{
moderator::*,
site::{Site, *},
},
};
use lemmy_db_views::{
comment_view::CommentQueryBuilder,
post_view::PostQueryBuilder,
site_view::SiteView,
};
use lemmy_db_views_actor::{
community_view::CommunityQueryBuilder,
person_view::{PersonQueryBuilder, PersonViewSafe},
};
use lemmy_db_views_moderator::{
mod_add_community_view::ModAddCommunityView,
mod_add_view::ModAddView,
mod_ban_from_community_view::ModBanFromCommunityView,
mod_ban_view::ModBanView,
mod_lock_post_view::ModLockPostView,
mod_remove_comment_view::ModRemoveCommentView,
mod_remove_community_view::ModRemoveCommunityView,
mod_remove_post_view::ModRemovePostView,
mod_sticky_post_view::ModStickyPostView,
};
use lemmy_utils::{
location_info,
settings::structs::Settings,
utils::{check_slurs, check_slurs_opt},
version,
ApiError,
ConnectionId,
LemmyError,
};
use lemmy_websocket::{
messages::{GetUsersOnline, SendAllMessage},
LemmyContext,
UserOperation,
};
use log::{debug, info};
use std::str::FromStr;
#[async_trait::async_trait(?Send)]
impl Perform for GetModlog {
type Response = GetModlogResponse;
async fn perform(
&self,
context: &Data<LemmyContext>,
_websocket_id: Option<ConnectionId>,
) -> Result<GetModlogResponse, LemmyError> {
let data: &GetModlog = &self;
let community_id = data.community_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_person_id, page, limit)
})
.await??;
let locked_posts = blocking(context.pool(), move |conn| {
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_person_id, page, limit)
})
.await??;
let removed_comments = blocking(context.pool(), move |conn| {
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_person_id, page, limit)
})
.await??;
let added_to_community = blocking(context.pool(), move |conn| {
ModAddCommunityView::list(conn, community_id, mod_person_id, page, limit)
})
.await??;
// These arrays are only for the full modlog, when a community isn't given
let (removed_communities, banned, added) = if data.community_id.is_none() {
blocking(context.pool(), move |conn| {
Ok((
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??
} else {
(Vec::new(), Vec::new(), Vec::new())
};
// Return the jwt
Ok(GetModlogResponse {
removed_posts,
locked_posts,
stickied_posts,
removed_comments,
removed_communities,
banned_from_community,
banned,
added_to_community,
added,
})
}
}
#[async_trait::async_trait(?Send)]
impl Perform for CreateSite {
type Response = SiteResponse;
async fn perform(
&self,
context: &Data<LemmyContext>,
_websocket_id: Option<ConnectionId>,
) -> Result<SiteResponse, LemmyError> {
let data: &CreateSite = &self;
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());
};
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(&local_user_view)?;
let site_form = SiteForm {
name: data.name.to_owned(),
description: data.description.to_owned(),
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,
updated: None,
};
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());
}
let site_view = blocking(context.pool(), move |conn| SiteView::read(conn)).await??;
Ok(SiteResponse { site_view })
}
}
#[async_trait::async_trait(?Send)]
impl Perform for EditSite {
type Response = SiteResponse;
async fn perform(
&self,
context: &Data<LemmyContext>,
websocket_id: Option<ConnectionId>,
) -> Result<SiteResponse, LemmyError> {
let data: &EditSite = &self;
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(&local_user_view)?;
let found_site = blocking(context.pool(), move |conn| Site::read_simple(conn)).await??;
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(),
description: data.description.to_owned(),
icon,
banner,
creator_id: found_site.creator_id,
updated: Some(naive_now()),
enable_downvotes: data.enable_downvotes,
open_registration: data.open_registration,
enable_nsfw: data.enable_nsfw,
};
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());
}
let site_view = blocking(context.pool(), move |conn| SiteView::read(conn)).await??;
let res = SiteResponse { site_view };
context.chat_server().do_send(SendAllMessage {
op: UserOperation::EditSite,
response: res.clone(),
websocket_id,
});
Ok(res)
}
}
#[async_trait::async_trait(?Send)]
impl Perform for GetSite {
type Response = GetSiteResponse;
async fn perform(
&self,
context: &Data<LemmyContext>,
websocket_id: Option<ConnectionId>,
) -> Result<GetSiteResponse, LemmyError> {
let data: &GetSite = &self;
let site_view = match blocking(context.pool(), move |conn| SiteView::read(conn)).await? {
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() {
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(),
show_nsfw: true,
captcha_uuid: None,
captcha_answer: None,
};
let login_response = register.perform(context, websocket_id).await?;
info!("Admin {} created", setup.admin_username);
let create_site = CreateSite {
name: setup.site_name.to_owned(),
description: None,
icon: None,
banner: None,
enable_downvotes: true,
open_registration: true,
enable_nsfw: true,
auth: login_response.jwt,
};
create_site.perform(context, websocket_id).await?;
info!("Site {} created", setup.site_name);
Some(blocking(context.pool(), move |conn| SiteView::read(conn)).await??)
} else {
None
}
}
};
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.person.id == site_creator_id) {
let creator_person = admins.remove(creator_index);
admins.insert(0, creator_person);
}
}
let banned = blocking(context.pool(), move |conn| PersonViewSafe::banned(conn)).await??;
let online = context
.chat_server()
.send(GetUsersOnline)
.await
.unwrap_or(1);
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,
admins,
banned,
online,
version: version::VERSION.to_string(),
my_user,
federated_instances,
})
}
}
#[async_trait::async_trait(?Send)]
impl Perform for Search {
type Response = SearchResponse;
async fn perform(
&self,
context: &Data<LemmyContext>,
_websocket_id: Option<ConnectionId>,
) -> Result<SearchResponse, LemmyError> {
let data: &Search = &self;
match search_by_apub_id(&data.q, context).await {
Ok(r) => return Ok(r),
Err(e) => debug!("Failed to resolve search query as activitypub ID: {}", e),
}
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_)?;
let mut posts = Vec::new();
let mut comments = Vec::new();
let mut communities = Vec::new();
let mut users = Vec::new();
// TODO no clean / non-nsfw searching rn
let q = data.q.to_owned();
let page = data.page;
let limit = data.limit;
let sort = SortType::from_str(&data.sort)?;
let community_id = data.community_id;
let community_name = data.community_name.to_owned();
match type_ {
SearchType::Posts => {
posts = blocking(context.pool(), move |conn| {
PostQueryBuilder::create(conn)
.sort(&sort)
.show_nsfw(true)
.community_id(community_id)
.community_name(community_name)
.my_person_id(person_id)
.search_term(q)
.page(page)
.limit(limit)
.list()
})
.await??;
}
SearchType::Comments => {
comments = blocking(context.pool(), move |conn| {
CommentQueryBuilder::create(&conn)
.sort(&sort)
.search_term(q)
.my_person_id(person_id)
.page(page)
.limit(limit)
.list()
})
.await??;
}
SearchType::Communities => {
communities = blocking(context.pool(), move |conn| {
CommunityQueryBuilder::create(conn)
.sort(&sort)
.search_term(q)
.my_person_id(person_id)
.page(page)
.limit(limit)
.list()
})
.await??;
}
SearchType::Users => {
users = blocking(context.pool(), move |conn| {
PersonQueryBuilder::create(conn)
.sort(&sort)
.search_term(q)
.page(page)
.limit(limit)
.list()
})
.await??;
}
SearchType::All => {
posts = blocking(context.pool(), move |conn| {
PostQueryBuilder::create(conn)
.sort(&sort)
.show_nsfw(true)
.community_id(community_id)
.community_name(community_name)
.my_person_id(person_id)
.search_term(q)
.page(page)
.limit(limit)
.list()
})
.await??;
let q = data.q.to_owned();
let sort = SortType::from_str(&data.sort)?;
comments = blocking(context.pool(), move |conn| {
CommentQueryBuilder::create(conn)
.sort(&sort)
.search_term(q)
.my_person_id(person_id)
.page(page)
.limit(limit)
.list()
})
.await??;
let q = data.q.to_owned();
let sort = SortType::from_str(&data.sort)?;
communities = blocking(context.pool(), move |conn| {
CommunityQueryBuilder::create(conn)
.sort(&sort)
.search_term(q)
.my_person_id(person_id)
.page(page)
.limit(limit)
.list()
})
.await??;
let q = data.q.to_owned();
let sort = SortType::from_str(&data.sort)?;
users = blocking(context.pool(), move |conn| {
PersonQueryBuilder::create(conn)
.sort(&sort)
.search_term(q)
.page(page)
.limit(limit)
.list()
})
.await??;
}
SearchType::Url => {
posts = blocking(context.pool(), move |conn| {
PostQueryBuilder::create(conn)
.sort(&sort)
.show_nsfw(true)
.my_person_id(person_id)
.community_id(community_id)
.community_name(community_name)
.url_search(q)
.page(page)
.limit(limit)
.list()
})
.await??;
}
};
// Return the jwt
Ok(SearchResponse {
type_: data.type_.to_owned(),
comments,
posts,
communities,
users,
})
}
}
#[async_trait::async_trait(?Send)]
impl Perform for TransferSite {
type Response = GetSiteResponse;
async fn perform(
&self,
context: &Data<LemmyContext>,
_websocket_id: Option<ConnectionId>,
) -> Result<GetSiteResponse, LemmyError> {
let data: &TransferSite = &self;
let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
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 != local_user_view.person.id {
return Err(ApiError::err("not_an_admin").into());
}
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());
};
// Mod tables
let form = ModAddForm {
mod_person_id: local_user_view.person.id,
other_person_id: data.person_id,
removed: Some(false),
};
blocking(context.pool(), move |conn| ModAdd::create(conn, &form)).await??;
let site_view = blocking(context.pool(), move |conn| SiteView::read(conn)).await??;
let mut admins = blocking(context.pool(), move |conn| PersonViewSafe::admins(conn)).await??;
let creator_index = admins
.iter()
.position(|r| r.person.id == site_view.creator.id)
.context(location_info!())?;
let creator_person = admins.remove(creator_index);
admins.insert(0, creator_person);
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),
admins,
banned,
online: 0,
version: version::VERSION.to_string(),
my_user,
federated_instances,
})
}
}
#[async_trait::async_trait(?Send)]
impl Perform for GetSiteConfig {
type Response = GetSiteConfigResponse;
async fn perform(
&self,
context: &Data<LemmyContext>,
_websocket_id: Option<ConnectionId>,
) -> Result<GetSiteConfigResponse, LemmyError> {
let data: &GetSiteConfig = &self;
let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
// Only let admins read this
is_admin(&local_user_view)?;
let config_hjson = Settings::read_config_file()?;
Ok(GetSiteConfigResponse { config_hjson })
}
}
#[async_trait::async_trait(?Send)]
impl Perform for SaveSiteConfig {
type Response = GetSiteConfigResponse;
async fn perform(
&self,
context: &Data<LemmyContext>,
_websocket_id: Option<ConnectionId>,
) -> Result<GetSiteConfigResponse, LemmyError> {
let data: &SaveSiteConfig = &self;
let local_user_view = get_local_user_view_from_jwt(&data.auth, context.pool()).await?;
// Only let admins read this
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()),
};
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

@ -0,0 +1,119 @@
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<CommentId>,
pub post_id: PostId,
pub form_id: Option<String>,
pub auth: String,
}
#[derive(Deserialize)]
pub struct EditComment {
pub content: String,
pub comment_id: CommentId,
pub form_id: Option<String>,
pub auth: String,
}
#[derive(Deserialize)]
pub struct DeleteComment {
pub comment_id: CommentId,
pub deleted: bool,
pub auth: String,
}
#[derive(Deserialize)]
pub struct RemoveComment {
pub comment_id: CommentId,
pub removed: bool,
pub reason: Option<String>,
pub auth: String,
}
#[derive(Deserialize)]
pub struct MarkCommentAsRead {
pub comment_id: CommentId,
pub read: bool,
pub auth: String,
}
#[derive(Deserialize)]
pub struct SaveComment {
pub comment_id: CommentId,
pub save: bool,
pub auth: String,
}
#[derive(Serialize, Clone)]
pub struct CommentResponse {
pub comment_view: CommentView,
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: CommentId,
pub score: i16,
pub auth: String,
}
#[derive(Deserialize)]
pub struct GetComments {
pub type_: String,
pub sort: String,
pub page: Option<i64>,
pub limit: Option<i64>,
pub community_id: Option<CommunityId>,
pub community_name: Option<String>,
pub auth: Option<String>,
}
#[derive(Serialize)]
pub struct GetCommentsResponse {
pub comments: Vec<CommentView>,
}
#[derive(Serialize, Deserialize)]
pub struct CreateCommentReport {
pub comment_id: CommentId,
pub reason: String,
pub auth: String,
}
#[derive(Serialize, Deserialize, Clone)]
pub struct CreateCommentReportResponse {
pub success: bool,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct ResolveCommentReport {
pub report_id: i32,
pub resolved: bool,
pub auth: String,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct ResolveCommentReportResponse {
// TODO this should probably return the view
pub report_id: i32,
pub resolved: bool,
}
#[derive(Serialize, Deserialize, Debug)]
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<CommunityId>,
pub auth: String,
}
#[derive(Serialize, Clone, Debug)]
pub struct ListCommentReportsResponse {
pub comments: Vec<CommentReportView>,
}

View file

@ -0,0 +1,133 @@
use lemmy_db_schema::{CommunityId, PersonId};
use lemmy_db_views_actor::{
community_follower_view::CommunityFollowerView,
community_moderator_view::CommunityModeratorView,
community_view::CommunityView,
person_view::PersonViewSafe,
};
use serde::{Deserialize, Serialize};
#[derive(Deserialize)]
pub struct GetCommunity {
pub id: Option<CommunityId>,
pub name: Option<String>,
pub auth: Option<String>,
}
#[derive(Serialize)]
pub struct GetCommunityResponse {
pub community_view: CommunityView,
pub moderators: Vec<CommunityModeratorView>,
pub online: usize,
}
#[derive(Deserialize)]
pub struct CreateCommunity {
pub name: String,
pub title: String,
pub description: Option<String>,
pub icon: Option<String>,
pub banner: Option<String>,
pub nsfw: bool,
pub auth: String,
}
#[derive(Serialize, Clone)]
pub struct CommunityResponse {
pub community_view: CommunityView,
}
#[derive(Deserialize, Debug)]
pub struct ListCommunities {
pub type_: String,
pub sort: String,
pub page: Option<i64>,
pub limit: Option<i64>,
pub auth: Option<String>,
}
#[derive(Serialize, Debug)]
pub struct ListCommunitiesResponse {
pub communities: Vec<CommunityView>,
}
#[derive(Deserialize, Clone)]
pub struct BanFromCommunity {
pub community_id: CommunityId,
pub person_id: PersonId,
pub ban: bool,
pub remove_data: bool,
pub reason: Option<String>,
pub expires: Option<i64>,
pub auth: String,
}
#[derive(Serialize, Clone)]
pub struct BanFromCommunityResponse {
pub person_view: PersonViewSafe,
pub banned: bool,
}
#[derive(Deserialize)]
pub struct AddModToCommunity {
pub community_id: CommunityId,
pub person_id: PersonId,
pub added: bool,
pub auth: String,
}
#[derive(Serialize, Clone)]
pub struct AddModToCommunityResponse {
pub moderators: Vec<CommunityModeratorView>,
}
#[derive(Deserialize)]
pub struct EditCommunity {
pub community_id: CommunityId,
pub title: String,
pub description: Option<String>,
pub icon: Option<String>,
pub banner: Option<String>,
pub nsfw: bool,
pub auth: String,
}
#[derive(Deserialize)]
pub struct DeleteCommunity {
pub community_id: CommunityId,
pub deleted: bool,
pub auth: String,
}
#[derive(Deserialize)]
pub struct RemoveCommunity {
pub community_id: CommunityId,
pub removed: bool,
pub reason: Option<String>,
pub expires: Option<i64>,
pub auth: String,
}
#[derive(Deserialize)]
pub struct FollowCommunity {
pub community_id: CommunityId,
pub follow: bool,
pub auth: String,
}
#[derive(Deserialize)]
pub struct GetFollowedCommunities {
pub auth: String,
}
#[derive(Serialize)]
pub struct GetFollowedCommunitiesResponse {
pub communities: Vec<CommunityFollowerView>,
}
#[derive(Deserialize)]
pub struct TransferCommunity {
pub community_id: CommunityId,
pub person_id: PersonId,
pub auth: String,
}

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

@ -0,0 +1,245 @@
use lemmy_db_views::{
comment_view::CommentView,
post_view::PostView,
private_message_view::PrivateMessageView,
};
use lemmy_db_views_actor::{
community_follower_view::CommunityFollowerView,
community_moderator_view::CommunityModeratorView,
person_mention_view::PersonMentionView,
person_view::PersonViewSafe,
};
use serde::{Deserialize, Serialize};
#[derive(Deserialize, Debug)]
pub struct Login {
pub username_or_email: String,
pub password: String,
}
use lemmy_db_schema::{CommunityId, PersonId, PersonMentionId, PrivateMessageId};
#[derive(Deserialize)]
pub struct Register {
pub username: String,
pub email: Option<String>,
pub password: String,
pub password_verify: String,
pub show_nsfw: bool,
pub captcha_uuid: Option<String>,
pub captcha_answer: Option<String>,
}
#[derive(Deserialize)]
pub struct GetCaptcha {}
#[derive(Serialize)]
pub struct GetCaptchaResponse {
pub ok: Option<CaptchaResponse>, // Will be None if captchas are disabled
}
#[derive(Serialize)]
pub struct CaptchaResponse {
pub png: String, // A Base64 encoded png
pub wav: Option<String>, // A Base64 encoded wav audio
pub uuid: String,
}
#[derive(Deserialize)]
pub struct SaveUserSettings {
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>,
pub email: Option<String>,
pub bio: Option<String>,
pub matrix_user_id: Option<String>,
pub new_password: Option<String>,
pub new_password_verify: Option<String>,
pub old_password: Option<String>,
pub show_avatars: Option<bool>,
pub send_notifications_to_email: Option<bool>,
pub auth: String,
}
#[derive(Serialize)]
pub struct LoginResponse {
pub jwt: String,
}
#[derive(Deserialize)]
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<CommunityId>,
pub saved_only: bool,
pub auth: Option<String>,
}
#[derive(Serialize)]
pub struct GetPersonDetailsResponse {
pub person_view: PersonViewSafe,
pub follows: Vec<CommunityFollowerView>,
pub moderates: Vec<CommunityModeratorView>,
pub comments: Vec<CommentView>,
pub posts: Vec<PostView>,
}
#[derive(Serialize)]
pub struct GetRepliesResponse {
pub replies: Vec<CommentView>,
}
#[derive(Serialize)]
pub struct GetPersonMentionsResponse {
pub mentions: Vec<PersonMentionView>,
}
#[derive(Deserialize)]
pub struct MarkAllAsRead {
pub auth: String,
}
#[derive(Deserialize)]
pub struct AddAdmin {
pub person_id: PersonId,
pub added: bool,
pub auth: String,
}
#[derive(Serialize, Clone)]
pub struct AddAdminResponse {
pub admins: Vec<PersonViewSafe>,
}
#[derive(Deserialize)]
pub struct BanPerson {
pub person_id: PersonId,
pub ban: bool,
pub remove_data: bool,
pub reason: Option<String>,
pub expires: Option<i64>,
pub auth: String,
}
#[derive(Serialize, Clone)]
pub struct BanPersonResponse {
pub person_view: PersonViewSafe,
pub banned: bool,
}
#[derive(Deserialize)]
pub struct GetReplies {
pub sort: String,
pub page: Option<i64>,
pub limit: Option<i64>,
pub unread_only: bool,
pub auth: String,
}
#[derive(Deserialize)]
pub struct GetPersonMentions {
pub sort: String,
pub page: Option<i64>,
pub limit: Option<i64>,
pub unread_only: bool,
pub auth: String,
}
#[derive(Deserialize)]
pub struct MarkPersonMentionAsRead {
pub person_mention_id: PersonMentionId,
pub read: bool,
pub auth: String,
}
#[derive(Serialize, Clone)]
pub struct PersonMentionResponse {
pub person_mention_view: PersonMentionView,
}
#[derive(Deserialize)]
pub struct DeleteAccount {
pub password: String,
pub auth: String,
}
#[derive(Deserialize)]
pub struct PasswordReset {
pub email: String,
}
#[derive(Serialize, Clone)]
pub struct PasswordResetResponse {}
#[derive(Deserialize)]
pub struct PasswordChange {
pub token: String,
pub password: String,
pub password_verify: String,
}
#[derive(Deserialize)]
pub struct CreatePrivateMessage {
pub content: String,
pub recipient_id: PersonId,
pub auth: String,
}
#[derive(Deserialize)]
pub struct EditPrivateMessage {
pub private_message_id: PrivateMessageId,
pub content: String,
pub auth: String,
}
#[derive(Deserialize)]
pub struct DeletePrivateMessage {
pub private_message_id: PrivateMessageId,
pub deleted: bool,
pub auth: String,
}
#[derive(Deserialize)]
pub struct MarkPrivateMessageAsRead {
pub private_message_id: PrivateMessageId,
pub read: bool,
pub auth: String,
}
#[derive(Deserialize)]
pub struct GetPrivateMessages {
pub unread_only: bool,
pub page: Option<i64>,
pub limit: Option<i64>,
pub auth: String,
}
#[derive(Serialize, Clone)]
pub struct PrivateMessagesResponse {
pub private_messages: Vec<PrivateMessageView>,
}
#[derive(Serialize, Clone)]
pub struct PrivateMessageResponse {
pub private_message_view: PrivateMessageView,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct GetReportCount {
pub community: Option<CommunityId>,
pub auth: String,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct GetReportCountResponse {
pub community: Option<CommunityId>,
pub comment_reports: i64,
pub post_reports: i64,
}

View file

@ -0,0 +1,149 @@
use lemmy_db_schema::{CommunityId, PostId};
use lemmy_db_views::{
comment_view::CommentView,
post_report_view::PostReportView,
post_view::PostView,
};
use lemmy_db_views_actor::{
community_moderator_view::CommunityModeratorView,
community_view::CommunityView,
};
use serde::{Deserialize, Serialize};
use url::Url;
#[derive(Deserialize, Debug)]
pub struct CreatePost {
pub name: String,
pub url: Option<Url>,
pub body: Option<String>,
pub nsfw: bool,
pub community_id: CommunityId,
pub auth: String,
}
#[derive(Serialize, Clone)]
pub struct PostResponse {
pub post_view: PostView,
}
#[derive(Deserialize)]
pub struct GetPost {
pub id: PostId,
pub auth: Option<String>,
}
#[derive(Serialize)]
pub struct GetPostResponse {
pub post_view: PostView,
pub community_view: CommunityView,
pub comments: Vec<CommentView>,
pub moderators: Vec<CommunityModeratorView>,
pub online: usize,
}
#[derive(Deserialize, Debug)]
pub struct GetPosts {
pub type_: String,
pub sort: String,
pub page: Option<i64>,
pub limit: Option<i64>,
pub community_id: Option<CommunityId>,
pub community_name: Option<String>,
pub auth: Option<String>,
}
#[derive(Serialize, Debug)]
pub struct GetPostsResponse {
pub posts: Vec<PostView>,
}
#[derive(Deserialize)]
pub struct CreatePostLike {
pub post_id: PostId,
pub score: i16,
pub auth: String,
}
#[derive(Deserialize)]
pub struct EditPost {
pub post_id: PostId,
pub name: String,
pub url: Option<Url>,
pub body: Option<String>,
pub nsfw: bool,
pub auth: String,
}
#[derive(Deserialize)]
pub struct DeletePost {
pub post_id: PostId,
pub deleted: bool,
pub auth: String,
}
#[derive(Deserialize)]
pub struct RemovePost {
pub post_id: PostId,
pub removed: bool,
pub reason: Option<String>,
pub auth: String,
}
#[derive(Deserialize)]
pub struct LockPost {
pub post_id: PostId,
pub locked: bool,
pub auth: String,
}
#[derive(Deserialize)]
pub struct StickyPost {
pub post_id: PostId,
pub stickied: bool,
pub auth: String,
}
#[derive(Deserialize)]
pub struct SavePost {
pub post_id: PostId,
pub save: bool,
pub auth: String,
}
#[derive(Serialize, Deserialize)]
pub struct CreatePostReport {
pub post_id: PostId,
pub reason: String,
pub auth: String,
}
#[derive(Serialize, Deserialize, Clone)]
pub struct CreatePostReportResponse {
pub success: bool,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct ResolvePostReport {
pub report_id: i32,
pub resolved: bool,
pub auth: String,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct ResolvePostReportResponse {
pub report_id: i32,
pub resolved: bool,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct ListPostReports {
pub page: Option<i64>,
pub limit: Option<i64>,
pub community: Option<CommunityId>,
pub auth: String,
}
#[derive(Serialize, Clone, Debug)]
pub struct ListPostReportsResponse {
pub posts: Vec<PostReportView>,
}

View file

@ -0,0 +1,137 @@
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,
mod_ban_from_community_view::ModBanFromCommunityView,
mod_ban_view::ModBanView,
mod_lock_post_view::ModLockPostView,
mod_remove_comment_view::ModRemoveCommentView,
mod_remove_community_view::ModRemoveCommunityView,
mod_remove_post_view::ModRemovePostView,
mod_sticky_post_view::ModStickyPostView,
};
use serde::{Deserialize, Serialize};
use url::Url;
#[derive(Deserialize, Debug)]
pub struct Search {
pub q: String,
pub type_: String,
pub community_id: Option<CommunityId>,
pub community_name: Option<String>,
pub sort: String,
pub page: Option<i64>,
pub limit: Option<i64>,
pub auth: Option<String>,
}
#[derive(Serialize, Debug)]
pub struct SearchResponse {
pub type_: String,
pub comments: Vec<CommentView>,
pub posts: Vec<PostView>,
pub communities: Vec<CommunityView>,
pub users: Vec<PersonViewSafe>,
}
#[derive(Deserialize)]
pub struct GetModlog {
pub mod_person_id: Option<PersonId>,
pub community_id: Option<CommunityId>,
pub page: Option<i64>,
pub limit: Option<i64>,
}
#[derive(Serialize)]
pub struct GetModlogResponse {
pub removed_posts: Vec<ModRemovePostView>,
pub locked_posts: Vec<ModLockPostView>,
pub stickied_posts: Vec<ModStickyPostView>,
pub removed_comments: Vec<ModRemoveCommentView>,
pub removed_communities: Vec<ModRemoveCommunityView>,
pub banned_from_community: Vec<ModBanFromCommunityView>,
pub banned: Vec<ModBanView>,
pub added_to_community: Vec<ModAddCommunityView>,
pub added: Vec<ModAddView>,
}
#[derive(Deserialize)]
pub struct CreateSite {
pub name: String,
pub description: Option<String>,
pub icon: Option<Url>,
pub banner: Option<Url>,
pub enable_downvotes: bool,
pub open_registration: bool,
pub enable_nsfw: bool,
pub auth: String,
}
#[derive(Deserialize)]
pub struct EditSite {
pub name: String,
pub description: Option<String>,
pub icon: Option<String>,
pub banner: Option<String>,
pub enable_downvotes: bool,
pub open_registration: bool,
pub enable_nsfw: bool,
pub auth: String,
}
#[derive(Deserialize)]
pub struct GetSite {
pub auth: Option<String>,
}
#[derive(Serialize, Clone)]
pub struct SiteResponse {
pub site_view: SiteView,
}
#[derive(Serialize)]
pub struct GetSiteResponse {
pub site_view: Option<SiteView>, // Because the site might not be set up yet
pub admins: Vec<PersonViewSafe>,
pub banned: Vec<PersonViewSafe>,
pub online: usize,
pub version: String,
pub my_user: Option<LocalUserSettingsView>,
pub federated_instances: Option<FederatedInstances>, // Federation may be disabled
}
#[derive(Deserialize)]
pub struct TransferSite {
pub person_id: PersonId,
pub auth: String,
}
#[derive(Deserialize)]
pub struct GetSiteConfig {
pub auth: String,
}
#[derive(Serialize)]
pub struct GetSiteConfigResponse {
pub config_hjson: String,
}
#[derive(Deserialize)]
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,
}

52
crates/apub/Cargo.toml Normal file
View file

@ -0,0 +1,52 @@
[package]
name = "lemmy_apub"
version = "0.1.0"
edition = "2018"
[lib]
name = "lemmy_apub"
path = "src/lib.rs"
doctest = false
[dependencies]
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.10"
activitystreams-ext = "0.1.0-alpha.2"
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"] }
percent-encoding = "2.1.0"
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.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.38"
thiserror = "1.0.23"
background-jobs = "0.8.0"
reqwest = { version = "0.10.10", features = ["json"] }
backtrace = "0.3.56"

View file

@ -0,0 +1,2 @@
pub(crate) mod receive;
pub(crate) mod send;

View file

@ -0,0 +1,260 @@
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_utils::{location_info, utils::scrape_text_for_mentions, LemmyError};
use lemmy_websocket::{messages::SendComment, LemmyContext, UserOperation};
pub(crate) async fn receive_create_comment(
create: Create,
context: &LemmyContext,
request_counter: &mut i32,
) -> Result<(), LemmyError> {
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, 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??;
// Note:
// Although mentions could be gotten from the post tags (they are included there), or the ccs,
// 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(),
person,
post,
context.pool(),
true,
)
.await?;
// Refetch the view
let comment_view = blocking(context.pool(), move |conn| {
CommentView::read(conn, comment.id, None)
})
.await??;
let res = CommentResponse {
comment_view,
recipient_ids,
form_id: None,
};
context.chat_server().do_send(SendComment {
op: UserOperation::CreateComment,
comment: res,
websocket_id: None,
});
Ok(())
}
pub(crate) async fn receive_update_comment(
update: Update,
context: &LemmyContext,
request_counter: &mut i32,
) -> Result<(), LemmyError> {
let note = NoteExt::from_any_base(update.object().to_owned().one().context(location_info!())?)?
.context(location_info!())?;
let person = get_actor_as_person(&update, context, 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;
let post = blocking(context.pool(), move |conn| Post::read(conn, post_id)).await??;
let mentions = scrape_text_for_mentions(&comment.content);
let recipient_ids =
send_local_notifs(mentions, comment, person, post, context.pool(), false).await?;
// Refetch the view
let comment_view = blocking(context.pool(), move |conn| {
CommentView::read(conn, comment_id, None)
})
.await??;
let res = CommentResponse {
comment_view,
recipient_ids,
form_id: None,
};
context.chat_server().do_send(SendComment {
op: UserOperation::EditComment,
comment: res,
websocket_id: None,
});
Ok(())
}
pub(crate) async fn receive_like_comment(
like: Like,
comment: Comment,
context: &LemmyContext,
request_counter: &mut i32,
) -> Result<(), LemmyError> {
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,
person_id: person.id,
score: 1,
};
let person_id = person.id;
blocking(context.pool(), move |conn| {
CommentLike::remove(conn, person_id, comment_id)?;
CommentLike::like(conn, &like_form)
})
.await??;
// Refetch the view
let comment_view = blocking(context.pool(), move |conn| {
CommentView::read(conn, comment_id, None)
})
.await??;
// TODO get those recipient actor ids from somewhere
let recipient_ids = vec![];
let res = CommentResponse {
comment_view,
recipient_ids,
form_id: None,
};
context.chat_server().do_send(SendComment {
op: UserOperation::CreateCommentLike,
comment: res,
websocket_id: None,
});
Ok(())
}
pub(crate) async fn receive_dislike_comment(
dislike: Dislike,
comment: Comment,
context: &LemmyContext,
request_counter: &mut i32,
) -> Result<(), LemmyError> {
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,
person_id: person.id,
score: -1,
};
let person_id = person.id;
blocking(context.pool(), move |conn| {
CommentLike::remove(conn, person_id, comment_id)?;
CommentLike::like(conn, &like_form)
})
.await??;
// Refetch the view
let comment_view = blocking(context.pool(), move |conn| {
CommentView::read(conn, comment_id, None)
})
.await??;
// TODO get those recipient actor ids from somewhere
let recipient_ids = vec![];
let res = CommentResponse {
comment_view,
recipient_ids,
form_id: None,
};
context.chat_server().do_send(SendComment {
op: UserOperation::CreateCommentLike,
comment: res,
websocket_id: None,
});
Ok(())
}
pub(crate) async fn receive_delete_comment(
context: &LemmyContext,
comment: Comment,
) -> Result<(), LemmyError> {
let deleted_comment = blocking(context.pool(), move |conn| {
Comment::update_deleted(conn, comment.id, true)
})
.await??;
// Refetch the view
let comment_id = deleted_comment.id;
let comment_view = blocking(context.pool(), move |conn| {
CommentView::read(conn, comment_id, None)
})
.await??;
// TODO get those recipient actor ids from somewhere
let recipient_ids = vec![];
let res = CommentResponse {
comment_view,
recipient_ids,
form_id: None,
};
context.chat_server().do_send(SendComment {
op: UserOperation::EditComment,
comment: res,
websocket_id: None,
});
Ok(())
}
pub(crate) async fn receive_remove_comment(
context: &LemmyContext,
_remove: Remove,
comment: Comment,
) -> Result<(), LemmyError> {
let removed_comment = blocking(context.pool(), move |conn| {
Comment::update_removed(conn, comment.id, true)
})
.await??;
// Refetch the view
let comment_id = removed_comment.id;
let comment_view = blocking(context.pool(), move |conn| {
CommentView::read(conn, comment_id, None)
})
.await??;
// TODO get those recipient actor ids from somewhere
let recipient_ids = vec![];
let res = CommentResponse {
comment_view,
recipient_ids,
form_id: None,
};
context.chat_server().do_send(SendComment {
op: UserOperation::EditComment,
comment: res,
websocket_id: None,
});
Ok(())
}

View file

@ -0,0 +1,150 @@
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_utils::LemmyError;
use lemmy_websocket::{messages::SendComment, LemmyContext, UserOperation};
pub(crate) async fn receive_undo_like_comment(
like: &Like,
comment: Comment,
context: &LemmyContext,
request_counter: &mut i32,
) -> Result<(), LemmyError> {
let person = get_actor_as_person(like, context, request_counter).await?;
let comment_id = comment.id;
let person_id = person.id;
blocking(context.pool(), move |conn| {
CommentLike::remove(conn, person_id, comment_id)
})
.await??;
// Refetch the view
let comment_view = blocking(context.pool(), move |conn| {
CommentView::read(conn, comment_id, None)
})
.await??;
// TODO get those recipient actor ids from somewhere
let recipient_ids = vec![];
let res = CommentResponse {
comment_view,
recipient_ids,
form_id: None,
};
context.chat_server().do_send(SendComment {
op: UserOperation::CreateCommentLike,
comment: res,
websocket_id: None,
});
Ok(())
}
pub(crate) async fn receive_undo_dislike_comment(
dislike: &Dislike,
comment: Comment,
context: &LemmyContext,
request_counter: &mut i32,
) -> Result<(), LemmyError> {
let person = get_actor_as_person(dislike, context, request_counter).await?;
let comment_id = comment.id;
let person_id = person.id;
blocking(context.pool(), move |conn| {
CommentLike::remove(conn, person_id, comment_id)
})
.await??;
// Refetch the view
let comment_view = blocking(context.pool(), move |conn| {
CommentView::read(conn, comment_id, None)
})
.await??;
// TODO get those recipient actor ids from somewhere
let recipient_ids = vec![];
let res = CommentResponse {
comment_view,
recipient_ids,
form_id: None,
};
context.chat_server().do_send(SendComment {
op: UserOperation::CreateCommentLike,
comment: res,
websocket_id: None,
});
Ok(())
}
pub(crate) async fn receive_undo_delete_comment(
context: &LemmyContext,
comment: Comment,
) -> Result<(), LemmyError> {
let deleted_comment = blocking(context.pool(), move |conn| {
Comment::update_deleted(conn, comment.id, false)
})
.await??;
// Refetch the view
let comment_id = deleted_comment.id;
let comment_view = blocking(context.pool(), move |conn| {
CommentView::read(conn, comment_id, None)
})
.await??;
// TODO get those recipient actor ids from somewhere
let recipient_ids = vec![];
let res = CommentResponse {
comment_view,
recipient_ids,
form_id: None,
};
context.chat_server().do_send(SendComment {
op: UserOperation::EditComment,
comment: res,
websocket_id: None,
});
Ok(())
}
pub(crate) async fn receive_undo_remove_comment(
context: &LemmyContext,
comment: Comment,
) -> Result<(), LemmyError> {
let removed_comment = blocking(context.pool(), move |conn| {
Comment::update_removed(conn, comment.id, false)
})
.await??;
// Refetch the view
let comment_id = removed_comment.id;
let comment_view = blocking(context.pool(), move |conn| {
CommentView::read(conn, comment_id, None)
})
.await??;
// TODO get those recipient actor ids from somewhere
let recipient_ids = vec![];
let res = CommentResponse {
comment_view,
recipient_ids,
form_id: None,
};
context.chat_server().do_send(SendComment {
op: UserOperation::EditComment,
comment: res,
websocket_id: None,
});
Ok(())
}

View file

@ -0,0 +1,167 @@
use crate::{activities::receive::verify_activity_domains_valid, inbox::is_addressed_to_public};
use activitystreams::{
activity::{ActorAndObjectRefExt, Delete, Remove, Undo},
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_utils::{location_info, LemmyError};
use lemmy_websocket::{messages::SendCommunityRoomMessage, LemmyContext, UserOperation};
use url::Url;
pub(crate) async fn receive_delete_community(
context: &LemmyContext,
community: Community,
) -> Result<(), LemmyError> {
let deleted_community = blocking(context.pool(), move |conn| {
Community::update_deleted(conn, community.id, true)
})
.await??;
let community_id = deleted_community.id;
let res = CommunityResponse {
community_view: blocking(context.pool(), move |conn| {
CommunityView::read(conn, community_id, None)
})
.await??,
};
let community_id = res.community_view.community.id;
context.chat_server().do_send(SendCommunityRoomMessage {
op: UserOperation::EditCommunity,
response: res,
community_id,
websocket_id: None,
});
Ok(())
}
pub(crate) async fn receive_remove_community(
context: &LemmyContext,
activity: AnyBase,
expected_domain: &Url,
) -> Result<(), LemmyError> {
let remove = Remove::from_any_base(activity)?.context(location_info!())?;
verify_activity_domains_valid(&remove, expected_domain, true)?;
is_addressed_to_public(&remove)?;
let community_uri = remove
.object()
.to_owned()
.single_xsd_any_uri()
.context(location_info!())?;
let community = blocking(context.pool(), move |conn| {
Community::read_from_apub_id(conn, &community_uri.into())
})
.await??;
let removed_community = blocking(context.pool(), move |conn| {
Community::update_removed(conn, community.id, true)
})
.await??;
let community_id = removed_community.id;
let res = CommunityResponse {
community_view: blocking(context.pool(), move |conn| {
CommunityView::read(conn, community_id, None)
})
.await??,
};
let community_id = res.community_view.community.id;
context.chat_server().do_send(SendCommunityRoomMessage {
op: UserOperation::EditCommunity,
response: res,
community_id,
websocket_id: None,
});
Ok(())
}
pub(crate) async fn receive_undo_delete_community(
context: &LemmyContext,
undo: Undo,
community: Community,
expected_domain: &Url,
) -> Result<(), LemmyError> {
is_addressed_to_public(&undo)?;
let inner = undo.object().to_owned().one().context(location_info!())?;
let delete = Delete::from_any_base(inner)?.context(location_info!())?;
verify_activity_domains_valid(&delete, expected_domain, true)?;
is_addressed_to_public(&delete)?;
let deleted_community = blocking(context.pool(), move |conn| {
Community::update_deleted(conn, community.id, false)
})
.await??;
let community_id = deleted_community.id;
let res = CommunityResponse {
community_view: blocking(context.pool(), move |conn| {
CommunityView::read(conn, community_id, None)
})
.await??,
};
let community_id = res.community_view.community.id;
context.chat_server().do_send(SendCommunityRoomMessage {
op: UserOperation::EditCommunity,
response: res,
community_id,
websocket_id: None,
});
Ok(())
}
pub(crate) async fn receive_undo_remove_community(
context: &LemmyContext,
undo: Undo,
expected_domain: &Url,
) -> Result<(), LemmyError> {
is_addressed_to_public(&undo)?;
let inner = undo.object().to_owned().one().context(location_info!())?;
let remove = Remove::from_any_base(inner)?.context(location_info!())?;
verify_activity_domains_valid(&remove, &expected_domain, true)?;
is_addressed_to_public(&remove)?;
let community_uri = remove
.object()
.to_owned()
.single_xsd_any_uri()
.context(location_info!())?;
let community = blocking(context.pool(), move |conn| {
Community::read_from_apub_id(conn, &community_uri.into())
})
.await??;
let removed_community = blocking(context.pool(), move |conn| {
Community::update_removed(conn, community.id, false)
})
.await??;
let community_id = removed_community.id;
let res = CommunityResponse {
community_view: blocking(context.pool(), move |conn| {
CommunityView::read(conn, community_id, None)
})
.await??,
};
let community_id = res.community_view.community.id;
context.chat_server().do_send(SendCommunityRoomMessage {
op: UserOperation::EditCommunity,
response: res,
community_id,
websocket_id: None,
});
Ok(())
}

View file

@ -0,0 +1,81 @@
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::person::Person;
use lemmy_utils::{location_info, LemmyError};
use lemmy_websocket::LemmyContext;
use log::debug;
use std::fmt::Debug;
use url::Url;
pub(crate) mod comment;
pub(crate) mod comment_undo;
pub(crate) mod community;
pub(crate) mod post;
pub(crate) mod post_undo;
pub(crate) mod private_message;
/// Return HTTP 501 for unsupported activities in inbox.
pub(crate) fn receive_unhandled_activity<A>(activity: A) -> Result<(), LemmyError>
where
A: Debug,
{
debug!("received unhandled activity type: {:?}", activity);
Err(anyhow!("Activity not supported").into())
}
/// 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<Person, LemmyError>
where
T: AsBase<A> + ActorAndObjectRef,
{
let actor = activity.actor()?;
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
/// also checks the ID of the inner object.
///
/// The reason that this starts with the actor ID is that it was already confirmed as correct by the
/// HTTP signature.
pub(crate) fn verify_activity_domains_valid<T, Kind>(
activity: &T,
actor_id: &Url,
object_domain_must_match: bool,
) -> Result<(), LemmyError>
where
T: AsBase<Kind> + ActorAndObjectRef,
{
let expected_domain = actor_id.domain().context(location_info!())?;
activity.id(expected_domain)?;
let object_id = match activity.object().to_owned().single_xsd_any_uri() {
// object is just an ID
Some(id) => id,
// object is something like an activity, a comment or a post
None => activity
.object()
.to_owned()
.one()
.context(location_info!())?
.id()
.context(location_info!())?
.to_owned(),
};
if object_domain_must_match && object_id.domain() != Some(expected_domain) {
return Err(DomainError.into());
}
Ok(())
}

View file

@ -0,0 +1,199 @@
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_utils::{location_info, LemmyError};
use lemmy_websocket::{messages::SendPost, LemmyContext, UserOperation};
pub(crate) async fn receive_create_post(
create: Create,
context: &LemmyContext,
request_counter: &mut i32,
) -> Result<(), LemmyError> {
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, person.actor_id(), request_counter).await?;
// Refetch the view
let post_id = post.id;
let post_view = blocking(context.pool(), move |conn| {
PostView::read(conn, post_id, None)
})
.await??;
let res = PostResponse { post_view };
context.chat_server().do_send(SendPost {
op: UserOperation::CreatePost,
post: res,
websocket_id: None,
});
Ok(())
}
pub(crate) async fn receive_update_post(
update: Update,
context: &LemmyContext,
request_counter: &mut i32,
) -> Result<(), LemmyError> {
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, person.actor_id(), request_counter).await?;
let post_id = post.id;
// Refetch the view
let post_view = blocking(context.pool(), move |conn| {
PostView::read(conn, post_id, None)
})
.await??;
let res = PostResponse { post_view };
context.chat_server().do_send(SendPost {
op: UserOperation::EditPost,
post: res,
websocket_id: None,
});
Ok(())
}
pub(crate) async fn receive_like_post(
like: Like,
post: Post,
context: &LemmyContext,
request_counter: &mut i32,
) -> Result<(), LemmyError> {
let person = get_actor_as_person(&like, context, request_counter).await?;
let post_id = post.id;
let like_form = PostLikeForm {
post_id,
person_id: person.id,
score: 1,
};
let person_id = person.id;
blocking(context.pool(), move |conn| {
PostLike::remove(conn, person_id, post_id)?;
PostLike::like(conn, &like_form)
})
.await??;
// Refetch the view
let post_view = blocking(context.pool(), move |conn| {
PostView::read(conn, post_id, None)
})
.await??;
let res = PostResponse { post_view };
context.chat_server().do_send(SendPost {
op: UserOperation::CreatePostLike,
post: res,
websocket_id: None,
});
Ok(())
}
pub(crate) async fn receive_dislike_post(
dislike: Dislike,
post: Post,
context: &LemmyContext,
request_counter: &mut i32,
) -> Result<(), LemmyError> {
let person = get_actor_as_person(&dislike, context, request_counter).await?;
let post_id = post.id;
let like_form = PostLikeForm {
post_id,
person_id: person.id,
score: -1,
};
let person_id = person.id;
blocking(context.pool(), move |conn| {
PostLike::remove(conn, person_id, post_id)?;
PostLike::like(conn, &like_form)
})
.await??;
// Refetch the view
let post_view = blocking(context.pool(), move |conn| {
PostView::read(conn, post_id, None)
})
.await??;
let res = PostResponse { post_view };
context.chat_server().do_send(SendPost {
op: UserOperation::CreatePostLike,
post: res,
websocket_id: None,
});
Ok(())
}
pub(crate) async fn receive_delete_post(
context: &LemmyContext,
post: Post,
) -> Result<(), LemmyError> {
let deleted_post = blocking(context.pool(), move |conn| {
Post::update_deleted(conn, post.id, true)
})
.await??;
// Refetch the view
let post_id = deleted_post.id;
let post_view = blocking(context.pool(), move |conn| {
PostView::read(conn, post_id, None)
})
.await??;
let res = PostResponse { post_view };
context.chat_server().do_send(SendPost {
op: UserOperation::EditPost,
post: res,
websocket_id: None,
});
Ok(())
}
pub(crate) async fn receive_remove_post(
context: &LemmyContext,
_remove: Remove,
post: Post,
) -> Result<(), LemmyError> {
let removed_post = blocking(context.pool(), move |conn| {
Post::update_removed(conn, post.id, true)
})
.await??;
// Refetch the view
let post_id = removed_post.id;
let post_view = blocking(context.pool(), move |conn| {
PostView::read(conn, post_id, None)
})
.await??;
let res = PostResponse { post_view };
context.chat_server().do_send(SendPost {
op: UserOperation::EditPost,
post: res,
websocket_id: None,
});
Ok(())
}

View file

@ -0,0 +1,125 @@
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_utils::LemmyError;
use lemmy_websocket::{messages::SendPost, LemmyContext, UserOperation};
pub(crate) async fn receive_undo_like_post(
like: &Like,
post: Post,
context: &LemmyContext,
request_counter: &mut i32,
) -> Result<(), LemmyError> {
let person = get_actor_as_person(like, context, request_counter).await?;
let post_id = post.id;
let person_id = person.id;
blocking(context.pool(), move |conn| {
PostLike::remove(conn, person_id, post_id)
})
.await??;
// Refetch the view
let post_view = blocking(context.pool(), move |conn| {
PostView::read(conn, post_id, None)
})
.await??;
let res = PostResponse { post_view };
context.chat_server().do_send(SendPost {
op: UserOperation::CreatePostLike,
post: res,
websocket_id: None,
});
Ok(())
}
pub(crate) async fn receive_undo_dislike_post(
dislike: &Dislike,
post: Post,
context: &LemmyContext,
request_counter: &mut i32,
) -> Result<(), LemmyError> {
let person = get_actor_as_person(dislike, context, request_counter).await?;
let post_id = post.id;
let person_id = person.id;
blocking(context.pool(), move |conn| {
PostLike::remove(conn, person_id, post_id)
})
.await??;
// Refetch the view
let post_view = blocking(context.pool(), move |conn| {
PostView::read(conn, post_id, None)
})
.await??;
let res = PostResponse { post_view };
context.chat_server().do_send(SendPost {
op: UserOperation::CreatePostLike,
post: res,
websocket_id: None,
});
Ok(())
}
pub(crate) async fn receive_undo_delete_post(
context: &LemmyContext,
post: Post,
) -> Result<(), LemmyError> {
let deleted_post = blocking(context.pool(), move |conn| {
Post::update_deleted(conn, post.id, false)
})
.await??;
// Refetch the view
let post_id = deleted_post.id;
let post_view = blocking(context.pool(), move |conn| {
PostView::read(conn, post_id, None)
})
.await??;
let res = PostResponse { post_view };
context.chat_server().do_send(SendPost {
op: UserOperation::EditPost,
post: res,
websocket_id: None,
});
Ok(())
}
pub(crate) async fn receive_undo_remove_post(
context: &LemmyContext,
post: Post,
) -> Result<(), LemmyError> {
let removed_post = blocking(context.pool(), move |conn| {
Post::update_removed(conn, post.id, false)
})
.await??;
// Refetch the view
let post_id = removed_post.id;
let post_view = blocking(context.pool(), move |conn| {
PostView::read(conn, post_id, None)
})
.await??;
let res = PostResponse { post_view };
context.chat_server().do_send(SendPost {
op: UserOperation::EditPost,
post: res,
websocket_id: None,
});
Ok(())
}

View file

@ -0,0 +1,228 @@
use crate::{
activities::receive::verify_activity_domains_valid,
check_is_apub_id_valid,
fetcher::person::get_or_fetch_and_upsert_person,
inbox::get_activity_to_and_cc,
objects::FromApub,
NoteExt,
};
use activitystreams::{
activity::{ActorAndObjectRefExt, Create, Delete, Undo, Update},
base::{AsBase, ExtendsExt},
object::AsObject,
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::{local_user_view::LocalUserView, private_message_view::PrivateMessageView};
use lemmy_utils::{location_info, LemmyError};
use lemmy_websocket::{messages::SendUserRoomMessage, LemmyContext, UserOperation};
use url::Url;
pub(crate) async fn receive_create_private_message(
context: &LemmyContext,
create: Create,
expected_domain: Url,
request_counter: &mut i32,
) -> Result<(), LemmyError> {
check_private_message_activity_valid(&create, context, request_counter).await?;
let note = NoteExt::from_any_base(
create
.object()
.as_one()
.context(location_info!())?
.to_owned(),
)?
.context(location_info!())?;
let private_message =
PrivateMessage::from_apub(&note, context, expected_domain, request_counter).await?;
let message = blocking(&context.pool(), move |conn| {
PrivateMessageView::read(conn, private_message.id)
})
.await??;
let res = PrivateMessageResponse {
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,
local_recipient_id,
websocket_id: None,
});
Ok(())
}
pub(crate) async fn receive_update_private_message(
context: &LemmyContext,
update: Update,
expected_domain: Url,
request_counter: &mut i32,
) -> Result<(), LemmyError> {
check_private_message_activity_valid(&update, context, request_counter).await?;
let object = update
.object()
.as_one()
.context(location_info!())?
.to_owned();
let note = NoteExt::from_any_base(object)?.context(location_info!())?;
let private_message =
PrivateMessage::from_apub(&note, context, expected_domain, request_counter).await?;
let private_message_id = private_message.id;
let message = blocking(&context.pool(), move |conn| {
PrivateMessageView::read(conn, private_message_id)
})
.await??;
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,
local_recipient_id,
websocket_id: None,
});
Ok(())
}
pub(crate) async fn receive_delete_private_message(
context: &LemmyContext,
delete: Delete,
private_message: PrivateMessage,
request_counter: &mut i32,
) -> Result<(), LemmyError> {
check_private_message_activity_valid(&delete, context, request_counter).await?;
let deleted_private_message = blocking(context.pool(), move |conn| {
PrivateMessage::update_deleted(conn, private_message.id, true)
})
.await??;
let message = blocking(&context.pool(), move |conn| {
PrivateMessageView::read(&conn, deleted_private_message.id)
})
.await??;
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,
local_recipient_id,
websocket_id: None,
});
Ok(())
}
pub(crate) async fn receive_undo_delete_private_message(
context: &LemmyContext,
undo: Undo,
expected_domain: &Url,
private_message: PrivateMessage,
request_counter: &mut i32,
) -> Result<(), LemmyError> {
check_private_message_activity_valid(&undo, context, request_counter).await?;
let object = undo.object().to_owned().one().context(location_info!())?;
let delete = Delete::from_any_base(object)?.context(location_info!())?;
verify_activity_domains_valid(&delete, expected_domain, true)?;
check_private_message_activity_valid(&delete, context, request_counter).await?;
let deleted_private_message = blocking(context.pool(), move |conn| {
PrivateMessage::update_deleted(conn, private_message.id, false)
})
.await??;
let message = blocking(&context.pool(), move |conn| {
PrivateMessageView::read(&conn, deleted_private_message.id)
})
.await??;
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,
local_recipient_id,
websocket_id: None,
});
Ok(())
}
async fn check_private_message_activity_valid<T, Kind>(
activity: &T,
context: &LemmyContext,
request_counter: &mut i32,
) -> Result<(), LemmyError>
where
T: AsBase<Kind> + AsObject<Kind> + ActorAndObjectRefExt,
{
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 person").into());
}
if to_and_cc.contains(&public()) {
return Err(anyhow!("Private message cant be public").into());
}
let person_id = activity
.actor()?
.to_owned()
.single_xsd_any_uri()
.context(location_info!())?;
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

@ -0,0 +1,440 @@
use crate::{
activities::send::generate_activity_id,
activity_queue::{send_comment_mentions, send_to_community},
extensions::context::lemmy_context,
fetcher::person::get_or_fetch_and_upsert_person,
objects::ToApub,
ActorType,
ApubLikeableType,
ApubObjectType,
};
use activitystreams::{
activity::{
kind::{CreateType, DeleteType, DislikeType, LikeType, RemoveType, UndoType, UpdateType},
Create,
Delete,
Dislike,
Like,
Remove,
Undo,
Update,
},
base::AnyBase,
link::Mention,
prelude::*,
public,
};
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, person::Person, post::Post};
use lemmy_utils::{
request::{retry, RecvError},
settings::structs::Settings,
utils::{scrape_text_for_mentions, MentionData},
LemmyError,
};
use lemmy_websocket::LemmyContext;
use log::debug;
use reqwest::Client;
use serde_json::Error;
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 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;
let post = blocking(context.pool(), move |conn| Post::read(conn, post_id)).await??;
let community_id = post.community_id;
let community = blocking(context.pool(), move |conn| {
Community::read(conn, community_id)
})
.await??;
let maa = collect_non_local_mentions(&self, &community, context).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(public())
.set_many_ccs(maa.ccs.to_owned())
// Set the mention tags
.set_many_tags(maa.get_tags()?);
send_to_community(create.clone(), &creator, &community, context).await?;
send_comment_mentions(&creator, maa.inboxes, create, context).await?;
Ok(())
}
/// Send out information about an edited post, to the followers of the community and mentioned
/// 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;
let post = blocking(context.pool(), move |conn| Post::read(conn, post_id)).await??;
let community_id = post.community_id;
let community = blocking(context.pool(), move |conn| {
Community::read(conn, community_id)
})
.await??;
let maa = collect_non_local_mentions(&self, &community, context).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(public())
.set_many_ccs(maa.ccs.to_owned())
// Set the mention tags
.set_many_tags(maa.get_tags()?);
send_to_community(update.clone(), &creator, &community, context).await?;
send_comment_mentions(&creator, maa.inboxes, update, context).await?;
Ok(())
}
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??;
let community_id = post.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().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()]);
send_to_community(delete, &creator, &community, context).await?;
Ok(())
}
async fn send_undo_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??;
let community_id = post.community_id;
let community = blocking(context.pool(), move |conn| {
Community::read(conn, community_id)
})
.await??;
// Generate a fake delete activity, with the correct object
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()]);
// 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(public())
.set_many_ccs(vec![community.actor_id()]);
send_to_community(undo, &creator, &community, context).await?;
Ok(())
}
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??;
let community_id = post.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().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()]);
send_to_community(remove, &mod_, &community, context).await?;
Ok(())
}
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??;
let community_id = post.community_id;
let community = blocking(context.pool(), move |conn| {
Community::read(conn, community_id)
})
.await??;
// Generate a fake delete activity, with the correct object
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()]);
// Undo that fake activity
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()]);
send_to_community(undo, &mod_, &community, context).await?;
Ok(())
}
}
#[async_trait::async_trait(?Send)]
impl ApubLikeableType for Comment {
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??;
let community_id = post.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().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()]);
send_to_community(like, &creator, &community, context).await?;
Ok(())
}
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??;
let community_id = post.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().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()]);
send_to_community(dislike, &creator, &community, context).await?;
Ok(())
}
async fn send_undo_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??;
let community_id = post.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().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()]);
// Undo that fake activity
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()]);
send_to_community(undo, &creator, &community, context).await?;
Ok(())
}
}
struct MentionsAndAddresses {
ccs: Vec<Url>,
inboxes: Vec<Url>,
tags: Vec<Mention>,
}
impl MentionsAndAddresses {
fn get_tags(&self) -> Result<Vec<AnyBase>, Error> {
self
.tags
.iter()
.map(|t| t.to_owned().into_any_base())
.collect::<Result<Vec<AnyBase>, Error>>()
}
}
/// 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 persons / addresses that go in the cc field.
async fn collect_non_local_mentions(
comment: &Comment,
community: &Community,
context: &LemmyContext,
) -> Result<MentionsAndAddresses, LemmyError> {
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 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>>();
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_person = get_or_fetch_and_upsert_person(&actor_id, context, &mut 0).await?;
inboxes.push(mention_person.get_shared_inbox_or_inbox_url());
let mut mention_tag = Mention::new();
mention_tag.set_href(actor_id).set_name(mention.full_name());
tags.push(mention_tag);
}
}
let inboxes = inboxes.into_iter().unique().collect();
Ok(MentionsAndAddresses {
ccs: addressed_ccs,
inboxes,
tags,
})
}
/// 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(
pool: &DbPool,
comment: &Comment,
) -> 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??;
parent_comment.creator_id
} else {
let parent_post_id = comment.post_id;
let parent_post = blocking(pool, move |conn| Post::read(conn, parent_post_id)).await??;
parent_post.creator_id
};
Ok(blocking(pool, move |conn| Person::read(conn, parent_creator_id)).await??)
}
/// 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!(
"{}://{}/.well-known/webfinger?resource=acct:{}@{}",
Settings::get().get_protocol_string(),
mention.domain,
mention.name,
mention.domain
);
debug!("Fetching webfinger url: {}", &fetch_url);
let response = retry(|| client.get(&fetch_url).send()).await?;
let res: WebFingerResponse = response
.json()
.await
.map_err(|e| RecvError(e.to_string()))?;
let link = res
.links
.iter()
.find(|l| l.type_.eq(&Some("application/activity+json".to_string())))
.ok_or_else(|| anyhow!("No application/activity+json link found."))?;
link
.href
.to_owned()
.ok_or_else(|| anyhow!("No href found.").into())
}

View file

@ -0,0 +1,214 @@
use crate::{
activities::send::generate_activity_id,
activity_queue::{send_activity_single_dest, send_to_community_followers},
check_is_apub_id_valid,
extensions::context::lemmy_context,
fetcher::person::get_or_fetch_and_upsert_person,
insert_activity,
ActorType,
};
use activitystreams::{
activity::{
kind::{AcceptType, AnnounceType, DeleteType, LikeType, RemoveType, UndoType},
Accept,
ActorAndObjectRefExt,
Announce,
Delete,
Follow,
Remove,
Undo,
},
base::{AnyBase, BaseExt, ExtendsExt},
object::ObjectExt,
public,
};
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_utils::{location_info, settings::structs::Settings, LemmyError};
use lemmy_websocket::LemmyContext;
use url::Url;
#[async_trait::async_trait(?Send)]
impl ActorType for Community {
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()
}
fn private_key(&self) -> Option<String> {
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,
_context: &LemmyContext,
) -> Result<(), LemmyError> {
unimplemented!()
}
async fn send_unfollow(
&self,
_follow_actor_id: &Url,
_context: &LemmyContext,
) -> Result<(), LemmyError> {
unimplemented!()
}
/// As a local community, accept the follow request from a remote person.
async fn send_accept_follow(
&self,
follow: Follow,
context: &LemmyContext,
) -> Result<(), LemmyError> {
let actor_uri = follow
.actor()?
.as_single_xsd_any_uri()
.context(location_info!())?;
let person = get_or_fetch_and_upsert_person(actor_uri, context, &mut 0).await?;
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(person.actor_id());
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());
delete
.set_many_contexts(lemmy_context()?)
.set_id(generate_activity_id(DeleteType::Delete)?)
.set_to(public())
.set_many_ccs(vec![self.followers_url.clone().into_inner()]);
send_to_community_followers(delete, self, context).await?;
Ok(())
}
/// 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());
delete
.set_many_contexts(lemmy_context()?)
.set_id(generate_activity_id(DeleteType::Delete)?)
.set_to(public())
.set_many_ccs(vec![self.followers_url.clone().into_inner()]);
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.followers_url.clone().into_inner()]);
send_to_community_followers(undo, self, context).await?;
Ok(())
}
/// 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());
remove
.set_many_contexts(lemmy_context()?)
.set_id(generate_activity_id(RemoveType::Remove)?)
.set_to(public())
.set_many_ccs(vec![self.followers_url.clone().into_inner()]);
send_to_community_followers(remove, self, context).await?;
Ok(())
}
/// 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());
remove
.set_many_contexts(lemmy_context()?)
.set_id(generate_activity_id(RemoveType::Remove)?)
.set_to(public())
.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()?);
undo
.set_many_contexts(lemmy_context()?)
.set_id(generate_activity_id(LikeType::Like)?)
.set_to(public())
.set_many_ccs(vec![self.followers_url.clone().into_inner()]);
send_to_community_followers(undo, self, context).await?;
Ok(())
}
/// 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 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.followers_url.clone().into_inner()]);
send_to_community_followers(announce, self, context).await?;
Ok(())
}
/// For a given community, returns the inboxes of all followers.
async fn get_follower_inboxes(&self, pool: &DbPool) -> Result<Vec<Url>, LemmyError> {
let id = self.id;
let follows = blocking(pool, move |conn| {
CommunityFollowerView::for_community(conn, id)
})
.await??;
let inboxes = follows
.into_iter()
.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())
.collect();
Ok(inboxes)
}
}

View file

@ -0,0 +1,24 @@
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;
/// Generate a unique ID for an activity, in the format:
/// `http(s)://example.com/receive/create/202daf0a-1489-45df-8d2e-c8a3173fed36`
fn generate_activity_id<T>(kind: T) -> Result<Url, ParseError>
where
T: ToString,
{
let id = format!(
"{}/activities/{}/{}",
Settings::get().get_protocol_and_hostname(),
kind.to_string().to_lowercase(),
Uuid::new_v4()
);
Url::parse(&id)
}

View file

@ -0,0 +1,149 @@
use crate::{
activities::send::generate_activity_id,
activity_queue::send_activity_single_dest,
extensions::context::lemmy_context,
ActorType,
};
use activitystreams::{
activity::{
kind::{FollowType, UndoType},
Follow,
Undo,
},
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},
person::Person,
};
use lemmy_utils::LemmyError;
use lemmy_websocket::LemmyContext;
use url::Url;
#[async_trait::async_trait(?Send)]
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> {
self.public_key.to_owned()
}
fn private_key(&self) -> Option<String> {
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()
}
/// 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_owned();
let community = blocking(context.pool(), move |conn| {
Community::read_from_apub_id(conn, &follow_actor_id.into())
})
.await??;
let community_follower_form = CommunityFollowerForm {
community_id: community.id,
person_id: self.id,
pending: true,
};
blocking(&context.pool(), move |conn| {
CommunityFollower::follow(conn, &community_follower_form).ok()
})
.await?;
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());
send_activity_single_dest(follow, self, community.inbox_url.into(), context).await?;
Ok(())
}
async fn send_unfollow(
&self,
follow_actor_id: &Url,
context: &LemmyContext,
) -> Result<(), LemmyError> {
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.into())
})
.await??;
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());
// Undo that fake activity
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());
send_activity_single_dest(undo, self, community.inbox_url.into(), context).await?;
Ok(())
}
async fn send_accept_follow(
&self,
_follow: Follow,
_context: &LemmyContext,
) -> Result<(), LemmyError> {
unimplemented!()
}
async fn send_delete(&self, _context: &LemmyContext) -> Result<(), LemmyError> {
unimplemented!()
}
async fn send_undo_delete(&self, _context: &LemmyContext) -> Result<(), LemmyError> {
unimplemented!()
}
async fn send_remove(&self, _context: &LemmyContext) -> Result<(), LemmyError> {
unimplemented!()
}
async fn send_undo_remove(&self, _context: &LemmyContext) -> Result<(), LemmyError> {
unimplemented!()
}
async fn send_announce(
&self,
_activity: AnyBase,
_context: &LemmyContext,
) -> Result<(), LemmyError> {
unimplemented!()
}
async fn get_follower_inboxes(&self, _pool: &DbPool) -> Result<Vec<Url>, LemmyError> {
unimplemented!()
}
}

View file

@ -0,0 +1,274 @@
use crate::{
activities::send::generate_activity_id,
activity_queue::send_to_community,
extensions::context::lemmy_context,
objects::ToApub,
ActorType,
ApubLikeableType,
ApubObjectType,
};
use activitystreams::{
activity::{
kind::{CreateType, DeleteType, DislikeType, LikeType, RemoveType, UndoType, UpdateType},
Create,
Delete,
Dislike,
Like,
Remove,
Undo,
Update,
},
prelude::*,
public,
};
use lemmy_api_structs::blocking;
use lemmy_db_queries::Crud;
use lemmy_db_schema::source::{community::Community, person::Person, post::Post};
use lemmy_utils::LemmyError;
use lemmy_websocket::LemmyContext;
#[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: &Person, context: &LemmyContext) -> Result<(), LemmyError> {
let page = self.to_apub(context.pool()).await?;
let community_id = self.community_id;
let community = blocking(context.pool(), move |conn| {
Community::read(conn, community_id)
})
.await??;
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()]);
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: &Person, context: &LemmyContext) -> Result<(), LemmyError> {
let page = self.to_apub(context.pool()).await?;
let community_id = self.community_id;
let community = blocking(context.pool(), move |conn| {
Community::read(conn, community_id)
})
.await??;
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()]);
send_to_community(update, creator, &community, context).await?;
Ok(())
}
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().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()]);
send_to_community(delete, creator, &community, context).await?;
Ok(())
}
async fn send_undo_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().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()]);
// 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(public())
.set_many_ccs(vec![community.actor_id()]);
send_to_community(undo, creator, &community, context).await?;
Ok(())
}
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().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()]);
send_to_community(remove, mod_, &community, context).await?;
Ok(())
}
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().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()]);
// Undo that fake activity
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()]);
send_to_community(undo, mod_, &community, context).await?;
Ok(())
}
}
#[async_trait::async_trait(?Send)]
impl ApubLikeableType for Post {
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().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()]);
send_to_community(like, &creator, &community, context).await?;
Ok(())
}
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().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()]);
send_to_community(dislike, &creator, &community, context).await?;
Ok(())
}
async fn send_undo_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().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()]);
// Undo that fake activity
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()]);
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

@ -0,0 +1,320 @@
use crate::{
check_is_apub_id_valid,
extensions::signatures::sign_and_send,
insert_activity,
ActorType,
APUB_JSON_CONTENT_TYPE,
};
use activitystreams::{
base::{BaseExt, Extends, ExtendsExt},
object::AsObject,
};
use anyhow::{anyhow, Context, Error};
use background_jobs::{
create_server,
memory_storage::Storage,
ActixJob,
Backoff,
MaxRetries,
QueueHandle,
WorkerConfig,
};
use itertools::Itertools;
use lemmy_db_queries::DbPool;
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::{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.
///
/// * `activity` the apub activity to be sent
/// * `creator` the local actor which created the activity
/// * `inbox` the inbox url where the activity should be delivered to
pub(crate) async fn send_activity_single_dest<T, Kind>(
activity: T,
creator: &dyn ActorType,
inbox: Url,
context: &LemmyContext,
) -> Result<(), LemmyError>
where
T: AsObject<Kind> + Extends<Kind> + Debug + BaseExt<Kind>,
Kind: Serialize,
<T as Extends<Kind>>::Error: From<serde_json::Error> + Send + Sync + 'static,
{
if check_is_apub_id_valid(&inbox).is_ok() {
debug!(
"Sending activity {:?} to {}",
&activity.id_unchecked(),
&inbox
);
send_activity_internal(
context.activity_queue(),
activity,
creator,
vec![inbox],
context.pool(),
true,
true,
)
.await?;
}
Ok(())
}
/// From a local community, send activity to all remote followers.
///
/// * `activity` the apub activity to send
/// * `community` the sending community
/// * `sender_shared_inbox` in case of an announce, this should be the shared inbox of the inner
/// activities creator, as receiving a known activity will cause an error
pub(crate) async fn send_to_community_followers<T, Kind>(
activity: T,
community: &Community,
context: &LemmyContext,
) -> Result<(), LemmyError>
where
T: AsObject<Kind> + Extends<Kind> + Debug + BaseExt<Kind>,
Kind: Serialize,
<T as Extends<Kind>>::Error: From<serde_json::Error> + Send + Sync + 'static,
{
let follower_inboxes: Vec<Url> = community
.get_follower_inboxes(context.pool())
.await?
.iter()
.unique()
.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().map(|i| i.to_string()),
&community.actor_id
);
send_activity_internal(
context.activity_queue(),
activity,
community,
follower_inboxes,
context.pool(),
true,
false,
)
.await?;
Ok(())
}
/// Sends an activity from a local person to a remote community.
///
/// * `activity` the activity to send
/// * `creator` the creator of the activity
/// * `community` the destination community
///
pub(crate) async fn send_to_community<T, Kind>(
activity: T,
creator: &Person,
community: &Community,
context: &LemmyContext,
) -> Result<(), LemmyError>
where
T: AsObject<Kind> + Extends<Kind> + Debug + BaseExt<Kind>,
Kind: Serialize,
<T as Extends<Kind>>::Error: From<serde_json::Error> + Send + Sync + 'static,
{
// if this is a local community, we need to do an announce from the community instead
if community.local {
community
.send_announce(activity.into_any_base()?, context)
.await?;
} else {
let inbox = community.get_shared_inbox_or_inbox_url();
check_is_apub_id_valid(&inbox)?;
debug!(
"Sending activity {:?} to community {}",
&activity.id_unchecked(),
&community.actor_id
);
send_activity_internal(
context.activity_queue(),
activity,
creator,
vec![inbox],
context.pool(),
true,
false,
)
.await?;
}
Ok(())
}
/// Sends notification to any persons mentioned in a 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: &Person,
mentions: Vec<Url>,
activity: T,
context: &LemmyContext,
) -> Result<(), LemmyError>
where
T: AsObject<Kind> + Extends<Kind> + Debug + BaseExt<Kind>,
Kind: Serialize,
<T as Extends<Kind>>::Error: From<serde_json::Error> + Send + Sync + 'static,
{
debug!(
"Sending mentions activity {:?} to {:?}",
&activity.id_unchecked(),
&mentions
);
let mentions = mentions
.iter()
.filter(|inbox| check_is_apub_id_valid(inbox).is_ok())
.map(|i| i.to_owned())
.collect();
send_activity_internal(
context.activity_queue(),
activity,
creator,
mentions,
context.pool(),
false, // Don't create a new DB row
false,
)
.await?;
Ok(())
}
/// Create new `SendActivityTasks`, which will deliver the given activity to inboxes, as well as
/// handling signing and retrying failed deliveres.
///
/// The caller of this function needs to remove any blocked domains from `to`,
/// using `check_is_apub_id_valid()`.
async fn send_activity_internal<T, Kind>(
activity_sender: &QueueHandle,
activity: T,
actor: &dyn ActorType,
inboxes: Vec<Url>,
pool: &DbPool,
insert_into_db: bool,
sensitive: bool,
) -> Result<(), LemmyError>
where
T: AsObject<Kind> + Extends<Kind> + Debug,
Kind: Serialize,
<T as Extends<Kind>>::Error: From<serde_json::Error> + Send + Sync + 'static,
{
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)?;
// This is necessary because send_comment and send_comment_mentions
// might send the same ap_id
if insert_into_db {
let id = activity.id().context(location_info!())?;
insert_activity(id, activity.clone(), true, sensitive, pool).await?;
}
for i in inboxes {
let message = SendActivityTask {
activity: serialised_activity.to_owned(),
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() {
do_send(message, &Client::default()).await?;
} else {
activity_sender.queue::<SendActivityTask>(message)?;
}
}
Ok(())
}
#[derive(Clone, Debug, Deserialize, Serialize)]
struct SendActivityTask {
activity: String,
inbox: Url,
actor_id: Url,
private_key: String,
}
/// Signs the activity with the sending actor's key, and delivers to the given inbox. Also retries
/// if the delivery failed.
impl ActixJob for SendActivityTask {
type State = MyState;
type Future = Pin<Box<dyn Future<Output = Result<(), Error>>>>;
const NAME: &'static str = "SendActivityTask";
const MAX_RETRIES: MaxRetries = MaxRetries::Count(10);
const BACKOFF: Backoff = Backoff::Exponential(2);
fn run(self, state: Self::State) -> Self::Future {
Box::pin(async move { do_send(self, &state.client).await })
}
}
async fn do_send(task: SendActivityTask, client: &Client) -> Result<(), Error> {
let mut headers = BTreeMap::<String, String>::new();
headers.insert("Content-Type".into(), APUB_JSON_CONTENT_TYPE.to_string());
let result = sign_and_send(
client,
headers,
&task.inbox,
task.activity.clone(),
&task.actor_id,
task.private_key.to_owned(),
)
.await;
if let Err(e) = result {
warn!("{}", e);
return Err(anyhow!(
"Failed to send activity {} to {}",
&task.activity,
task.inbox
));
}
Ok(())
}
pub fn create_activity_queue() -> QueueHandle {
// Start the application server. This guards access to to the jobs store
let queue_handle = create_server(Storage::new());
// Configure and start our workers
WorkerConfig::new(|| MyState {
client: Client::default(),
})
.register::<SendActivityTask>()
.start(queue_handle.clone());
queue_handle
}
#[derive(Clone)]
struct MyState {
pub client: Client,
}

View file

@ -0,0 +1,17 @@
use activitystreams::{base::AnyBase, context};
use lemmy_utils::LemmyError;
use serde_json::json;
pub(crate) fn lemmy_context() -> Result<Vec<AnyBase>, LemmyError> {
let context_ext = AnyBase::from_arbitrary_json(json!(
{
"sc": "http://schema.org#",
"sensitive": "as:sensitive",
"stickied": "as:stickied",
"comments_enabled": {
"kind": "sc:Boolean",
"id": "pt:commentsEnabled"
}
}))?;
Ok(vec![AnyBase::from(context()), context_ext])
}

View file

@ -0,0 +1,38 @@
use activitystreams::unparsed::UnparsedMutExt;
use activitystreams_ext::UnparsedExtension;
use lemmy_utils::LemmyError;
use serde::{Deserialize, Serialize};
/// 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 sensitive: Option<bool>,
}
impl GroupExtension {
pub fn new(sensitive: bool) -> Result<GroupExtension, LemmyError> {
Ok(GroupExtension {
sensitive: Some(sensitive),
})
}
}
impl<U> UnparsedExtension<U> for GroupExtension
where
U: UnparsedMutExt,
{
type Error = serde_json::Error;
fn try_from_unparsed(unparsed_mut: &mut U) -> Result<Self, Self::Error> {
Ok(GroupExtension {
sensitive: unparsed_mut.remove("sensitive")?,
})
}
fn try_into_unparsed(self, unparsed_mut: &mut U) -> Result<(), Self::Error> {
unparsed_mut.insert("sensitive", self.sensitive)?;
Ok(())
}
}

View file

@ -0,0 +1,4 @@
pub(crate) mod context;
pub(crate) mod group_extensions;
pub(crate) mod page_extension;
pub(crate) mod signatures;

View file

@ -2,12 +2,15 @@ use activitystreams::unparsed::UnparsedMutExt;
use activitystreams_ext::UnparsedExtension;
use serde::{Deserialize, Serialize};
/// Activitystreams extension to allow (de)serializing additional Post fields
/// `comemnts_enabled` (called 'locked' in Lemmy),
/// `sensitive` (called 'nsfw') and `stickied`.
#[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

@ -1,36 +1,51 @@
use crate::{apub::ActorType, LemmyError};
use crate::ActorType;
use activitystreams::unparsed::UnparsedMutExt;
use activitystreams_ext::UnparsedExtension;
use actix_web::{client::ClientRequest, HttpRequest};
use actix_web::HttpRequest;
use anyhow::{anyhow, Context};
use http_signature_normalization_actix::{
digest::{DigestClient, SignExt},
Config,
};
use lemmy_utils::location_info;
use http::{header::HeaderName, HeaderMap, HeaderValue};
use http_signature_normalization_actix::Config as ConfigActix;
use http_signature_normalization_reqwest::prelude::{Config, SignExt};
use lemmy_utils::{location_info, LemmyError};
use log::debug;
use openssl::{
hash::MessageDigest,
pkey::PKey,
sign::{Signer, Verifier},
};
use reqwest::{Client, Response};
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use std::{collections::BTreeMap, str::FromStr};
use url::Url;
lazy_static! {
static ref CONFIG2: ConfigActix = ConfigActix::new();
static ref HTTP_SIG_CONFIG: Config = Config::new();
}
/// Signs request headers with the given keypair.
pub async fn sign(
request: ClientRequest,
actor: &dyn ActorType,
/// Creates an HTTP post request to `inbox_url`, with the given `client` and `headers`, and
/// `activity` as request body. The request is signed with `private_key` and then sent.
pub async fn sign_and_send(
client: &Client,
headers: BTreeMap<String, String>,
inbox_url: &Url,
activity: String,
) -> Result<DigestClient<String>, LemmyError> {
let signing_key_id = format!("{}#main-key", actor.actor_id()?);
let private_key = actor.private_key().context(location_info!())?;
actor_id: &Url,
private_key: String,
) -> Result<Response, LemmyError> {
let signing_key_id = format!("{}#main-key", actor_id);
let digest_client = request
let mut header_map = HeaderMap::new();
for h in headers {
header_map.insert(
HeaderName::from_str(h.0.as_str())?,
HeaderValue::from_str(h.1.as_str())?,
);
}
let response = client
.post(&inbox_url.to_string())
.headers(header_map)
.signature_with_digest(
HTTP_SIG_CONFIG.clone(),
signing_key_id,
@ -46,12 +61,16 @@ pub async fn sign(
)
.await?;
Ok(digest_client)
Ok(response)
}
pub fn verify(request: &HttpRequest, actor: &dyn ActorType) -> Result<(), LemmyError> {
/// Verifies the HTTP signature on an incoming inbox request.
pub(crate) fn verify_signature(
request: &HttpRequest,
actor: &dyn ActorType,
) -> Result<(), LemmyError> {
let public_key = actor.public_key().context(location_info!())?;
let verified = HTTP_SIG_CONFIG
let verified = CONFIG2
.begin_verify(
request.method(),
request.uri().path_and_query(),
@ -76,23 +95,23 @@ pub fn verify(request: &HttpRequest, actor: &dyn ActorType) -> Result<(), LemmyE
}
}
// The following is taken from here:
// https://docs.rs/activitystreams/0.5.0-alpha.17/activitystreams/ext/index.html
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct PublicKey {
pub id: String,
pub owner: String,
pub public_key_pem: String,
}
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
/// 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, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct PublicKeyExtension {
pub public_key: PublicKey,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct PublicKey {
pub id: String,
pub owner: Url,
pub public_key_pem: String,
}
impl PublicKey {
pub fn to_ext(&self) -> PublicKeyExtension {
PublicKeyExtension {

View file

@ -0,0 +1,145 @@
use crate::{
fetcher::{
fetch::fetch_remote_object,
get_or_fetch_and_upsert_person,
is_deleted,
should_refetch_actor,
},
inbox::person_inbox::receive_announce,
objects::FromApub,
GroupExt,
};
use activitystreams::{
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};
use lemmy_utils::{location_info, LemmyError};
use lemmy_websocket::LemmyContext;
use log::debug;
use url::Url;
/// Get a community 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_community(
apub_id: &Url,
context: &LemmyContext,
recursion_counter: &mut i32,
) -> 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.into())
})
.await?;
match community {
Ok(c) if !c.local && should_refetch_actor(c.last_refreshed_at) => {
debug!("Fetching and updating from remote community: {}", apub_id);
fetch_remote_community(apub_id, context, Some(c), recursion_counter).await
}
Ok(c) => Ok(c),
Err(NotFound {}) => {
debug!("Fetching and creating remote community: {}", apub_id);
fetch_remote_community(apub_id, context, None, recursion_counter).await
}
Err(e) => Err(e.into()),
}
}
/// Request a community by apub ID from a remote instance, including moderators. If `old_community`,
/// is set, this is an update for a community which is already known locally. If not, we don't know
/// the community yet and also pull the outbox, to get some initial posts.
async fn fetch_remote_community(
apub_id: &Url,
context: &LemmyContext,
old_community: Option<Community>,
recursion_counter: &mut i32,
) -> Result<Community, LemmyError> {
let group = fetch_remote_object::<GroupExt>(context.client(), apub_id, recursion_counter).await;
if let Some(c) = old_community.to_owned() {
if is_deleted(&group) {
blocking(context.pool(), move |conn| {
Community::update_deleted(conn, c.id, true)
})
.await??;
} else if group.is_err() {
// If fetching failed, return the existing data.
return Ok(c);
}
}
let group = group?;
let community =
Community::from_apub(&group, context, apub_id.to_owned(), recursion_counter).await?;
// Also add the community moderators too
let attributed_to = group.inner.attributed_to().context(location_info!())?;
let creator_and_moderator_uris: Vec<&Url> = attributed_to
.as_many()
.context(location_info!())?
.iter()
.map(|a| a.as_xsd_any_uri().context(""))
.collect::<Result<Vec<&Url>, anyhow::Error>>()?;
let mut creator_and_moderators = Vec::new();
for uri in creator_and_moderator_uris {
let c_or_m = get_or_fetch_and_upsert_person(uri, context, recursion_counter).await?;
creator_and_moderators.push(c_or_m);
}
// TODO: need to make this work to update mods of existing communities
if old_community.is_none() {
let community_id = community.id;
blocking(context.pool(), move |conn| {
for mod_ in creator_and_moderators {
let community_moderator_form = CommunityModeratorForm {
community_id,
person_id: mod_.id,
};
CommunityModerator::join(conn, &community_moderator_form)?;
}
Ok(()) as Result<(), LemmyError>
})
.await??;
}
// 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

@ -0,0 +1,83 @@
use crate::{check_is_apub_id_valid, APUB_JSON_CONTENT_TYPE};
use anyhow::anyhow;
use lemmy_utils::{request::retry, LemmyError};
use reqwest::{Client, StatusCode};
use serde::Deserialize;
use std::time::Duration;
use thiserror::Error;
use url::Url;
/// Maximum number of HTTP requests allowed to handle a single incoming activity (or a single object
/// fetch through the search).
///
/// 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 {
pub inner: anyhow::Error,
pub status_code: Option<StatusCode>,
}
impl From<LemmyError> for FetchError {
fn from(t: LemmyError) -> Self {
FetchError {
inner: t.inner,
status_code: None,
}
}
}
impl From<reqwest::Error> for FetchError {
fn from(t: reqwest::Error) -> Self {
let status = t.status();
FetchError {
inner: t.into(),
status_code: status,
}
}
}
impl std::fmt::Display for FetchError {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
std::fmt::Display::fmt(&self, f)
}
}
/// Fetch any type of ActivityPub object, handling things like HTTP headers, deserialisation,
/// timeouts etc.
pub(in crate::fetcher) async fn fetch_remote_object<Response>(
client: &Client,
url: &Url,
recursion_counter: &mut i32,
) -> Result<Response, FetchError>
where
Response: for<'de> Deserialize<'de> + std::fmt::Debug,
{
*recursion_counter += 1;
if *recursion_counter > MAX_REQUEST_NUMBER {
return Err(LemmyError::from(anyhow!("Maximum recursion depth reached")).into());
}
check_is_apub_id_valid(&url)?;
let timeout = Duration::from_secs(60);
let res = retry(|| {
client
.get(url.as_str())
.header("Accept", APUB_JSON_CONTENT_TYPE)
.timeout(timeout)
.send()
})
.await?;
if res.status() == StatusCode::GONE {
return Err(FetchError {
inner: anyhow!("Remote object {} was deleted", url),
status_code: Some(res.status()),
});
}
Ok(res.json().await?)
}

View file

@ -0,0 +1,72 @@
pub(crate) mod community;
mod fetch;
pub(crate) mod objects;
pub(crate) mod person;
pub mod search;
use crate::{
fetcher::{
community::get_or_fetch_and_upsert_community,
fetch::FetchError,
person::get_or_fetch_and_upsert_person,
},
ActorType,
};
use chrono::NaiveDateTime;
use http::StatusCode;
use lemmy_db_schema::naive_now;
use lemmy_utils::LemmyError;
use lemmy_websocket::LemmyContext;
use serde::Deserialize;
use url::Url;
static ACTOR_REFETCH_INTERVAL_SECONDS: i64 = 24 * 60 * 60;
static ACTOR_REFETCH_INTERVAL_SECONDS_DEBUG: i64 = 10;
fn is_deleted<Response>(fetch_response: &Result<Response, FetchError>) -> bool
where
Response: for<'de> Deserialize<'de>,
{
if let Err(e) = fetch_response {
if let Some(status) = e.status_code {
if status == StatusCode::GONE {
return true;
}
}
}
false
}
/// 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.
pub(crate) async fn get_or_fetch_and_upsert_actor(
apub_id: &Url,
context: &LemmyContext,
recursion_counter: &mut i32,
) -> Result<Box<dyn ActorType>, LemmyError> {
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_person(apub_id, context, recursion_counter).await?),
};
Ok(actor)
}
/// Determines when a remote actor should be refetched from its instance. In release builds, this is
/// `ACTOR_REFETCH_INTERVAL_SECONDS` after the last refetch, in debug builds
/// `ACTOR_REFETCH_INTERVAL_SECONDS_DEBUG`.
///
/// TODO it won't pick up new avatars, summaries etc until a day after.
/// Actors need an "update" activity pushed to other servers to fix this.
fn should_refetch_actor(last_refreshed: NaiveDateTime) -> bool {
let update_interval = if cfg!(debug_assertions) {
// avoid infinite loop when fetching community outbox
chrono::Duration::seconds(ACTOR_REFETCH_INTERVAL_SECONDS_DEBUG)
} else {
chrono::Duration::seconds(ACTOR_REFETCH_INTERVAL_SECONDS)
};
last_refreshed.lt(&(naive_now() - update_interval))
}

View file

@ -0,0 +1,83 @@
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_utils::LemmyError;
use lemmy_websocket::LemmyContext;
use log::debug;
use url::Url;
/// Gets a post by its apub ID. If it exists locally, it is returned directly. Otherwise it is
/// pulled from its apub ID, inserted and returned.
///
/// The parent community is also pulled if necessary. Comments are not pulled.
pub(crate) async fn get_or_fetch_and_insert_post(
post_ap_id: &Url,
context: &LemmyContext,
recursion_counter: &mut i32,
) -> 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.into())
})
.await?;
match post {
Ok(p) => Ok(p),
Err(NotFound {}) => {
debug!("Fetching and creating remote post: {}", post_ap_id);
let page =
fetch_remote_object::<PageExt>(context.client(), post_ap_id, recursion_counter).await?;
let post = Post::from_apub(&page, context, post_ap_id.to_owned(), recursion_counter).await?;
Ok(post)
}
Err(e) => Err(e.into()),
}
}
/// Gets a comment by its apub ID. If it exists locally, it is returned directly. Otherwise it is
/// pulled from its apub ID, inserted and returned.
///
/// The parent community, post and comment are also pulled if necessary.
pub(crate) async fn get_or_fetch_and_insert_comment(
comment_ap_id: &Url,
context: &LemmyContext,
recursion_counter: &mut i32,
) -> 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.into())
})
.await?;
match comment {
Ok(p) => Ok(p),
Err(NotFound {}) => {
debug!(
"Fetching and creating remote comment and its parents: {}",
comment_ap_id
);
let comment =
fetch_remote_object::<NoteExt>(context.client(), comment_ap_id, recursion_counter).await?;
let comment = Comment::from_apub(
&comment,
context,
comment_ap_id.to_owned(),
recursion_counter,
)
.await?;
let post_id = comment.post_id;
let post = blocking(context.pool(), move |conn| Post::read(conn, post_id)).await??;
if post.locked {
return Err(anyhow!("Post is locked").into());
}
Ok(comment)
}
Err(e) => Err(e.into()),
}
}

View file

@ -0,0 +1,73 @@
use crate::{
fetcher::{fetch::fetch_remote_object, is_deleted, should_refetch_actor},
objects::FromApub,
PersonExt,
};
use anyhow::anyhow;
use diesel::result::Error::NotFound;
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 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_person(
apub_id: &Url,
context: &LemmyContext,
recursion_counter: &mut i32,
) -> Result<Person, LemmyError> {
let apub_id_owned = apub_id.to_owned();
let person = blocking(context.pool(), move |conn| {
Person::read_from_apub_id(conn, &apub_id_owned.into())
})
.await?;
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 person: {}", apub_id);
let person =
fetch_remote_object::<PersonExt>(context.client(), apub_id, recursion_counter).await;
if is_deleted(&person) {
// TODO: use Person::update_deleted() once implemented
blocking(context.pool(), move |conn| {
Person::delete_account(conn, u.id)
})
.await??;
return Err(anyhow!("Person was deleted by remote instance").into());
} else if person.is_err() {
return Ok(u);
}
let person =
Person::from_apub(&person?, context, apub_id.to_owned(), recursion_counter).await?;
let person_id = person.id;
blocking(context.pool(), move |conn| {
Person::mark_as_updated(conn, person_id)
})
.await??;
Ok(person)
}
Ok(u) => Ok(u),
Err(NotFound {}) => {
debug!("Fetching and creating remote person: {}", apub_id);
let person =
fetch_remote_object::<PersonExt>(context.client(), apub_id, recursion_counter).await?;
let person =
Person::from_apub(&person, context, apub_id.to_owned(), recursion_counter).await?;
Ok(person)
}
Err(e) => Err(e.into()),
}
}

View file

@ -0,0 +1,206 @@
use crate::{
fetcher::{
fetch::fetch_remote_object,
get_or_fetch_and_upsert_community,
get_or_fetch_and_upsert_person,
is_deleted,
},
find_object_by_id,
objects::FromApub,
GroupExt,
NoteExt,
Object,
PageExt,
PersonExt,
};
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_,
},
SearchType,
};
use lemmy_db_schema::source::{
comment::Comment,
community::Community,
person::Person,
post::Post,
private_message::PrivateMessage,
};
use lemmy_db_views::{comment_view::CommentView, post_view::PostView};
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.
#[derive(serde::Deserialize, Debug)]
#[serde(untagged)]
enum SearchAcceptedObjects {
Person(Box<PersonExt>),
Group(Box<GroupExt>),
Page(Box<PageExt>),
Comment(Box<NoteExt>),
}
/// Attempt to parse the query as URL, and fetch an ActivityPub object from it.
///
/// Some working examples for use with the `docker/federation/` setup:
/// http://lemmy_alpha:8541/c/main, or !main@lemmy_alpha:8541
/// http://lemmy_beta:8551/u/lemmy_alpha, or @lemmy_beta@lemmy_beta:8551
/// http://lemmy_gamma:8561/post/3
/// http://lemmy_delta:8571/comment/2
pub async fn search_by_apub_id(
query: &str,
context: &LemmyContext,
) -> Result<SearchResponse, LemmyError> {
// Parse the shorthand query url
let query_url = if query.contains('@') {
debug!("Search for {}", query);
let split = query.split('@').collect::<Vec<&str>>();
// 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])
} else if split.len() == 2 {
if split[0].contains('!') {
let split2 = split[0].split('!').collect::<Vec<&str>>();
(format!("/c/{}", split2[1]), split[1])
} else {
return Err(anyhow!("Invalid search query: {}", query).into());
}
} else {
return Err(anyhow!("Invalid search query: {}", query).into());
};
let url = format!(
"{}://{}{}",
Settings::get().get_protocol_string(),
instance,
name
);
Url::parse(&url)?
} else {
Url::parse(&query)?
};
let recursion_counter = &mut 0;
let fetch_response =
fetch_remote_object::<SearchAcceptedObjects>(context.client(), &query_url, recursion_counter)
.await;
if is_deleted(&fetch_response) {
delete_object_locally(&query_url, 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(
fetch_response: SearchAcceptedObjects,
query_url: Url,
recursion_counter: &mut i32,
context: &LemmyContext,
) -> Result<SearchResponse, LemmyError> {
let domain = query_url.domain().context("url has no domain")?;
let mut response = SearchResponse {
type_: SearchType::All.to_string(),
comments: vec![],
posts: vec![],
communities: vec![],
users: vec![],
};
match fetch_response {
SearchAcceptedObjects::Person(p) => {
let person_uri = p.inner.id(domain)?.context("person has no id")?;
let person = get_or_fetch_and_upsert_person(&person_uri, context, recursion_counter).await?;
response.users = vec![
blocking(context.pool(), move |conn| {
PersonViewSafe::read(conn, person.id)
})
.await??,
];
}
SearchAcceptedObjects::Group(g) => {
let community_uri = g.inner.id(domain)?.context("group has no id")?;
let community =
get_or_fetch_and_upsert_community(community_uri, context, recursion_counter).await?;
response.communities = vec![
blocking(context.pool(), move |conn| {
CommunityView::read(conn, community.id, None)
})
.await??,
];
}
SearchAcceptedObjects::Page(p) => {
let p = Post::from_apub(&p, context, query_url, recursion_counter).await?;
response.posts =
vec![blocking(context.pool(), move |conn| PostView::read(conn, p.id, None)).await??];
}
SearchAcceptedObjects::Comment(c) => {
let c = Comment::from_apub(&c, context, query_url, recursion_counter).await?;
response.comments = vec![
blocking(context.pool(), move |conn| {
CommentView::read(conn, c.id, None)
})
.await??,
];
}
};
Ok(response)
}
async fn delete_object_locally(query_url: &Url, context: &LemmyContext) -> Result<(), LemmyError> {
let res = find_object_by_id(context, query_url.to_owned()).await?;
match res {
Object::Comment(c) => {
blocking(context.pool(), move |conn| {
Comment::update_deleted(conn, c.id, true)
})
.await??;
}
Object::Post(p) => {
blocking(context.pool(), move |conn| {
Post::update_deleted(conn, p.id, true)
})
.await??;
}
Object::Person(u) => {
// TODO: implement update_deleted() for user, move it to ApubObject trait
blocking(context.pool(), move |conn| {
Person::delete_account(conn, u.id)
})
.await??;
}
Object::Community(c) => {
blocking(context.pool(), move |conn| {
Community::update_deleted(conn, c.id, true)
})
.await??;
}
Object::PrivateMessage(pm) => {
blocking(context.pool(), move |conn| {
PrivateMessage::update_deleted(conn, pm.id, true)
})
.await??;
}
}
Err(anyhow!("Object was deleted").into())
}

View file

@ -0,0 +1,37 @@
use crate::{
http::{create_apub_response, create_apub_tombstone_response},
objects::ToApub,
};
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, CommentId};
use lemmy_utils::LemmyError;
use lemmy_websocket::LemmyContext;
use serde::Deserialize;
#[derive(Deserialize)]
pub struct CommentQuery {
comment_id: String,
}
/// Return the ActivityPub json representation of a local comment over HTTP.
pub async fn get_apub_comment(
info: Path<CommentQuery>,
context: web::Data<LemmyContext>,
) -> Result<HttpResponse<Body>, LemmyError> {
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());
}
if !comment.deleted {
Ok(create_apub_response(
&comment.to_apub(context.pool()).await?,
))
} else {
Ok(create_apub_tombstone_response(&comment.to_tombstone()?))
}
}

View file

@ -0,0 +1,113 @@
use crate::{
extensions::context::lemmy_context,
http::{create_apub_response, create_apub_tombstone_response},
objects::ToApub,
ActorType,
};
use activitystreams::{
base::{AnyBase, BaseExt},
collection::{CollectionExt, OrderedCollection, UnorderedCollection},
};
use actix_web::{body::Body, web, HttpResponse};
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_utils::LemmyError;
use lemmy_websocket::LemmyContext;
use serde::Deserialize;
#[derive(Deserialize)]
pub struct CommunityQuery {
community_name: String,
}
/// Return the ActivityPub json representation of a local community over HTTP.
pub async fn get_apub_community_http(
info: web::Path<CommunityQuery>,
context: web::Data<LemmyContext>,
) -> Result<HttpResponse<Body>, LemmyError> {
let community = blocking(context.pool(), move |conn| {
Community::read_from_name(conn, &info.community_name)
})
.await??;
if !community.deleted {
let apub = community.to_apub(context.pool()).await?;
Ok(create_apub_response(&apub))
} else {
Ok(create_apub_tombstone_response(&community.to_tombstone()?))
}
}
/// Returns an empty followers collection, only populating the size (for privacy).
pub async fn get_apub_community_followers(
info: web::Path<CommunityQuery>,
context: web::Data<LemmyContext>,
) -> Result<HttpResponse<Body>, LemmyError> {
let community = blocking(context.pool(), move |conn| {
Community::read_from_name(&conn, &info.community_name)
})
.await??;
let community_id = community.id;
let community_followers = blocking(context.pool(), move |conn| {
CommunityFollowerView::for_community(&conn, community_id)
})
.await??;
let mut collection = UnorderedCollection::new();
collection
.set_many_contexts(lemmy_context()?)
.set_id(community.followers_url.into())
.set_total_items(community_followers.len() as u64);
Ok(create_apub_response(&collection))
}
/// Returns the community outbox, which is populated by a maximum of 20 posts (but no other
/// activites like votes or comments).
pub async fn get_apub_community_outbox(
info: web::Path<CommunityQuery>,
context: web::Data<LemmyContext>,
) -> Result<HttpResponse<Body>, LemmyError> {
let community = blocking(context.pool(), move |conn| {
Community::read_from_name(&conn, &info.community_name)
})
.await??;
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 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(activities)
.set_many_contexts(lemmy_context()?)
.set_id(community.get_outbox_url()?)
.set_total_items(len as u64);
Ok(create_apub_response(&collection))
}
pub async fn get_apub_community_inbox(
info: web::Path<CommunityQuery>,
context: web::Data<LemmyContext>,
) -> Result<HttpResponse<Body>, LemmyError> {
let community = blocking(context.pool(), move |conn| {
Community::read_from_name(&conn, &info.community_name)
})
.await??;
let mut collection = OrderedCollection::new();
collection
.set_id(format!("{}/inbox", community.actor_id).parse()?)
.set_many_contexts(lemmy_context()?);
Ok(create_apub_response(&collection))
}

View file

@ -0,0 +1,68 @@
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_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;
/// Convert the data to json and turn it into an HTTP Response with the correct ActivityPub
/// headers.
fn create_apub_response<T>(data: &T) -> HttpResponse<Body>
where
T: Serialize,
{
HttpResponse::Ok()
.content_type(APUB_JSON_CONTENT_TYPE)
.json(data)
}
fn create_apub_tombstone_response<T>(data: &T) -> HttpResponse<Body>
where
T: Serialize,
{
HttpResponse::Gone()
.content_type(APUB_JSON_CONTENT_TYPE)
.status(StatusCode::GONE)
.json(data)
}
#[derive(Deserialize)]
pub struct CommunityQuery {
type_: String,
id: String,
}
/// Return the ActivityPub json representation of a local community over HTTP.
pub async fn get_activity(
info: web::Path<CommunityQuery>,
context: web::Data<LemmyContext>,
) -> Result<HttpResponse<Body>, LemmyError> {
let settings = Settings::get();
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)
})
.await??;
let sensitive = activity.sensitive.unwrap_or(true);
if !activity.local || sensitive {
Ok(HttpResponse::NotFound().finish())
} else {
Ok(create_apub_response(&activity.data))
}
}

View file

@ -0,0 +1,78 @@
use crate::{
extensions::context::lemmy_context,
http::{create_apub_response, create_apub_tombstone_response},
objects::ToApub,
ActorType,
};
use activitystreams::{
base::BaseExt,
collection::{CollectionExt, OrderedCollection},
};
use actix_web::{body::Body, web, HttpResponse};
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 PersonQuery {
user_name: String,
}
/// 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 persons, so that it can send tombstones
let person = blocking(context.pool(), move |conn| {
Person::find_by_name(conn, &user_name)
})
.await??;
if !person.deleted {
let apub = person.to_apub(context.pool()).await?;
Ok(create_apub_response(&apub))
} else {
Ok(create_apub_tombstone_response(&person.to_tombstone()?))
}
}
pub async fn get_apub_person_outbox(
info: web::Path<PersonQuery>,
context: web::Data<LemmyContext>,
) -> Result<HttpResponse<Body>, LemmyError> {
let person = blocking(context.pool(), move |conn| {
Person::find_by_name(&conn, &info.user_name)
})
.await??;
// TODO: populate the person outbox
let mut collection = OrderedCollection::new();
collection
.set_many_items(Vec::<Url>::new())
.set_many_contexts(lemmy_context()?)
.set_id(person.get_outbox_url()?)
.set_total_items(0_u64);
Ok(create_apub_response(&collection))
}
pub async fn get_apub_person_inbox(
info: web::Path<PersonQuery>,
context: web::Data<LemmyContext>,
) -> Result<HttpResponse<Body>, LemmyError> {
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", person.actor_id.into_inner()).parse()?)
.set_many_contexts(lemmy_context()?);
Ok(create_apub_response(&collection))
}

View file

@ -0,0 +1,35 @@
use crate::{
http::{create_apub_response, create_apub_tombstone_response},
objects::ToApub,
};
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, PostId};
use lemmy_utils::LemmyError;
use lemmy_websocket::LemmyContext;
use serde::Deserialize;
#[derive(Deserialize)]
pub struct PostQuery {
post_id: String,
}
/// Return the ActivityPub json representation of a local post over HTTP.
pub async fn get_apub_post(
info: web::Path<PostQuery>,
context: web::Data<LemmyContext>,
) -> Result<HttpResponse<Body>, LemmyError> {
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());
}
if !post.deleted {
Ok(create_apub_response(&post.to_apub(context.pool()).await?))
} else {
Ok(create_apub_tombstone_response(&post.to_tombstone()?))
}
}

View file

@ -0,0 +1,281 @@
use crate::{
activities::receive::verify_activity_domains_valid,
inbox::{
assert_activity_not_local,
get_activity_id,
get_activity_to_and_cc,
inbox_verify_http_signature,
is_activity_already_known,
is_addressed_to_public,
receive_for_community::{
receive_create_for_community,
receive_delete_for_community,
receive_dislike_for_community,
receive_like_for_community,
receive_undo_for_community,
receive_update_for_community,
},
},
insert_activity,
ActorType,
};
use activitystreams::{
activity::{kind::FollowType, ActorAndObject, Follow, Undo},
base::AnyBase,
prelude::*,
};
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::{
community::{Community, CommunityFollower, CommunityFollowerForm},
person::Person,
},
CommunityId,
};
use lemmy_db_views_actor::community_person_ban_view::CommunityPersonBanView;
use lemmy_utils::{location_info, LemmyError};
use lemmy_websocket::LemmyContext;
use log::info;
use serde::{Deserialize, Serialize};
use std::fmt::Debug;
use url::Url;
/// Allowed activities for community inbox.
#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd, Deserialize, Serialize)]
#[serde(rename_all = "PascalCase")]
pub enum CommunityValidTypes {
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
Dislike, // downvote post or comment
Delete, // post or comment deleted by creator
Remove, // post or comment removed by mod or admin
}
pub type CommunityAcceptedActivities = ActorAndObject<CommunityValidTypes>;
/// Handler for all incoming receive to community inboxes.
pub async fn community_inbox(
request: HttpRequest,
input: web::Json<CommunityAcceptedActivities>,
path: web::Path<String>,
context: web::Data<LemmyContext>,
) -> Result<HttpResponse, LemmyError> {
let activity = input.into_inner();
// First of all check the http signature
let request_counter = &mut 0;
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())?;
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 path = path.into_inner();
let community = blocking(&context.pool(), move |conn| {
Community::read_from_name(&conn, &path)
})
.await??;
let to_and_cc = get_activity_to_and_cc(&activity);
if !to_and_cc.contains(&&community.actor_id()) {
return Err(anyhow!("Activity delivered to wrong community").into());
}
assert_activity_not_local(&activity)?;
insert_activity(&activity_id, activity.clone(), false, true, context.pool()).await?;
info!(
"Community {} received activity {:?} from {}",
community.name,
&activity.id_unchecked(),
&actor.actor_id()
);
community_receive_message(
activity.clone(),
community.clone(),
actor.as_ref(),
&context,
request_counter,
)
.await
}
/// Receives Follow, Undo/Follow, post actions, comment actions (including votes)
pub(crate) async fn community_receive_message(
activity: CommunityAcceptedActivities,
to_community: Community,
actor: &dyn ActorType,
context: &LemmyContext,
request_counter: &mut i32,
) -> Result<HttpResponse, LemmyError> {
// Only persons can send activities to the community, so we can get the actor as person
// unconditionally.
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(&person, to_community.id, context.pool()).await?;
let any_base = activity.clone().into_any_base()?;
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(), person, &to_community, &context).await?;
false
}
CommunityValidTypes::Undo => {
handle_undo(
context,
activity.clone(),
actor_url,
&to_community,
request_counter,
)
.await?
}
CommunityValidTypes::Create => {
receive_create_for_community(context, any_base.clone(), &actor_url, request_counter).await?;
true
}
CommunityValidTypes::Update => {
receive_update_for_community(context, any_base.clone(), &actor_url, request_counter).await?;
true
}
CommunityValidTypes::Like => {
receive_like_for_community(context, any_base.clone(), &actor_url, request_counter).await?;
true
}
CommunityValidTypes::Dislike => {
receive_dislike_for_community(context, any_base.clone(), &actor_url, request_counter).await?;
true
}
CommunityValidTypes::Delete => {
receive_delete_for_community(context, any_base.clone(), &actor_url).await?;
true
}
CommunityValidTypes::Remove => {
// TODO: we dont support remote mods, so this is ignored for now
//receive_remove_for_community(context, any_base.clone(), &person_url).await?
false
}
};
if do_announce {
// Check again that the activity is public, just to be sure
is_addressed_to_public(&activity)?;
to_community
.send_announce(activity.into_any_base()?, context)
.await?;
}
Ok(HttpResponse::Ok().finish())
}
/// Handle a follow request from a remote person, adding the person as follower and returning an
/// Accept activity.
async fn handle_follow(
activity: AnyBase,
person: Person,
community: &Community,
context: &LemmyContext,
) -> Result<HttpResponse, LemmyError> {
let follow = Follow::from_any_base(activity)?.context(location_info!())?;
verify_activity_domains_valid(&follow, &person.actor_id(), false)?;
let community_follower_form = CommunityFollowerForm {
community_id: community.id,
person_id: person.id,
pending: false,
};
// This will fail if they're already a follower, but ignore the error.
blocking(&context.pool(), move |conn| {
CommunityFollower::follow(&conn, &community_follower_form).ok()
})
.await?;
community.send_accept_follow(follow, context).await?;
Ok(HttpResponse::Ok().finish())
}
async fn handle_undo(
context: &LemmyContext,
activity: CommunityAcceptedActivities,
actor_url: Url,
to_community: &Community,
request_counter: &mut i32,
) -> Result<bool, LemmyError> {
let inner_kind = activity
.object()
.is_single_kind(&FollowType::Follow.to_string());
let any_base = activity.into_any_base()?;
if inner_kind {
handle_undo_follow(any_base, actor_url, to_community, &context).await?;
Ok(false)
} else {
receive_undo_for_community(context, any_base, &actor_url, request_counter).await?;
Ok(true)
}
}
/// Handle `Undo/Follow` from a person, removing the person from followers list.
async fn handle_undo_follow(
activity: AnyBase,
person_url: Url,
community: &Community,
context: &LemmyContext,
) -> Result<(), LemmyError> {
let undo = Undo::from_any_base(activity)?.context(location_info!())?;
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, &person_url, false)?;
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,
person_id: person.id,
pending: false,
};
// This will fail if they aren't a follower, but ignore the error.
blocking(&context.pool(), move |conn| {
CommunityFollower::unfollow(&conn, &community_follower_form).ok()
})
.await?;
Ok(())
}
pub(crate) async fn check_community_or_site_ban(
person: &Person,
community_id: CommunityId,
pool: &DbPool,
) -> Result<(), LemmyError> {
if person.banned {
return Err(anyhow!("Person is banned from site").into());
}
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!("Person is banned from community").into());
}
Ok(())
}

View file

@ -0,0 +1,180 @@
use crate::{
check_is_apub_id_valid,
extensions::signatures::verify_signature,
fetcher::get_or_fetch_and_upsert_actor,
ActorType,
};
use activitystreams::{
activity::ActorAndObjectRefExt,
base::{AsBase, BaseExt, Extends},
object::{AsObject, ObjectExt},
public,
};
use actix_web::HttpRequest;
use anyhow::{anyhow, Context};
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::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(crate) fn get_activity_id<T, Kind>(activity: &T, creator_uri: &Url) -> Result<Url, LemmyError>
where
T: BaseExt<Kind> + Extends<Kind> + Debug,
Kind: Serialize,
<T as Extends<Kind>>::Error: From<serde_json::Error> + Send + Sync + 'static,
{
let creator_domain = creator_uri.host_str().context(location_info!())?;
let activity_id = activity.id(creator_domain)?;
Ok(activity_id.context(location_info!())?.to_owned())
}
pub(crate) async fn is_activity_already_known(
pool: &DbPool,
activity_id: &Url,
) -> Result<bool, LemmyError> {
let activity_id = activity_id.to_owned().into();
let existing = blocking(pool, move |conn| {
Activity::read_from_apub_id(&conn, &activity_id)
})
.await?;
match existing {
Ok(_) => Ok(true),
Err(_) => Ok(false),
}
}
pub(crate) fn get_activity_to_and_cc<T, Kind>(activity: &T) -> Vec<Url>
where
T: AsBase<Kind> + AsObject<Kind> + ActorAndObjectRefExt,
{
let mut to_and_cc = vec![];
if let Some(to) = activity.to() {
let to = to.to_owned().unwrap_to_vec();
let mut to = to
.iter()
.map(|t| t.as_xsd_any_uri())
.flatten()
.map(|t| t.to_owned())
.collect();
to_and_cc.append(&mut to);
}
if let Some(cc) = activity.cc() {
let cc = cc.to_owned().unwrap_to_vec();
let mut cc = cc
.iter()
.map(|c| c.as_xsd_any_uri())
.flatten()
.map(|c| c.to_owned())
.collect();
to_and_cc.append(&mut cc);
}
to_and_cc
}
pub(crate) fn is_addressed_to_public<T, Kind>(activity: &T) -> Result<(), LemmyError>
where
T: AsBase<Kind> + AsObject<Kind> + ActorAndObjectRefExt,
{
let to_and_cc = get_activity_to_and_cc(activity);
if to_and_cc.contains(&public()) {
Ok(())
} else {
Err(anyhow!("Activity is not addressed to public").into())
}
}
pub(crate) async fn inbox_verify_http_signature<T, Kind>(
activity: &T,
context: &LemmyContext,
request: HttpRequest,
request_counter: &mut i32,
) -> Result<Box<dyn ActorType>, LemmyError>
where
T: AsObject<Kind> + ActorAndObjectRefExt + Extends<Kind> + AsBase<Kind>,
Kind: Serialize,
<T as Extends<Kind>>::Error: From<serde_json::Error> + Send + Sync + 'static,
{
let actor_id = activity
.actor()?
.to_owned()
.single_xsd_any_uri()
.context(location_info!())?;
check_is_apub_id_valid(&actor_id)?;
let actor = get_or_fetch_and_upsert_actor(&actor_id, &context, request_counter).await?;
verify_signature(&request, actor.as_ref())?;
Ok(actor)
}
/// Returns true if `to_and_cc` contains at least one 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_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);
}
}
}
Ok(false)
}
/// If `to_and_cc` contains the followers collection of a remote community, returns this community
/// (like `https://example.com/c/main/followers`)
pub(crate) async fn is_addressed_to_community_followers(
to_and_cc: &[Url],
pool: &DbPool,
) -> Result<Option<Community>, LemmyError> {
for url in to_and_cc {
let url = url.to_owned().into();
let community = blocking(&pool, move |conn| {
// ignore errors here, because the current url might not actually be a followers url
Community::read_from_followers_url(&conn, &url).ok()
})
.await?;
if let Some(c) = community {
if !c.local {
return Ok(Some(c));
}
}
}
Ok(None)
}
pub(in crate::inbox) fn assert_activity_not_local<T, Kind>(activity: &T) -> Result<(), LemmyError>
where
T: BaseExt<Kind> + Debug,
{
let id = activity.id_unchecked().context(location_info!())?;
let activity_domain = id.domain().context(location_info!())?;
if activity_domain == Settings::get().hostname() {
return Err(
anyhow!(
"Error: received activity which was sent by local instance: {:?}",
activity
)
.into(),
);
}
Ok(())
}

View file

@ -0,0 +1,420 @@
use crate::{
activities::receive::{
comment::{receive_create_comment, receive_update_comment},
community::{
receive_delete_community,
receive_remove_community,
receive_undo_delete_community,
receive_undo_remove_community,
},
private_message::{
receive_create_private_message,
receive_delete_private_message,
receive_undo_delete_private_message,
receive_update_private_message,
},
receive_unhandled_activity,
verify_activity_domains_valid,
},
check_is_apub_id_valid,
fetcher::community::get_or_fetch_and_upsert_community,
inbox::{
assert_activity_not_local,
get_activity_id,
get_activity_to_and_cc,
inbox_verify_http_signature,
is_activity_already_known,
is_addressed_to_community_followers,
is_addressed_to_local_person,
is_addressed_to_public,
receive_for_community::{
receive_create_for_community,
receive_delete_for_community,
receive_dislike_for_community,
receive_like_for_community,
receive_remove_for_community,
receive_undo_for_community,
receive_update_for_community,
},
},
insert_activity,
ActorType,
};
use activitystreams::{
activity::{Accept, ActorAndObject, Announce, Create, Delete, Follow, Undo, Update},
base::AnyBase,
prelude::*,
};
use actix_web::{web, HttpRequest, HttpResponse};
use anyhow::{anyhow, Context};
use diesel::NotFound;
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,
};
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 person inbox.
#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd, Deserialize, Serialize)]
#[serde(rename_all = "PascalCase")]
pub enum PersonValidTypes {
Accept, // community accepted our follow request
Create, // create private message
Update, // edit private message
Delete, // private message or community deleted by creator
Undo, // private message or community restored
Remove, // community removed by admin
Announce, // post, comment or vote in community
}
pub type PersonAcceptedActivities = ActorAndObject<PersonValidTypes>;
/// Handler for all incoming activities to person inboxes.
pub async fn person_inbox(
request: HttpRequest,
input: web::Json<PersonAcceptedActivities>,
path: web::Path<String>,
context: web::Data<LemmyContext>,
) -> Result<HttpResponse, LemmyError> {
let activity = input.into_inner();
// First of all check the http signature
let request_counter = &mut 0;
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())?;
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 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(&&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!(
"Person {} received activity {:?} from {}",
person.name,
&activity.id_unchecked(),
&actor.actor_id()
);
person_receive_message(
activity.clone(),
Some(person.clone()),
actor.as_ref(),
&context,
request_counter,
)
.await
}
/// Receives Accept/Follow, Announce, private messages and community (undo) remove, (undo) delete
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_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();
match kind {
PersonValidTypes::Accept => {
receive_accept(
&context,
any_base,
actor,
to_person.expect("person provided"),
request_counter,
)
.await?;
}
PersonValidTypes::Announce => {
receive_announce(&context, any_base, actor, request_counter).await?
}
PersonValidTypes::Create => {
receive_create(&context, any_base, actor_url, request_counter).await?
}
PersonValidTypes::Update => {
receive_update(&context, any_base, actor_url, request_counter).await?
}
PersonValidTypes::Delete => {
receive_delete(context, any_base, &actor_url, request_counter).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
Ok(HttpResponse::Ok().finish())
}
/// 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_person_inbox(
context: &LemmyContext,
activity: &PersonAcceptedActivities,
) -> Result<(), LemmyError> {
let to_and_cc = get_activity_to_and_cc(activity);
// Check if it is addressed directly to any local person
if is_addressed_to_local_person(&to_and_cc, context.pool()).await? {
return Ok(());
}
// Check if it is addressed to any followers collection of a remote community, and that the
// community has local followers.
let community = is_addressed_to_community_followers(&to_and_cc, context.pool()).await?;
if let Some(c) = community {
let community_id = c.id;
let has_local_followers = blocking(&context.pool(), move |conn| {
CommunityFollower::has_local_followers(conn, community_id)
})
.await??;
if c.local {
return Err(
anyhow!("Remote activity cant be addressed to followers of local community").into(),
);
}
if has_local_followers {
return Ok(());
}
}
Err(anyhow!("Not addressed for any local person").into())
}
/// Handle accepted follows.
async fn receive_accept(
context: &LemmyContext,
activity: AnyBase,
actor: &dyn ActorType,
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)?;
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, &person.actor_id(), false)?;
let community_uri = accept
.actor()?
.to_owned()
.single_xsd_any_uri()
.context(location_info!())?;
let community =
get_or_fetch_and_upsert_community(&community_uri, context, request_counter).await?;
let community_id = community.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, 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.
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)?;
is_addressed_to_public(&announce)?;
let kind = announce
.object()
.as_single_kind_str()
.and_then(|s| s.parse().ok());
let inner_activity = announce
.object()
.to_owned()
.one()
.context(location_info!())?;
let inner_id = inner_activity.id().context(location_info!())?.to_owned();
check_is_apub_id_valid(&inner_id)?;
if is_activity_already_known(context.pool(), &inner_id).await? {
return Ok(());
}
use AnnouncableActivities::*;
match kind {
Some(Create) => {
receive_create_for_community(context, inner_activity, &inner_id, request_counter).await
}
Some(Update) => {
receive_update_for_community(context, inner_activity, &inner_id, request_counter).await
}
Some(Like) => {
receive_like_for_community(context, inner_activity, &inner_id, request_counter).await
}
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) => {
receive_undo_for_community(context, inner_activity, &inner_id, request_counter).await
}
_ => receive_unhandled_activity(inner_activity),
}
}
async fn receive_create(
context: &LemmyContext,
activity: AnyBase,
expected_domain: Url,
request_counter: &mut i32,
) -> Result<(), LemmyError> {
let create = Create::from_any_base(activity)?.context(location_info!())?;
verify_activity_domains_valid(&create, &expected_domain, true)?;
if is_addressed_to_public(&create).is_ok() {
receive_create_comment(create, context, request_counter).await
} else {
receive_create_private_message(&context, create, expected_domain, request_counter).await
}
}
async fn receive_update(
context: &LemmyContext,
activity: AnyBase,
expected_domain: Url,
request_counter: &mut i32,
) -> Result<(), LemmyError> {
let update = Update::from_any_base(activity)?.context(location_info!())?;
verify_activity_domains_valid(&update, &expected_domain, true)?;
if is_addressed_to_public(&update).is_ok() {
receive_update_comment(update, context, request_counter).await
} else {
receive_update_private_message(&context, update, expected_domain, request_counter).await
}
}
async fn receive_delete(
context: &LemmyContext,
any_base: AnyBase,
expected_domain: &Url,
request_counter: &mut i32,
) -> Result<(), LemmyError> {
use CommunityOrPrivateMessage::*;
let delete = Delete::from_any_base(any_base.clone())?.context(location_info!())?;
verify_activity_domains_valid(&delete, expected_domain, true)?;
let object_uri = delete
.object()
.to_owned()
.single_xsd_any_uri()
.context(location_info!())?;
match find_community_or_private_message_by_id(context, object_uri).await? {
Community(c) => receive_delete_community(context, c).await,
PrivateMessage(p) => receive_delete_private_message(context, delete, p, request_counter).await,
}
}
async fn receive_undo(
context: &LemmyContext,
any_base: AnyBase,
expected_domain: &Url,
request_counter: &mut i32,
) -> Result<(), LemmyError> {
use CommunityOrPrivateMessage::*;
let undo = Undo::from_any_base(any_base)?.context(location_info!())?;
verify_activity_domains_valid(&undo, expected_domain, true)?;
let inner_activity = undo.object().to_owned().one().context(location_info!())?;
let kind = inner_activity.kind_str();
match kind {
Some("Delete") => {
let delete = Delete::from_any_base(inner_activity)?.context(location_info!())?;
verify_activity_domains_valid(&delete, expected_domain, true)?;
let object_uri = delete
.object()
.to_owned()
.single_xsd_any_uri()
.context(location_info!())?;
match find_community_or_private_message_by_id(context, object_uri).await? {
Community(c) => receive_undo_delete_community(context, undo, c, expected_domain).await,
PrivateMessage(p) => {
receive_undo_delete_private_message(context, undo, expected_domain, p, request_counter)
.await
}
}
}
Some("Remove") => receive_undo_remove_community(context, undo, expected_domain).await,
_ => receive_unhandled_activity(undo),
}
}
enum CommunityOrPrivateMessage {
Community(Community),
PrivateMessage(PrivateMessage),
}
async fn find_community_or_private_message_by_id(
context: &LemmyContext,
apub_id: Url,
) -> Result<CommunityOrPrivateMessage, LemmyError> {
let ap_id = apub_id.to_owned();
let community = blocking(context.pool(), move |conn| {
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_owned();
let private_message = blocking(context.pool(), move |conn| {
PrivateMessage::read_from_apub_id(conn, &ap_id.into())
})
.await?;
if let Ok(p) = private_message {
return Ok(CommunityOrPrivateMessage::PrivateMessage(p));
}
Err(NotFound.into())
}

View file

@ -0,0 +1,376 @@
use crate::{
activities::receive::{
comment::{
receive_create_comment,
receive_delete_comment,
receive_dislike_comment,
receive_like_comment,
receive_remove_comment,
receive_update_comment,
},
comment_undo::{
receive_undo_delete_comment,
receive_undo_dislike_comment,
receive_undo_like_comment,
receive_undo_remove_comment,
},
post::{
receive_create_post,
receive_delete_post,
receive_dislike_post,
receive_like_post,
receive_remove_post,
receive_update_post,
},
post_undo::{
receive_undo_delete_post,
receive_undo_dislike_post,
receive_undo_like_post,
receive_undo_remove_post,
},
receive_unhandled_activity,
verify_activity_domains_valid,
},
fetcher::objects::{get_or_fetch_and_insert_comment, get_or_fetch_and_insert_post},
find_post_or_comment_by_id,
inbox::is_addressed_to_public,
PostOrComment,
};
use activitystreams::{
activity::{Create, Delete, Dislike, Like, Remove, Undo, Update},
base::AnyBase,
prelude::*,
};
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_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 person.
/// A post or comment being created
pub(in crate::inbox) async fn receive_create_for_community(
context: &LemmyContext,
activity: AnyBase,
expected_domain: &Url,
request_counter: &mut i32,
) -> Result<(), LemmyError> {
let create = Create::from_any_base(activity)?.context(location_info!())?;
verify_activity_domains_valid(&create, &expected_domain, true)?;
is_addressed_to_public(&create)?;
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),
}
}
/// A post or comment being edited
pub(in crate::inbox) async fn receive_update_for_community(
context: &LemmyContext,
activity: AnyBase,
expected_domain: &Url,
request_counter: &mut i32,
) -> Result<(), LemmyError> {
let update = Update::from_any_base(activity)?.context(location_info!())?;
verify_activity_domains_valid(&update, &expected_domain, true)?;
is_addressed_to_public(&update)?;
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),
}
}
/// A post or comment being upvoted
pub(in crate::inbox) async fn receive_like_for_community(
context: &LemmyContext,
activity: AnyBase,
expected_domain: &Url,
request_counter: &mut i32,
) -> Result<(), LemmyError> {
let like = Like::from_any_base(activity)?.context(location_info!())?;
verify_activity_domains_valid(&like, &expected_domain, false)?;
is_addressed_to_public(&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::Comment(comment) => {
receive_like_comment(like, *comment, context, request_counter).await
}
}
}
/// A post or comment being downvoted
pub(in crate::inbox) async fn receive_dislike_for_community(
context: &LemmyContext,
activity: AnyBase,
expected_domain: &Url,
request_counter: &mut i32,
) -> Result<(), LemmyError> {
let enable_downvotes = blocking(context.pool(), move |conn| {
Site::read(conn, 1).map(|s| s.enable_downvotes)
})
.await??;
if !enable_downvotes {
return Ok(());
}
let dislike = Dislike::from_any_base(activity)?.context(location_info!())?;
verify_activity_domains_valid(&dislike, &expected_domain, false)?;
is_addressed_to_public(&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
}
PostOrComment::Comment(comment) => {
receive_dislike_comment(dislike, *comment, context, request_counter).await
}
}
}
/// A post or comment being deleted by its creator
pub(in crate::inbox) async fn receive_delete_for_community(
context: &LemmyContext,
activity: AnyBase,
expected_domain: &Url,
) -> Result<(), LemmyError> {
let delete = Delete::from_any_base(activity)?.context(location_info!())?;
verify_activity_domains_valid(&delete, &expected_domain, true)?;
is_addressed_to_public(&delete)?;
let object = delete
.object()
.to_owned()
.single_xsd_any_uri()
.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,
// if we dont have the object, no need to do anything
Err(_) => Ok(()),
}
}
/// A post or comment being removed by a mod/admin
pub(in crate::inbox) async fn receive_remove_for_community(
context: &LemmyContext,
activity: AnyBase,
expected_domain: &Url,
) -> Result<(), LemmyError> {
let remove = Remove::from_any_base(activity)?.context(location_info!())?;
verify_activity_domains_valid(&remove, &expected_domain, false)?;
is_addressed_to_public(&remove)?;
let cc = remove
.cc()
.map(|c| c.as_many())
.flatten()
.context(location_info!())?;
let community_id = cc
.first()
.map(|c| c.as_xsd_any_uri())
.flatten()
.context(location_info!())?;
let object = remove
.object()
.to_owned()
.single_xsd_any_uri()
.context(location_info!())?;
// Ensure that remove activity comes from the same domain as the 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,
// 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,
activity: AnyBase,
expected_domain: &Url,
request_counter: &mut i32,
) -> Result<(), LemmyError> {
let undo = Undo::from_any_base(activity)?.context(location_info!())?;
verify_activity_domains_valid(&undo, &expected_domain.to_owned(), true)?;
is_addressed_to_public(&undo)?;
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) => {
receive_undo_dislike_for_community(context, undo, expected_domain, request_counter).await
}
_ => receive_unhandled_activity(undo),
}
}
/// A post or comment deletion being reverted
pub(in crate::inbox) async fn receive_undo_delete_for_community(
context: &LemmyContext,
undo: Undo,
expected_domain: &Url,
) -> Result<(), LemmyError> {
let delete = Delete::from_any_base(undo.object().to_owned().one().context(location_info!())?)?
.context(location_info!())?;
verify_activity_domains_valid(&delete, &expected_domain, true)?;
is_addressed_to_public(&delete)?;
let object = delete
.object()
.to_owned()
.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,
// if we dont have the object, no need to do anything
Err(_) => Ok(()),
}
}
/// A post or comment removal being reverted
pub(in crate::inbox) async fn receive_undo_remove_for_community(
context: &LemmyContext,
undo: Undo,
expected_domain: &Url,
) -> Result<(), LemmyError> {
let remove = Remove::from_any_base(undo.object().to_owned().one().context(location_info!())?)?
.context(location_info!())?;
verify_activity_domains_valid(&remove, &expected_domain, false)?;
is_addressed_to_public(&remove)?;
let object = remove
.object()
.to_owned()
.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,
// if we dont have the object, no need to do anything
Err(_) => Ok(()),
}
}
/// A post or comment upvote being reverted
pub(in crate::inbox) async fn receive_undo_like_for_community(
context: &LemmyContext,
undo: Undo,
expected_domain: &Url,
request_counter: &mut i32,
) -> Result<(), LemmyError> {
let like = Like::from_any_base(undo.object().to_owned().one().context(location_info!())?)?
.context(location_info!())?;
verify_activity_domains_valid(&like, &expected_domain, false)?;
is_addressed_to_public(&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
}
PostOrComment::Comment(comment) => {
receive_undo_like_comment(&like, *comment, context, request_counter).await
}
}
}
/// A post or comment downvote being reverted
pub(in crate::inbox) async fn receive_undo_dislike_for_community(
context: &LemmyContext,
undo: Undo,
expected_domain: &Url,
request_counter: &mut i32,
) -> Result<(), LemmyError> {
let dislike = Dislike::from_any_base(undo.object().to_owned().one().context(location_info!())?)?
.context(location_info!())?;
verify_activity_domains_valid(&dislike, &expected_domain, false)?;
is_addressed_to_public(&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
}
PostOrComment::Comment(comment) => {
receive_undo_dislike_comment(&dislike, *comment, context, request_counter).await
}
}
}
async fn fetch_post_or_comment_by_id(
apub_id: &Url,
context: &LemmyContext,
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(Box::new(post)));
}
if let Ok(comment) = get_or_fetch_and_insert_comment(apub_id, context, request_counter).await {
return Ok(PostOrComment::Comment(Box::new(comment)));
}
Err(NotFound.into())
}

View file

@ -0,0 +1,152 @@
use crate::{
inbox::{
assert_activity_not_local,
community_inbox::{community_receive_message, CommunityAcceptedActivities},
get_activity_id,
get_activity_to_and_cc,
inbox_verify_http_signature,
is_activity_already_known,
is_addressed_to_community_followers,
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_utils::{location_info, LemmyError};
use lemmy_websocket::LemmyContext;
use serde::{Deserialize, Serialize};
use std::fmt::Debug;
use url::Url;
/// Allowed activity types for shared inbox.
#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd, Deserialize, Serialize)]
#[serde(rename_all = "PascalCase")]
pub enum ValidTypes {
Create,
Update,
Like,
Dislike,
Delete,
Undo,
Remove,
Announce,
}
// TODO: this isnt entirely correct, cause some of these receive are not ActorAndObject,
// but it still works due to the anybase conversion
pub type AcceptedActivities = ActorAndObject<ValidTypes>;
/// Handler for all incoming requests to shared inbox.
pub async fn shared_inbox(
request: HttpRequest,
input: web::Json<AcceptedActivities>,
context: web::Data<LemmyContext>,
) -> Result<HttpResponse, LemmyError> {
let activity = input.into_inner();
// First of all check the http signature
let request_counter = &mut 0;
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 activity_id = get_activity_id(&activity, &actor_id)?;
if is_activity_already_known(context.pool(), &activity_id).await? {
return Ok(HttpResponse::Ok().finish());
}
assert_activity_not_local(&activity)?;
// Log the activity, so we avoid receiving and parsing it twice. Note that this could still happen
// if we receive the same activity twice in very quick succession.
insert_activity(&activity_id, activity.clone(), false, true, context.pool()).await?;
let activity_any_base = activity.clone().into_any_base()?;
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 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 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?;
if let Some(community) = community {
let community_activity = CommunityAcceptedActivities::from_any_base(activity_any_base.clone())?
.context(location_info!())?;
res = Some(
community_receive_message(
community_activity,
community,
actor.as_ref(),
&context,
request_counter,
)
.await?,
);
} 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_person` is only used for follow activities (which we dont receive here), so no need to pass
// it in
person_receive_message(
person_activity,
None,
actor.as_ref(),
&context,
request_counter,
)
.await?;
} else if is_addressed_to_community_followers(&to_and_cc, context.pool())
.await?
.is_some()
{
let person_activity = PersonAcceptedActivities::from_any_base(activity_any_base.clone())?
.context(location_info!())?;
res = Some(
person_receive_message(
person_activity,
None,
actor.as_ref(),
&context,
request_counter,
)
.await?,
);
}
// If none of those, throw an error
if let Some(r) = res {
Ok(r)
} else {
Ok(HttpResponse::NotImplemented().finish())
}
}
/// If `to_and_cc` contains the ID of a local community, return that community, otherwise return
/// None.
///
/// This doesnt handle the case where an activity is addressed to multiple communities (because
/// Lemmy doesnt generate such activities).
async fn extract_local_community_from_destinations(
to_and_cc: &[Url],
pool: &DbPool,
) -> Result<Option<Community>, LemmyError> {
for url in to_and_cc {
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));
}
}
}
Ok(None)
}

379
crates/apub/src/lib.rs Normal file
View file

@ -0,0 +1,379 @@
#[macro_use]
extern crate lazy_static;
pub mod activities;
pub mod activity_queue;
pub mod extensions;
pub mod fetcher;
pub mod http;
pub mod inbox;
pub mod objects;
pub mod routes;
use crate::extensions::{
group_extensions::GroupExtension,
page_extension::PageExtension,
signatures::{PublicKey, PublicKeyExtension},
};
use activitystreams::{
activity::Follow,
actor::{ApActor, Group, Person},
base::AnyBase,
object::{ApObject, Note, Page},
};
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::{
activity::Activity,
comment::Comment,
community::Community,
person::Person as DbPerson,
post::Post,
private_message::PrivateMessage,
},
DbUrl,
};
use lemmy_utils::{location_info, settings::structs::Settings, LemmyError};
use lemmy_websocket::LemmyContext;
use serde::Serialize;
use std::net::IpAddr;
use url::{ParseError, Url};
/// Activitystreams type for community
type GroupExt = Ext2<ApActor<ApObject<Group>>, GroupExtension, PublicKeyExtension>;
/// Activitystreams type for person
type PersonExt = Ext1<ApActor<ApObject<Person>>, PublicKeyExtension>;
/// Activitystreams type for post
type PageExt = Ext1<ApObject<Page>, PageExtension>;
type NoteExt = ApObject<Note>;
pub static APUB_JSON_CONTENT_TYPE: &str = "application/activity+json";
/// Checks if the ID is allowed for sending or receiving.
///
/// In particular, it checks for:
/// - federation being enabled (if its disabled, only local URLs are allowed)
/// - the correct scheme (either http or https)
/// - URL being in the allowlist (if it is active)
/// - URL not being in the blocklist (if it is active)
///
/// Note that only one of allowlist and blacklist can be enabled, not both.
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.get_hostname_without_port()?;
if !settings.federation().enabled {
return if domain == local_instance {
Ok(())
} else {
Err(
anyhow!(
"Trying to connect with {}, but federation is disabled",
domain
)
.into(),
)
};
}
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, apub_id).into());
}
if apub_id.scheme() != Settings::get().get_protocol_string() {
return Err(anyhow!("invalid apub id scheme {}: {}", apub_id.scheme(), apub_id).into());
}
let allowed_instances = Settings::get().get_allowed_instances();
let blocked_instances = Settings::get().get_blocked_instances();
if allowed_instances.is_none() && blocked_instances.is_none() {
Ok(())
} 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.push(local_instance);
if allowed.contains(&domain) {
Ok(())
} else {
Err(anyhow!("{} not in federation allowlist", domain).into())
}
} else if let Some(blocked) = blocked_instances {
if blocked.contains(&domain) {
Err(anyhow!("{} is in federation blocklist", domain).into())
} else {
Ok(())
}
} else {
panic!("Invalid config, both allowed_instances and blocked_instances are specified");
}
}
/// Common functions for ActivityPub objects, which are implemented by most (but not all) objects
/// and actors in Lemmy.
#[async_trait::async_trait(?Send)]
pub trait ApubObjectType {
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: &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_trait::async_trait(?Send)]
pub trait ApubLikeableType {
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 person). Not all methods are
/// implemented by all actors.
#[async_trait::async_trait(?Send)]
pub trait ActorType {
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>;
fn private_key(&self) -> Option<String>;
async fn send_follow(
&self,
follow_actor_id: &Url,
context: &LemmyContext,
) -> Result<(), LemmyError>;
async fn send_unfollow(
&self,
follow_actor_id: &Url,
context: &LemmyContext,
) -> Result<(), LemmyError>;
async fn send_accept_follow(
&self,
follow: Follow,
context: &LemmyContext,
) -> Result<(), LemmyError>;
async fn send_delete(&self, context: &LemmyContext) -> Result<(), LemmyError>;
async fn send_undo_delete(&self, context: &LemmyContext) -> Result<(), LemmyError>;
async fn send_remove(&self, context: &LemmyContext) -> Result<(), LemmyError>;
async fn send_undo_remove(&self, context: &LemmyContext) -> Result<(), LemmyError>;
async fn send_announce(
&self,
activity: AnyBase,
context: &LemmyContext,
) -> Result<(), LemmyError>;
/// For a given community, returns the inboxes of all followers.
async fn get_follower_inboxes(&self, pool: &DbPool) -> Result<Vec<Url>, LemmyError>;
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()))?)
}
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(),
)
}
}
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(),
&actor_id.host_str().context(location_info!())?,
if let Some(port) = actor_id.port() {
format!(":{}", port)
} else {
"".to_string()
},
);
Ok(Url::parse(&url)?.into())
}
/// Store a sent or received activity in the database, for logging purposes. These records are not
/// persistent.
pub(crate) async fn insert_activity<T>(
ap_id: &Url,
activity: T,
local: bool,
sensitive: bool,
pool: &DbPool,
) -> Result<(), LemmyError>
where
T: Serialize + std::fmt::Debug + Send + 'static,
{
let ap_id = ap_id.to_owned().into();
blocking(pool, move |conn| {
Activity::insert(conn, ap_id, &activity, local, sensitive)
})
.await??;
Ok(())
}
pub(crate) enum PostOrComment {
Comment(Box<Comment>),
Post(Box<Post>),
}
/// Tries to find a post or comment in the local database, without any network requests.
/// This is used to handle deletions and removals, because in case we dont have the object, we can
/// simply ignore the activity.
pub(crate) async fn find_post_or_comment_by_id(
context: &LemmyContext,
apub_id: Url,
) -> Result<PostOrComment, LemmyError> {
let ap_id = apub_id.clone();
let post = blocking(context.pool(), move |conn| {
Post::read_from_apub_id(conn, &ap_id.into())
})
.await?;
if let Ok(p) = post {
return Ok(PostOrComment::Post(Box::new(p)));
}
let ap_id = apub_id.clone();
let comment = blocking(context.pool(), move |conn| {
Comment::read_from_apub_id(conn, &ap_id.into())
})
.await?;
if let Ok(c) = comment {
return Ok(PostOrComment::Comment(Box::new(c)));
}
Err(NotFound.into())
}
pub(crate) enum Object {
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> {
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(Box::new(*p)),
PostOrComment::Comment(c) => Object::Comment(Box::new(*c)),
});
}
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) = person {
return Ok(Object::Person(Box::new(u)));
}
let ap_id = apub_id.clone();
let community = blocking(context.pool(), move |conn| {
Community::read_from_apub_id(conn, &ap_id.into())
})
.await?;
if let Ok(c) = community {
return Ok(Object::Community(Box::new(c)));
}
let private_message = blocking(context.pool(), move |conn| {
PrivateMessage::read_from_apub_id(conn, &apub_id.into())
})
.await?;
if let Ok(pm) = private_message {
return Ok(Object::PrivateMessage(Box::new(pm)));
}
Err(NotFound.into())
}

View file

@ -0,0 +1,187 @@
use crate::{
extensions::context::lemmy_context,
fetcher::objects::{get_or_fetch_and_insert_comment, get_or_fetch_and_insert_post},
objects::{
check_object_domain,
check_object_for_community_or_site_ban,
create_tombstone,
get_object_from_apub,
get_or_fetch_and_upsert_person,
get_source_markdown_value,
set_content_and_source,
FromApub,
FromApubToForm,
ToApub,
},
NoteExt,
};
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::{
comment::{Comment, CommentForm},
person::Person,
post::Post,
},
CommentId,
};
use lemmy_utils::{
location_info,
utils::{convert_datetime, remove_slurs},
LemmyError,
};
use lemmy_websocket::LemmyContext;
use url::Url;
#[async_trait::async_trait(?Send)]
impl ToApub for Comment {
type ApubType = NoteExt;
async fn to_apub(&self, pool: &DbPool) -> Result<NoteExt, LemmyError> {
let mut comment = ApObject::new(Note::new());
let creator_id = self.creator_id;
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??;
// 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.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.into_inner());
}
comment
// Not needed when the Post is embedded in a collection (like for community outbox)
.set_many_contexts(lemmy_context()?)
.set_id(self.ap_id.to_owned().into_inner())
.set_published(convert_datetime(self.published))
.set_to(public())
.set_many_in_reply_tos(in_reply_to_vec)
.set_attributed_to(creator.actor_id.into_inner());
set_content_and_source(&mut comment, &self.content)?;
if let Some(u) = self.updated {
comment.set_updated(convert_datetime(u));
}
Ok(comment)
}
fn to_tombstone(&self) -> Result<Tombstone, LemmyError> {
create_tombstone(
self.deleted,
self.ap_id.to_owned().into(),
self.updated,
NoteType::Note,
)
}
}
#[async_trait::async_trait(?Send)]
impl FromApub for Comment {
type ApubType = NoteExt;
/// Converts a `Note` to `Comment`.
///
/// If the parent community, post and comment(s) are not known locally, these are also fetched.
async fn from_apub(
note: &NoteExt,
context: &LemmyContext,
expected_domain: Url,
request_counter: &mut i32,
) -> Result<Comment, LemmyError> {
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.
blocking(context.pool(), move |conn| {
Comment::delete(conn, comment.id)
})
.await??;
Err(anyhow!("Post is locked").into())
} else {
Ok(comment)
}
}
}
#[async_trait::async_trait(?Send)]
impl FromApubToForm<NoteExt> for CommentForm {
async fn from_apub(
note: &NoteExt,
context: &LemmyContext,
expected_domain: Url,
request_counter: &mut i32,
) -> Result<CommentForm, LemmyError> {
let creator_actor_id = &note
.attributed_to()
.context(location_info!())?
.as_single_xsd_any_uri()
.context(location_info!())?;
let creator =
get_or_fetch_and_upsert_person(creator_actor_id, context, request_counter).await?;
let mut in_reply_tos = note
.in_reply_to()
.as_ref()
.context(location_info!())?
.as_many()
.context(location_info!())?
.iter()
.map(|i| i.as_xsd_any_uri().context(""));
let post_ap_id = in_reply_tos.next().context(location_info!())??;
// This post, or the parent comment might not yet exist on this server yet, fetch them.
let post = get_or_fetch_and_insert_post(&post_ap_id, context, request_counter).await?;
// 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<CommentId> = match in_reply_tos.next() {
Some(parent_comment_uri) => {
let parent_comment_ap_id = &parent_comment_uri?;
let parent_comment =
get_or_fetch_and_insert_comment(&parent_comment_ap_id, context, request_counter).await?;
Some(parent_comment.id)
}
None => None,
};
let content = get_source_markdown_value(note)?.context(location_info!())?;
let content_slurs_removed = remove_slurs(&content);
Ok(CommentForm {
creator_id: creator.id,
post_id: post.id,
parent_id,
content: content_slurs_removed,
removed: None,
read: None,
published: note.published().map(|u| u.to_owned().naive_local()),
updated: note.updated().map(|u| u.to_owned().naive_local()),
deleted: None,
ap_id: Some(check_object_domain(note, expected_domain)?),
local: false,
})
}
}

View file

@ -0,0 +1,228 @@
use crate::{
extensions::{context::lemmy_context, group_extensions::GroupExtension},
fetcher::person::get_or_fetch_and_upsert_person,
objects::{
check_object_domain,
create_tombstone,
get_object_from_apub,
get_source_markdown_value,
set_content_and_source,
FromApub,
FromApubToForm,
ToApub,
},
ActorType,
GroupExt,
};
use activitystreams::{
actor::{kind::GroupType, ApActor, Endpoints, Group},
base::BaseExt,
object::{ApObject, Image, Tombstone},
prelude::*,
};
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_utils::{
location_info,
utils::{check_slurs, check_slurs_opt, convert_datetime},
LemmyError,
};
use lemmy_websocket::LemmyContext;
use url::Url;
#[async_trait::async_trait(?Send)]
impl ToApub for Community {
type ApubType = GroupExt;
async fn to_apub(&self, pool: &DbPool) -> Result<GroupExt, LemmyError> {
// The attributed to, is an ordered vector with the creator actor_ids first,
// then the rest of the moderators
// TODO Technically the instance admins can mod the community, but lets
// ignore that for now
let id = self.id;
let moderators = blocking(pool, move |conn| {
CommunityModeratorView::for_community(&conn, id)
})
.await??;
let moderators: Vec<Url> = moderators
.into_iter()
.map(|m| m.moderator.actor_id.into_inner())
.collect();
let mut group = ApObject::new(Group::new());
group
.set_many_contexts(lemmy_context()?)
.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);
if let Some(u) = self.updated.to_owned() {
group.set_updated(convert_datetime(u));
}
if let Some(d) = self.description.to_owned() {
set_content_and_source(&mut group, &d)?;
}
if let Some(icon_url) = &self.icon {
let mut image = Image::new();
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>(banner_url.to_owned().into());
group.set_image(image.into_any_base()?);
}
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.followers_url.clone().into())
.set_endpoints(Endpoints {
shared_inbox: Some(self.get_shared_inbox_or_inbox_url()),
..Default::default()
});
Ok(Ext2::new(
ap_actor,
GroupExtension::new(self.nsfw)?,
self.get_public_key_ext()?,
))
}
fn to_tombstone(&self) -> Result<Tombstone, LemmyError> {
create_tombstone(
self.deleted,
self.actor_id.to_owned().into(),
self.updated,
GroupType::Group,
)
}
}
#[async_trait::async_trait(?Send)]
impl FromApub for Community {
type ApubType = GroupExt;
/// Converts a `Group` to `Community`.
async fn from_apub(
group: &GroupExt,
context: &LemmyContext,
expected_domain: Url,
request_counter: &mut i32,
) -> Result<Community, LemmyError> {
get_object_from_apub(group, context, expected_domain, request_counter).await
}
}
#[async_trait::async_trait(?Send)]
impl FromApubToForm<GroupExt> for CommunityForm {
async fn from_apub(
group: &GroupExt,
context: &LemmyContext,
expected_domain: Url,
request_counter: &mut i32,
) -> Result<Self, LemmyError> {
let creator_and_moderator_uris = group.inner.attributed_to().context(location_info!())?;
let creator_uri = creator_and_moderator_uris
.as_many()
.context(location_info!())?
.iter()
.next()
.context(location_info!())?
.as_xsd_any_uri()
.context(location_info!())?;
let creator = get_or_fetch_and_upsert_person(creator_uri, context, request_counter).await?;
let name = group
.inner
.preferred_username()
.context(location_info!())?
.to_string();
let title = group
.inner
.name()
.context(location_info!())?
.as_one()
.context(location_info!())?
.as_xsd_string()
.context(location_info!())?
.to_string();
let description = get_source_markdown_value(group)?;
check_slurs(&name)?;
check_slurs(&title)?;
check_slurs_opt(&description)?;
let icon = match group.icon() {
Some(any_image) => Some(
Image::from_any_base(any_image.as_one().context(location_info!())?.clone())
.context(location_info!())?
.context(location_info!())?
.url()
.context(location_info!())?
.as_single_xsd_any_uri()
.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())
.context(location_info!())?
.context(location_info!())?
.url()
.context(location_info!())?
.as_single_xsd_any_uri()
.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,
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.unwrap_or(false),
actor_id: Some(check_object_domain(group, expected_domain)?),
local: false,
private_key: None,
public_key: Some(group.ext_two.to_owned().public_key.public_key_pem),
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

@ -0,0 +1,247 @@
use crate::{
check_is_apub_id_valid,
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::{
base::{AsBase, BaseExt, ExtendsExt},
markers::Base,
mime::{FromStrError, Mime},
object::{ApObjectExt, Object, ObjectExt, Tombstone, TombstoneExt},
};
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_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;
/// Trait for converting an object or actor into the respective ActivityPub type.
#[async_trait::async_trait(?Send)]
pub(crate) trait ToApub {
type ApubType;
async fn to_apub(&self, pool: &DbPool) -> Result<Self::ApubType, LemmyError>;
fn to_tombstone(&self) -> Result<Tombstone, LemmyError>;
}
#[async_trait::async_trait(?Send)]
pub(crate) trait FromApub {
type ApubType;
/// Converts an object from ActivityPub type to Lemmy internal type.
///
/// * `apub` The object to read from
/// * `context` LemmyContext which holds DB pool, HTTP client etc
/// * `expected_domain` Domain where the object was received from
async fn from_apub(
apub: &Self::ApubType,
context: &LemmyContext,
expected_domain: Url,
request_counter: &mut i32,
) -> Result<Self, LemmyError>
where
Self: Sized;
}
#[async_trait::async_trait(?Send)]
pub(in crate::objects) trait FromApubToForm<ApubType> {
async fn from_apub(
apub: &ApubType,
context: &LemmyContext,
expected_domain: Url,
request_counter: &mut i32,
) -> Result<Self, LemmyError>
where
Self: Sized;
}
/// Updated is actually the deletion time
fn create_tombstone<T>(
deleted: bool,
object_id: Url,
updated: Option<NaiveDateTime>,
former_type: T,
) -> Result<Tombstone, LemmyError>
where
T: ToString,
{
if deleted {
if let Some(updated) = updated {
let mut tombstone = Tombstone::new();
tombstone.set_id(object_id);
tombstone.set_former_type(former_type.to_string());
tombstone.set_deleted(convert_datetime(updated));
Ok(tombstone)
} else {
Err(anyhow!("Cant convert to tombstone because updated time was None.").into())
}
} else {
Err(anyhow!("Cant convert object to tombstone if it wasnt deleted").into())
}
}
pub(in crate::objects) fn check_object_domain<T, Kind>(
apub: &T,
expected_domain: Url,
) -> 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_owned().into())
}
pub(in crate::objects) fn set_content_and_source<T, Kind1, Kind2>(
object: &mut T,
markdown_text: &str,
) -> Result<(), LemmyError>
where
T: ApObjectExt<Kind1> + ObjectExt<Kind2> + AsBase<Kind2>,
{
let mut source = Object::<()>::new_none_type();
source
.set_content(markdown_text)
.set_media_type(mime_markdown()?);
object.set_source(source.into_any_base()?);
object.set_content(markdown_to_html(markdown_text));
object.set_media_type(mime_html()?);
Ok(())
}
pub(in crate::objects) fn get_source_markdown_value<T, Kind1, Kind2>(
object: &T,
) -> Result<Option<String>, LemmyError>
where
T: ApObjectExt<Kind1> + ObjectExt<Kind2> + AsBase<Kind2>,
{
let content = object
.content()
.map(|s| s.as_single_xsd_string())
.flatten()
.map(|s| s.to_string());
if content.is_some() {
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
.content()
.map(|s| s.as_single_xsd_string())
.flatten()
.context(location_info!())?
.to_string();
return Ok(Some(source_content));
}
Ok(None)
}
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()?) {
Err(LemmyError::from(anyhow!(
"Lemmy only supports markdown content"
)))
} else {
Ok(())
}
}
/// 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, IdType>(
from: &From,
context: &LemmyContext,
expected_domain: Url,
request_counter: &mut i32,
) -> Result<To, LemmyError>
where
From: BaseExt<Kind>,
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 {
let object = blocking(context.pool(), move |conn| {
To::read_from_apub_id(conn, &object_id.into())
})
.await??;
Ok(object)
}
// otherwise parse and insert, assuring that it comes from the right domain
else {
let to_form = ToForm::from_apub(&from, context, expected_domain, request_counter).await?;
let to = blocking(context.pool(), move |conn| To::upsert(conn, &to_form)).await??;
Ok(to)
}
}
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 person_id = object
.attributed_to()
.context(location_info!())?
.as_single_xsd_any_uri()
.context(location_info!())?;
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_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

@ -0,0 +1,192 @@
use crate::{
extensions::context::lemmy_context,
objects::{
check_object_domain,
get_source_markdown_value,
set_content_and_source,
FromApub,
FromApubToForm,
ToApub,
},
ActorType,
PersonExt,
};
use activitystreams::{
actor::{ApActor, Endpoints, Person},
object::{ApObject, Image, Tombstone},
prelude::*,
};
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::person::{Person as DbPerson, PersonForm},
};
use lemmy_utils::{
location_info,
settings::structs::Settings,
utils::{check_slurs, check_slurs_opt, convert_datetime},
LemmyError,
};
use lemmy_websocket::LemmyContext;
use url::Url;
#[async_trait::async_trait(?Send)]
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(self.actor_id.to_owned().into_inner())
.set_published(convert_datetime(self.published));
if let Some(u) = self.updated {
person.set_updated(convert_datetime(u));
}
if let Some(avatar_url) = &self.avatar {
let mut image = Image::new();
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>(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)?;
}
if let Some(i) = self.preferred_username.to_owned() {
person.set_name(i);
}
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_or_inbox_url()),
..Default::default()
});
Ok(Ext1::new(ap_actor, self.get_public_key_ext()?))
}
fn to_tombstone(&self) -> Result<Tombstone, LemmyError> {
unimplemented!()
}
}
#[async_trait::async_trait(?Send)]
impl FromApub for DbPerson {
type ApubType = PersonExt;
async fn from_apub(
person: &PersonExt,
context: &LemmyContext,
expected_domain: Url,
request_counter: &mut i32,
) -> 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(person)
} else {
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 PersonForm {
async fn from_apub(
person: &PersonExt,
_context: &LemmyContext,
expected_domain: Url,
_request_counter: &mut i32,
) -> Result<Self, LemmyError> {
let avatar = match person.icon() {
Some(any_image) => Some(
Image::from_any_base(any_image.as_one().context(location_info!())?.clone())?
.context(location_info!())?
.url()
.context(location_info!())?
.as_single_xsd_any_uri()
.map(|url| url.to_owned()),
),
None => None,
};
let banner = match person.image() {
Some(any_image) => Some(
Image::from_any_base(any_image.as_one().context(location_info!())?.clone())
.context(location_info!())?
.context(location_info!())?
.url()
.context(location_info!())?
.as_single_xsd_any_uri()
.map(|url| url.to_owned()),
),
None => None,
};
let name: String = person
.inner
.preferred_username()
.context(location_info!())?
.to_string();
let preferred_username: Option<String> = person
.name()
.map(|n| n.one())
.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(PersonForm {
name,
preferred_username: Some(preferred_username),
banned: None,
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()),
actor_id: Some(check_object_domain(person, expected_domain)?),
bio: Some(bio),
local: Some(false),
private_key: None,
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

@ -0,0 +1,223 @@
use crate::{
extensions::{context::lemmy_context, page_extension::PageExtension},
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,
ToApub,
},
PageExt,
};
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::{
self,
source::{
community::Community,
person::Person,
post::{Post, PostForm},
},
};
use lemmy_utils::{
location_info,
request::fetch_iframely_and_pictrs_data,
utils::{check_slurs, convert_datetime, remove_slurs},
LemmyError,
};
use lemmy_websocket::LemmyContext;
use url::Url;
#[async_trait::async_trait(?Send)]
impl ToApub for Post {
type ApubType = PageExt;
// Turn a Lemmy post into an ActivityPub page that can be sent out over the network.
async fn to_apub(&self, pool: &DbPool) -> Result<PageExt, LemmyError> {
let mut page = ApObject::new(Page::new());
let creator_id = self.creator_id;
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??;
page
// Not needed when the Post is embedded in a collection (like for community outbox)
// 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.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_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)?;
}
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>(thumbnail_url.to_owned().into());
page.set_image(image.into_any_base()?);
}
if let Some(u) = self.updated {
page.set_updated(convert_datetime(u));
}
let ext = PageExtension {
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.to_owned().into(),
self.updated,
PageType::Page,
)
}
}
#[async_trait::async_trait(?Send)]
impl FromApub for Post {
type ApubType = PageExt;
/// Converts a `PageExt` to `PostForm`.
///
/// If the post's community or creator are not known locally, these are also fetched.
async fn from_apub(
page: &PageExt,
context: &LemmyContext,
expected_domain: Url,
request_counter: &mut i32,
) -> Result<Post, LemmyError> {
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)
}
}
#[async_trait::async_trait(?Send)]
impl FromApubToForm<PageExt> for PostForm {
async fn from_apub(
page: &PageExt,
context: &LemmyContext,
expected_domain: Url,
request_counter: &mut i32,
) -> Result<PostForm, LemmyError> {
let ext = &page.ext_one;
let creator_actor_id = page
.inner
.attributed_to()
.as_ref()
.context(location_info!())?
.as_single_xsd_any_uri()
.context(location_info!())?;
let creator =
get_or_fetch_and_upsert_person(creator_actor_id, context, request_counter).await?;
let community = get_to_community(page, context, request_counter).await?;
let thumbnail_url: Option<Url> = match &page.inner.image() {
Some(any_image) => Image::from_any_base(
any_image
.to_owned()
.as_one()
.context(location_info!())?
.to_owned(),
)?
.context(location_info!())?
.url()
.context(location_info!())?
.as_single_xsd_any_uri()
.map(|url| url.to_owned()),
None => None,
};
let url = page
.inner
.url()
.map(|u| u.as_single_xsd_any_uri())
.flatten()
.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)).await
} else {
(None, None, None, thumbnail_url)
};
let name = page
.inner
.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!())?
.to_string();
let body = get_source_markdown_value(page)?;
check_slurs(&name)?;
let body_slurs_removed = body.map(|b| remove_slurs(&b));
Ok(PostForm {
name,
url: url.map(|u| u.into()),
body: body_slurs_removed,
creator_id: creator.id,
community_id: community.id,
removed: None,
locked: ext.comments_enabled.map(|e| !e),
published: page
.inner
.published()
.as_ref()
.map(|u| u.to_owned().naive_local()),
updated: page
.inner
.updated()
.as_ref()
.map(|u| u.to_owned().naive_local()),
deleted: None,
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.map(|u| u.into()),
ap_id: Some(check_object_domain(page, expected_domain)?),
local: false,
})
}
}

View file

@ -0,0 +1,127 @@
use crate::{
check_is_apub_id_valid,
extensions::context::lemmy_context,
fetcher::person::get_or_fetch_and_upsert_person,
objects::{
check_object_domain,
create_tombstone,
get_object_from_apub,
get_source_markdown_value,
set_content_and_source,
FromApub,
FromApubToForm,
ToApub,
},
NoteExt,
};
use activitystreams::{
object::{kind::NoteType, ApObject, Note, Tombstone},
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},
};
use lemmy_utils::{location_info, utils::convert_datetime, LemmyError};
use lemmy_websocket::LemmyContext;
use url::Url;
#[async_trait::async_trait(?Send)]
impl ToApub for PrivateMessage {
type ApubType = NoteExt;
async fn to_apub(&self, pool: &DbPool) -> Result<NoteExt, LemmyError> {
let mut private_message = ApObject::new(Note::new());
let creator_id = self.creator_id;
let creator = blocking(pool, move |conn| Person::read(conn, creator_id)).await??;
let recipient_id = self.recipient_id;
let recipient = blocking(pool, move |conn| Person::read(conn, recipient_id)).await??;
private_message
.set_many_contexts(lemmy_context()?)
.set_id(self.ap_id.to_owned().into_inner())
.set_published(convert_datetime(self.published))
.set_to(recipient.actor_id.into_inner())
.set_attributed_to(creator.actor_id.into_inner());
set_content_and_source(&mut private_message, &self.content)?;
if let Some(u) = self.updated {
private_message.set_updated(convert_datetime(u));
}
Ok(private_message)
}
fn to_tombstone(&self) -> Result<Tombstone, LemmyError> {
create_tombstone(
self.deleted,
self.ap_id.to_owned().into(),
self.updated,
NoteType::Note,
)
}
}
#[async_trait::async_trait(?Send)]
impl FromApub for PrivateMessage {
type ApubType = NoteExt;
async fn from_apub(
note: &NoteExt,
context: &LemmyContext,
expected_domain: Url,
request_counter: &mut i32,
) -> Result<PrivateMessage, LemmyError> {
get_object_from_apub(note, context, expected_domain, request_counter).await
}
}
#[async_trait::async_trait(?Send)]
impl FromApubToForm<NoteExt> for PrivateMessageForm {
async fn from_apub(
note: &NoteExt,
context: &LemmyContext,
expected_domain: Url,
request_counter: &mut i32,
) -> Result<PrivateMessageForm, LemmyError> {
let creator_actor_id = note
.attributed_to()
.context(location_info!())?
.clone()
.single_xsd_any_uri()
.context(location_info!())?;
let creator =
get_or_fetch_and_upsert_person(&creator_actor_id, context, request_counter).await?;
let recipient_actor_id = note
.to()
.context(location_info!())?
.clone()
.single_xsd_any_uri()
.context(location_info!())?;
let recipient =
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)?)?;
let content = get_source_markdown_value(note)?.context(location_info!())?;
Ok(PrivateMessageForm {
creator_id: creator.id,
recipient_id: recipient.id,
content,
published: note.published().map(|u| u.to_owned().naive_local()),
updated: note.updated().map(|u| u.to_owned().naive_local()),
deleted: None,
read: None,
ap_id: Some(check_object_domain(note, expected_domain)?),
local: false,
})
}
}

80
crates/apub/src/routes.rs Normal file
View file

@ -0,0 +1,80 @@
use crate::{
http::{
comment::get_apub_comment,
community::{
get_apub_community_followers,
get_apub_community_http,
get_apub_community_inbox,
get_apub_community_outbox,
},
get_activity,
person::{get_apub_person_http, get_apub_person_inbox, get_apub_person_outbox},
post::get_apub_post,
},
inbox::{
community_inbox::community_inbox,
person_inbox::person_inbox,
shared_inbox::shared_inbox,
},
APUB_JSON_CONTENT_TYPE,
};
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());
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));
cfg
.service(
web::scope("/")
.guard(header_guard_accept)
.route(
"/c/{community_name}",
web::get().to(get_apub_community_http),
)
.route(
"/c/{community_name}/followers",
web::get().to(get_apub_community_followers),
)
.route(
"/c/{community_name}/outbox",
web::get().to(get_apub_community_outbox),
)
.route(
"/c/{community_name}/inbox",
web::get().to(get_apub_community_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)),
)
// Inboxes dont work with the header guard for some reason.
.service(
web::scope("/")
.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(person_inbox))
.route("/inbox", web::post().to(shared_inbox)),
);
}
}

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