Compare commits

...

113 commits

Author SHA1 Message Date
Dessalines b2a480f55c
Fixing sed command for postgres upgrade. (#4791)
- Context: https://github.com/LemmyNet/lemmy-ansible/issues/245
2024-06-07 12:39:23 -04:00
Dessalines 9236cf7d21
Remove ansible tagging lines. (#4790) 2024-06-07 11:26:43 -04:00
dullbananas b559e0206b
Replace wav with hound (#4788)
* Update lib.rs

* Update Cargo.toml

* Update lib.rs

* cargo.lock

* fix simultaneous mutable references
2024-06-07 10:27:49 -04:00
Dessalines f5f2b5ffc6 Version 0.19.4 2024-06-07 07:51:56 -04:00
dullbananas 1e11faf741
Improve comment in triggers.sql (#4789)
* Clarified existing info
* Added prohibition of inconsistent update order
2024-06-07 07:42:34 -04:00
Dessalines 5d31f0d516 Version 0.19.4-rc.11 2024-06-06 23:02:38 -04:00
Dessalines 844b84a01a Version 0.19.4-rc.10 2024-06-06 20:46:03 -04:00
Dessalines b0447ad94d
Upgrading lemmy-js-client version to 0.19.4 (#4787)
* Upgrading lemmy-js-client version to 0.19.4

* Upgrading deps before renovate.
2024-06-06 20:44:36 -04:00
Dessalines 3d25322089 Version 0.19.4-rc.9 2024-06-06 10:43:40 -04:00
Dessalines 16a82862b8
Allow empty string to clear URL-type DB fields. (#4780)
* Allow empty string to clear URL-type DB fields.

- To address difficulties with clearing URL-type fields like
  avatars, banners, site icons, this PR turns the URL type form
  fields into strings.
- This allows an empty string to be used as a "clear data", as
  in the case with the regular text form fields.
- Also includes various cleanups.
- Fixes #4777
- Context: #2287

* Fixing comment.

* Use Option<&str> and deref.

---------

Co-authored-by: SleeplessOne1917 <28871516+SleeplessOne1917@users.noreply.github.com>
2024-06-06 09:55:08 -04:00
dullbananas 79e6dbf0de
Remove PersonInsertForm builder (#4779)
* Update session_middleware.rs

* Update private_message_report_view.rs

* Update session_middleware.rs

* Update private_message_view.rs

* Update private_message.rs

* Update registration_application_view.rs

* Update actor_language.rs

* Update vote_view.rs

* Update code_migrations.rs

* Update comment_aggregates.rs

* Update person_view.rs

* Update user_settings_backup.rs

* Update person.rs

* Update create.rs

* Update comment_view.rs

* Update moderator.rs

* Update site_aggregates.rs

* Update claims.rs

* Update community_aggregates.rs

* Update post_report.rs

* Update person_mention_view.rs

* Update community_view.rs

* Update comment_report_view.rs

* Update post_report_view.rs

* Update community_moderators.rs

* Update comment.rs

* Update person_aggregates.rs

* Update comment_reply_view.rs

* Update password_reset_request.rs

* Update post_aggregates.rs

* Update community.rs

* Update main.rs

* Update post.rs

* Update person.rs

* Update person.rs

* Update claims.rs

* Update person.rs

* Update create.rs

* Update user_settings_backup.rs

* Update community_moderators.rs

* Update main.rs

* Update comment_aggregates.rs

* Update community_aggregates.rs

* Update person.rs

* Update Cargo.toml

* Update Cargo.toml

* Update person.rs

* fix

* Update code_migrations.rs

* fix submodule

* Update person.rs
2024-06-06 08:29:18 -04:00
Dessalines fda5ce4482 Version 0.19.4-rc.8 2024-06-05 19:01:37 -04:00
Dessalines e8cfb5665f
When banning from local communities, make sure they aren't deleted or removed. (#4784)
- This is causing some federation issues.
- Context: #4782
2024-06-05 18:59:46 -04:00
Nutomic bb94fb1c79
Revert apub library 0.5.7 (#4783)
Wasnt necessary after all
2024-06-05 18:04:02 -04:00
Dessalines 92214a9364 Version 0.19.4-rc.7 2024-06-05 17:30:43 -04:00
Nutomic 78ae874b89
Apub library 0.5.7 (#4781) 2024-06-05 17:28:33 -04:00
Dessalines a947474c64 Version 0.19.4-rc.6 2024-06-04 08:32:08 -04:00
Dessalines 8bf17946bd
Fix issue with avatar / icon deletion when saving settings. (#4774)
* Fix issue with avatar / icon deletion when saving settings.

- Fixes #4763

* Update crates/api_common/src/request.rs

Co-authored-by: dullbananas <dull.bananas0@gmail.com>

* Fixing an existing test, and adding another for replace images.

---------

Co-authored-by: dullbananas <dull.bananas0@gmail.com>
2024-06-04 08:28:22 -04:00
dullbananas 9ceb5b6386
Clean up build_update_instance_form in scheduled_tasks.rs (#4775)
* Clean up build_update_instance_form in scheduled_tasks.rs

* remove unused import
2024-06-04 08:04:16 -04:00
Dessalines aefb41b551
Remove .json from nodeinfo urls, according to spec. (#4773) 2024-06-03 17:30:16 -04:00
Dessalines 4195a9b5a1
Fetch nodeinfo href from .well-known/nodeinfo . Fixes #4757 (#4765)
* Fetch nodeinfo href from .well-known/nodeinfo . Fixes #4757

* Addressing PR comments.

* Fixing clippy.

* Adding tests.
2024-06-03 17:30:00 -04:00
Dessalines 69b4c6647b Version 0.19.4-rc.5 2024-06-01 13:30:00 -04:00
renovate[bot] f7fe0d46fc
Update Rust crate console-subscriber to 0.2.0 (#4771)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-05-31 22:35:34 -04:00
renovate[bot] 609a6411a7
Update pnpm to v9.1.4 (#4770)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-05-31 22:24:17 -04:00
renovate[bot] 44666a34a2
Update dependency ts-jest to v29.1.4 (#4768)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-05-31 22:04:44 -04:00
renovate[bot] 6db878f761
Update dependency typescript to v5.4.5 (#4769)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-05-31 21:34:29 -04:00
renovate[bot] 6031709fcf
Update Rust crate serde to v1.0.203 (#4766)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-05-31 21:14:05 -04:00
renovate[bot] 4d9e38d875
Update asonix/pictrs Docker tag to v0.5.14 (#4767)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-05-31 21:13:39 -04:00
Dessalines 6a6c915014
Changing NodeInfo metadata to HashMap from vector. Fixes #4762 (#4764) 2024-05-31 16:38:46 -04:00
phiresky 96b7afc0b1
upgrade rust to 1.78 to fix diesel cli (#4761) 2024-05-31 08:39:45 -04:00
Felix Ableitner d2083f79d9 Version 0.19.4-rc.4 2024-05-30 11:55:34 +02:00
phiresky e8a7bb07a3
fix both permanent stopping of federation queues and multiple creation of the same federation queues (#4754)
Co-authored-by: Nutomic <me@nutomic.com>
2024-05-30 05:08:27 -04:00
Richard Schwab 91e57ff954
Prevent bot replies from increasing unread reply count when bot accounts are not shown (#4747)
* Prevent bot replies from increasing unread reply count when bot accounts are not shown

* Pass LocalUser for unread replies count query

* Prevent bot mentions from increasing unread reply count when bot accounts are not shown
2024-05-29 17:55:15 -04:00
phiresky 7d80a3c7d6
replace instanceid with domain (#4753) 2024-05-29 23:10:25 +02:00
Dessalines abcfa266af
Fixing slowness in saved post fetching. #4756 (#4758)
* Fixing slowness in saved post fetching. #4756

* Also fix comment_view.rs
2024-05-29 17:03:42 -04:00
SleeplessOne1917 51970ffc81
Update dependencies to alleviate cargo audit peer dependency vulnerability (#4750) 2024-05-28 17:47:21 -07:00
Dessalines fd6a1283a5 Version 0.19.4-rc.3 2024-05-27 09:37:58 -04:00
Nutomic af034f3b5e
Unit tests and cleanup for outgoing federation code (#4733)
* test setup

* code cleanup

* cleanup

* move stats to own file

* basic test working

* cleanup

* processes test

* more test cases

* fmt

* add file

* add assert

* error handling

* fmt

* use instance id instead of domain for stats channel
2024-05-27 09:34:58 -04:00
Dessalines 0d5db29bc9
After creating a comment, update the unread comments for the post. (#4742)
* After creating a comment, update the unread comments for the post.

- Fixes #3863

* Addressing PR comments.

* Add comment.

---------

Co-authored-by: SleeplessOne1917 <28871516+SleeplessOne1917@users.noreply.github.com>
2024-05-27 12:55:44 +02:00
dullbananas ec77c00ef8
Fix lost separation caused by comment width change (#4739)
* Update post_view.rs

* Update structs.rs

* Update worker.rs

* Update worker.rs
2024-05-23 14:05:35 -04:00
Dessalines 69bdcb3069 Version 0.19.4-rc.2 2024-05-23 12:10:33 -04:00
Dessalines 6a6108ac55
Fixing proxied images for federated posts. (#4737)
* Fixing proxied images for federated posts.

- Also added test.
- Fixes #4736

* Address PR comments.
2024-05-23 11:11:25 -04:00
Nutomic b2c1a14234
Correct url for nodeinfo version (#4734)
* Correct url for nodeinfo version

* add compat redirect

---------

Co-authored-by: SleeplessOne1917 <28871516+SleeplessOne1917@users.noreply.github.com>
2024-05-23 10:59:56 -04:00
Nutomic d8dc38eb06
Upgrade dependencies (#4740) 2024-05-23 10:55:20 -04:00
Nutomic c96017c009
Configure max comment width in clippy (#4738)
* Configure max comment width in clippy

* update default config
2024-05-23 08:46:26 -04:00
Dessalines 9aa565b559 Version 0.19.4-rc.1 2024-05-22 08:58:31 -04:00
Dessalines 7d7cd8ded4
Dont show replies / mentions from blocked users. Fixes #4227 (#4727)
* Dont show replies / mentions from blocked users. Fixes #4227

* Adding unit tests for reply and mention views.

- Also cleaned up some unwraps in the tests.

* Add allow deprecated to pass clippy for deprecated wav crate.

---------

Co-authored-by: SleeplessOne1917 <28871516+SleeplessOne1917@users.noreply.github.com>
2024-05-22 08:50:26 -04:00
Nutomic 943c31cc72
Allow passing command line params via environment (fixes #4603) (#4729)
* Allow passing command line params via environment (fixes #4603)

* add prefix

---------

Co-authored-by: SleeplessOne1917 <28871516+SleeplessOne1917@users.noreply.github.com>
2024-05-22 08:39:01 -04:00
Nutomic 973f39601c
Dont allow removing comment which was deleted (fixes #4731) (#4732) 2024-05-22 08:29:01 -04:00
Felix Ableitner a39c19c9db Version 0.19.4-beta.8 2024-05-22 10:30:38 +02:00
Dessalines 55f84dd38a
Fixing proxy images (#4722)
* Adding an image_details table to store image dimensions.

- Adds an image_details table, which stores the height,
  width, and content_type for local and remote images.
- For LocalImages, this information already comes back with
  the upload.
- For RemoteImages, it calls the pictrs details endpoint.
- Fixed some issues with proxying non-image urls.
- Fixes #3328
- Also fixes #4703

* Running sql format.

* Running fmt.

* Don't fetch metadata in background for local API requests.

* Dont export remote_image table to typescript.

* Cleaning up validate.

* Dont proxy url.

* Fixing tests, fixing issue with federated thumbnails.

* Fix tests.

* Updating corepack, fixing issue.

* Refactoring image inserts to use transactions.

* Use select exists again.

* Fixing imports.

* Fix test.

* Removing pointless backgrounded metadata generation version.

* Removing public pictrs details route.

* Fixing clippy.

* Fixing proxy image fetching. Fixes #4703

- This extracts only the proxy image fixes from #4704, leaving off
  thumbnails.

* Fix test.

* Addressing PR comments.

* Address PR comments 2.

---------

Co-authored-by: SleeplessOne1917 <28871516+SleeplessOne1917@users.noreply.github.com>
2024-05-22 10:28:47 +02:00
Nutomic 6b46a70535
Extra logging to debug duplicate activities (ref #4609) (#4726)
* Extra logging to debug duplicate activities (ref #4609)

* Fix logging for api tests

* fmt
2024-05-21 14:47:06 -04:00
Nutomic 4ffaa93431
Dont allow reusing password reset token, use normal rate limit (#4719)
* Dont allow reusing password reset token, use normal rate limit

* fix
2024-05-21 14:46:49 -04:00
flamingos-cant a0ad7806cb
Increase alt_text size to 1500 (#4724) 2024-05-17 13:03:19 -04:00
Nutomic 99aac07714
Mark database fields as sensitive so they dont show up in logs (#4720)
* Mark database fields as sensitive so they dont show up in logs

* add file

* fix test

* Update crates/apub/src/objects/person.rs

Co-authored-by: SleeplessOne1917 <28871516+SleeplessOne1917@users.noreply.github.com>

* Update crates/apub/src/objects/community.rs

Co-authored-by: SleeplessOne1917 <28871516+SleeplessOne1917@users.noreply.github.com>

* Update crates/apub/src/objects/instance.rs

Co-authored-by: SleeplessOne1917 <28871516+SleeplessOne1917@users.noreply.github.com>

---------

Co-authored-by: SleeplessOne1917 <28871516+SleeplessOne1917@users.noreply.github.com>
Co-authored-by: Dessalines <dessalines@users.noreply.github.com>
2024-05-16 20:41:57 -04:00
Dessalines 1a4aa3eaba
Stop using a diesel_cli docker image, use cargo-install in woodpecker. (#4723)
* Stop using a diesel_cli docker image, use cargo-binstall in woodpecker.

- The diesel_cli image is 500MB, and rebuilt daily. Much easier to use
  binstall to install it.

* Trying out a multiline var.

* Try sequence merges 1

* Try removing features.

* Try path fix.

* Abstracting diesel cli install.

* Try installing mysql and postgres.

* Try installing mysql and postgres 2.

* Try installing mysql and postgres 3.

* Try installing mysql and postgres 4.

* Try installing mysql and postgres 5.

* Try installing mysql and postgres 6.

* Try installing mysql and postgres 7.

* Try installing mysql and postgres 8.

* Try installing mysql and postgres 9.

* Try installing mysql and postgres 10.

* Try installing mysql and postgres 11.

* Try installing mysql and postgres 12.

* Try installing mysql and postgres 13.

* Add logging line.

* Add logging line 2.

* Add logging line 3.

* Add logging line 4.

* Removing binstall.

* Extract taplo into its own image, ignore binstall.

* Use a smaller taplo.

* taplo is the same image.
2024-05-16 16:41:36 -04:00
Nutomic 93c9a5f2b1
Dont federate post locking via Update activity (#4717)
* Dont federate post locking via Update activity

* cleanup

* add missing mod log entries

* update assets
2024-05-15 07:36:00 -04:00
Nutomic 9a9d518153
Fix import blocked objects (#4712)
* Allow importing partial backup (fixes #4672)

* Fetch blocked objects if not known locally (fixes #4669)

* extract helper fn

* add comment

* cleanup

* remove test

* fmt

* remove .ok()
2024-05-14 23:03:43 -04:00
Nutomic 7fb03c502e
Add test to ensure reports are sent to user's home instance (ref #4701) (#4711)
* Add test to ensure reports are sent to user's home instance (ref #4701)

* enable all tests

* set package-manager-strict=false
2024-05-14 22:48:24 -04:00
Nutomic 49bb17b583
Stricter rate limit for login (#4718) 2024-05-14 22:43:43 -04:00
Nutomic 723cb549d4
Allow importing partial backup (fixes #4672) (#4705)
* Allow importing partial backup (fixes #4672)

* Dont throw error on empty LocalUser::update

* fix tests
2024-05-14 22:37:30 -04:00
Nutomic 8b6a4c060e
Make nodeinfo standard compliant, upgrade to nodeinfo 2.1 (fixes #4702) (#4706)
* Always set activitypub protocol in nodeinfo response (fixes #4702)

* Add mandatory fields
2024-05-13 22:53:20 -04:00
Dessalines cb80980027 Version 0.19.4-beta.7 2024-05-11 13:51:09 -04:00
dullbananas c4fc3a8ede
Optimize stuff in attempt to fix high amount of locks, and fix comment_aggregates.child_count (#4696)
* separate triggers

* auto_explain.log_triggers=on

* Revert "auto_explain.log_triggers=on"

This reverts commit 078b2dbb9b.

* Revert "separate triggers"

This reverts commit 95600da4af.

* bring back migration

* re-order statements

* add comment about statement ordering

* no redundant updates

* optimize post_aggregates update in comment trigger

* set comment path in trigger

* update comment_aggregates.child_count using trigger

* move `LEFT JOIN post` to inner query

* clean up newest_comment_time_necro

* add down.sql
2024-05-09 08:18:55 -04:00
Nutomic b4f9ef24a5
Dont exit early when running only scheduled tasks (#4707)
* Dont exit early when running only scheduled tasks (fixes #4709)

* fix
2024-05-08 14:56:44 +02:00
Nutomic 866d752a3c
Instance.preferred_username should be optional (fixes #4701) (#4713) 2024-05-08 08:01:04 -04:00
Nutomic e0b1d0553d
Add timeout for processing incoming activities (#4708)
* Add timeout for processing incoming activities

* move to const
2024-05-08 08:00:55 -04:00
Nutomic 7c146272c3
Federate with wordpress, improvements for NodeBB, Discourse federation (#4692)
* Federate with wordpress

* upgrade apub lib with fix

* Also read post's community from `audience`

* cleanup

* cargo update

* upgrade apub lib

* add wordpress test activity
2024-05-07 16:20:43 -04:00
Nutomic cfdc732d3a
On registration set show_nsfw based on site.content_warning (#4616)
Co-authored-by: SleeplessOne1917 <28871516+SleeplessOne1917@users.noreply.github.com>
2024-05-07 16:18:58 -04:00
Tim Coombs 522f974e30
fix: use docker compose v2 (#4622)
* fix: use docker compose v2

* Using sudo tee.

* fix: correct postgres sed command

---------

Co-authored-by: Dessalines <tyhou13@gmx.com>
Co-authored-by: SleeplessOne1917 <28871516+SleeplessOne1917@users.noreply.github.com>
2024-05-07 11:41:40 +02:00
SleeplessOne1917 b152be7951
Update rustls (#4690)
* Update rustls

* Format code
2024-05-03 16:06:14 -04:00
SleeplessOne1917 485b0f1a54
Replace unmaintained encoding dep with maintained encoding_rs dep (#4694)
* Replace dependency on unmaintained encoding crate with dep on maintained encoding_rs crate

* Update lockfile

* Taplo format Cargo.toml

* Use better variable name

* Replace into_owned with into
2024-05-03 10:42:48 +00:00
Nutomic 7540b02723
Update activitypub library (#4691)
https://github.com/LemmyNet/activitypub-federation-rust/releases/tag/0.5.5
2024-05-02 12:46:34 -04:00
Nutomic 7746db4169
Testing and minor fix for federation with Discourse (#4628)
* Testing and minor fix for federation with Discourse

* prettier
2024-05-02 07:49:19 -04:00
Dessalines db2ce81fc4
Show trigger logging. #4681 (#4688) 2024-05-01 18:46:14 -04:00
renovate[bot] 4175a1af80
chore(deps): update rust crate serde_with to 3.8.1 (#4687)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-04-30 23:47:02 -04:00
renovate[bot] 563280456e
chore(deps): update pnpm to v9.0.6 (#4682)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-04-30 23:43:14 -04:00
Dessalines 2fecb7ecdf
Dont show own comments for liked and disliked_only. Fixes #4675 (#4679)
Co-authored-by: SleeplessOne1917 <28871516+SleeplessOne1917@users.noreply.github.com>
2024-04-30 23:26:55 -04:00
renovate[bot] 2c6f9c7fd5
chore(deps): update rust crate serde to 1.0.199 (#4684)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-04-30 23:17:23 -04:00
renovate[bot] e338e59868
fix(deps): update rust crate lettre to 0.11.7 (#4685)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-04-30 22:54:16 -04:00
renovate[bot] b0caa85ed4
chore(deps): update rust crate base64 to 0.22.1 (#4683)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-04-30 22:30:19 -04:00
Dessalines ad60d91f5c
Dont publish lemmy_db_perf to fix crates.io publish. Fixes #4678 (#4680) 2024-04-30 17:51:12 +00:00
Dessalines 6423d2dde5 Version 0.19.4-beta.6 2024-04-30 06:38:44 -04:00
Nutomic 12163701e7
Avoid crash when handling urls without domain (#4676)
* Avoid crash when handling urls without domain

* Add some extra checks
2024-04-30 06:33:37 -04:00
Dessalines 5c35e97a75
Dont show deleted / removed posts when searching. Fixes #4576 (#4671)
* Dont show deleted / removed posts when searching. Fixes #4576

* Address PR comments.

* Clean up comment removed also.

---------

Co-authored-by: SleeplessOne1917 <28871516+SleeplessOne1917@users.noreply.github.com>
2024-04-30 12:24:18 +02:00
SleeplessOne1917 b05f221565
Remove login step from publish to crates.io (#4674) 2024-04-30 00:07:03 +00:00
Nutomic beec080274
Testing for federation with NodeBB, make community.followers_url optional (#4629)
* Testing for federation with NodeBB, make community.followers_url optional

* clippy
2024-04-29 12:34:11 +02:00
Dessalines 492d8f1b01
Fix communities with broken outboxes, and use PostView. Fixes #4658 (#4668)
* Fix communities with broken outboxes, and use PostView. Fixes #4658

* Fixing tests.

* Dont pass ref and clone.
2024-04-29 12:22:00 +02:00
dullbananas d3737d4453
Optimize actor_language.rs (#4612)
* Remove useless transaction in actor_language.rs

* Update actor_language.rs

* site

* community

* Update actor_language.rs

* Update actor_language.rs

* Update actor_language.rs

* Update actor_language.rs

* Update actor_language.rs
2024-04-27 10:59:58 -04:00
Dessalines b459949f57 Version 0.19.4-beta.5 2024-04-25 19:59:24 -04:00
Dessalines 93f5df2d2a
Adding post_id desc to all post_aggregates indexes. Fixes #4618 (#4662)
* Adding post_id desc to all post_aggregates indexes. Fixes #4618

* Running pg_format

* Not rebuilding indexes which had no changes.
2024-04-25 18:19:02 -04:00
Nutomic cf426493e1
Fix community add mod check (fixes #4624) (#4667) 2024-04-25 11:47:38 -04:00
Dessalines 8e3ff0408e
Fixing extra modlog entries when post_id or comment_id is given. (#4664)
- Previously when given a post_id, it didn't filter out any other
  modlog entries, such as community removals. This fixes that problem.
- Context: https://github.com/LemmyNet/lemmy-ui/pull/2437

Co-authored-by: SleeplessOne1917 <28871516+SleeplessOne1917@users.noreply.github.com>
2024-04-25 10:26:17 +02:00
Dessalines 66e06b3952
Removing scheme from block urls. Fixes #4656 (#4659)
* Removing scheme from block urls. Fixes #4656

* Fix comment.

* Fixing domain checking.

* Removing pointless URL building in url blocklist regex.

* Remove trailing /
2024-04-23 23:15:20 -04:00
Kroese 6b9d9dfaa5
Fix broken thumbnails (#4661)
* Check is_image_post flag

* Keep cargo_fmt happy

* Filter on is_image_post

* Trigger CI

* Keep cargo_fmt happy
2024-04-23 22:52:56 -04:00
tracyspacy 0eaf8d33e7
Filter_removed_comments_from_search (#4634)
* filter_removed_comments_from_search

* Revert "filter_removed_comments_from_search"

This reverts commit c6d6490afa.

* filtering_removed_comments_search

* filter_deleted_comments

* Revert "filter_deleted_comments"

This reverts commit 7dc1d13d24.

* Revert "filtering_removed_comments_search"

This reverts commit 6e9b1de7a2.

* filtering_removed_dELeted_comments_search
2024-04-22 11:33:02 -04:00
renovate[bot] c31a29ec7f
chore(deps): update dependency @types/node to v20.12.7 (#4647)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-04-19 18:56:35 -04:00
renovate[bot] 80635c9e24
chore(deps): update rust crate base64 to 0.22.0 (#4651)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-04-19 18:40:38 -04:00
renovate[bot] 95d75e07b2
chore(deps): update pnpm to v9.0.4 (#4649)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-04-19 18:23:06 -04:00
renovate[bot] efbfdc9340
chore(deps): update docker/dockerfile docker tag to v1.7 (#4650)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-04-19 18:12:26 -04:00
renovate[bot] 1ae3aab764
chore(deps): update dependency typescript to v5.4.5 (#4648)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-04-19 18:11:21 -04:00
renovate[bot] f68881c552
chore: Configure Renovate (#4644)
* Add renovate.json

* Updating renovate.

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Dessalines <tyhou13@gmx.com>
Co-authored-by: Dessalines <dessalines@users.noreply.github.com>
2024-04-19 17:00:23 -04:00
Dessalines 2ba1ba88b8
Upgrading deps. (#4645) 2024-04-19 16:50:27 -04:00
Dessalines 079fa0b8f6 Version 0.19.4-beta.4 2024-04-18 21:11:15 -04:00
dependabot[bot] b0a740d5c5
Bump h2 from 0.3.25 to 0.3.26 (#4639)
Bumps [h2](https://github.com/hyperium/h2) from 0.3.25 to 0.3.26.
- [Release notes](https://github.com/hyperium/h2/releases)
- [Changelog](https://github.com/hyperium/h2/blob/v0.3.26/CHANGELOG.md)
- [Commits](https://github.com/hyperium/h2/compare/v0.3.25...v0.3.26)

---
updated-dependencies:
- dependency-name: h2
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-18 21:05:45 -04:00
Kroese ee46242a43
Update aarch64-lemmy-linux-gnu to v0.3.0 (#4638) 2024-04-18 20:34:55 -04:00
dullbananas 4ba6221e04
Move SQL triggers from migrations into reusable sql file (#4333)
* stuff

* stuff including batch_upsert function

* stuff

* do things

* stuff

* different timestamps

* stuff

* Revert changes to comment.rs

* Update comment.rs

* Update comment.rs

* Update post_view.rs

* Update utils.rs

* Update up.sql

* Update up.sql

* Update down.sql

* Update up.sql

* Update main.rs

* use anyhow macro

* Create down.sql

* Create up.sql

* Create replaceable_schema.sql

* Update replaceable_schema.sql

* Update replaceable_schema.sql

* Update replaceable_schema.sql

* Update utils.rs

* Update .woodpecker.yml

* Update sql_format_check.sh

* Update replaceable_schema.sql

* Update replaceable_schema.sql

* Update replaceable_schema.sql

* Update replaceable_schema.sql

* Update replaceable_schema.sql

* Update replaceable_schema.sql

* Update replaceable_schema.sql

* Create dump_schema.sh

* Update start_dev_db.sh

* Update replaceable_schema.sql

* Update replaceable_schema.sql

* Update replaceable_schema.sql

* Update replaceable_schema.sql

* Update replaceable_schema.sql

* Update replaceable_schema.sql

* Update replaceable_schema.sql

* Update replaceable_schema.sql

* Update replaceable_schema.sql

* Update replaceable_schema.sql

* Update replaceable_schema.sql

* stuff

* Update replaceable_schema.sql

* Update .pg_format

* fmt

* stuff

* stuff (#21)

* Update replaceable_schema.sql

* Update up.sql

* Update replaceable_schema.sql

* fmt

* update cargo.lock

* stuff

* Update replaceable_schema.sql

* Remove truncate trigger because truncate is already restricted by foreign keys

* Update replaceable_schema.sql

* fix some things

* Update replaceable_schema.sql

* Update replaceable_schema.sql

* Update .woodpecker.yml

* stuff

* fix TG_OP

* Psql env vars

* try to fix combine_transition_tables parse error

* Revert "try to fix combine_transition_tables parse error"

This reverts commit 75d00a4626.

* refactor combine_transition_tables

* try to fix create_triggers

* fix some things

* try to fix combined_transition_tables

* fix sql errors

* update comment count in post trigger

* fmt

* Revert "fmt"

This reverts commit a5bcd0834b.

* Revert "update comment count in post trigger"

This reverts commit 0066a4b42b.

* fix everything

* Update replaceable_schema.sql

* actually fix everything

* refactor create_triggers

* fix

* add semicolons

* add is_counted function and fix incorrect bool operator in update_comment_count_from_post

* refactor comment trigger

* refactor post trigger

* fix

* Delete crates/db_schema/src/utils/series.rs

* subscribers_local

* edit migrations

* move migrations

* remove utils::series module declaration

* fix everything

* stuff

* Move sql to schema_setup dir

* utils.sql

* delete .pg_format

* Update .woodpecker.yml

* Update sql_format_check.sh

* Update .woodpecker.yml

* Merge remote-tracking branch 'upstream/main' into bliss

* fmt

* Create main.rs

* Update lib.rs

* Update main.rs

* Update .woodpecker.yml

* Update main.rs

* Update Cargo.toml

* Update .woodpecker.yml

* Update .woodpecker.yml

* Update triggers.sql

* YAY

* Update mod.rs

* Update Cargo.toml

* a

* Update Cargo.toml

* Update Cargo.toml

* Delete crates/db_schema/src/main.rs

* Update Cargo.toml

* Update .woodpecker.yml

* Update .woodpecker.yml

* Update .woodpecker.yml

* Update .woodpecker.yml

* Update .woodpecker.yml

* Update .woodpecker.yml

* Update .woodpecker.yml

* Update utils.sql

* Update utils.sql

* Update .woodpecker.yml

* Update .woodpecker.yml

* Update .woodpecker.yml

* Update .woodpecker.yml

* Update .woodpecker.yml

* Update down.sql

* Update up.sql

* Update triggers.sql

* Update .woodpecker.yml

* Update .woodpecker.yml

* Update triggers.sql

* Update down.sql

* Update .woodpecker.yml

* Update Cargo.toml

* Update .woodpecker.yml

* Update Cargo.toml

* Update .woodpecker.yml

* Update .woodpecker.yml

* Update .woodpecker.yml

* Update .woodpecker.yml

* Update .woodpecker.yml

* Update .woodpecker.yml

* Update .woodpecker.yml

* Update .woodpecker.yml

* Update .woodpecker.yml

* Update .woodpecker.yml

* Update .woodpecker.yml

* Update .woodpecker.yml

* Update .woodpecker.yml

* Update .woodpecker.yml

* Update .woodpecker.yml

* Update mod.rs

* Update Cargo.toml

* Update mod.rs

* make dump_schema.sh executable

* fix dump_schema.sh

* defer

* diff dumps

* fmt

* Update utils.sql

* Update .woodpecker.yml

* use correct version for pg_dump

* Update .woodpecker.yml

* Update .woodpecker.yml

* change migration date

* atomic site_aggregates insert

* temporarily repeat tests in CI

* drop r schema in CI migration check

* show ReceivedActivity::create error

* move check_diesel_migration CI step

* Update .woodpecker.yml

* Update scheduled_tasks.rs

* Update scheduled_tasks.rs

* update cargo.lock

* move sql files

* move rank functions

* filter post_aggregates update

* fmt

* cargo fmt

* replace post_id with id

* update cargo.lock

* avoid locking rows that need no change in up.sql

* only run replaceable_schema if migrations were run

* debug ci test failure

* make replaceable_schema work in CI

* Update .woodpecker.yml

* remove println

* Use migration revert and git checkout

* Update schema_setup.rs

* Fix

* Update schema_setup.rs

* Update schema_setup.rs

* Update .woodpecker.yml

---------

Co-authored-by: Nutomic <me@nutomic.com>
Co-authored-by: Dessalines <dessalines@users.noreply.github.com>
2024-04-17 20:58:44 -04:00
Nutomic 31829b6c05
Untangle thumbnail generation logic (ref #4604) (#4615)
* Untangle thumbnail generation logic (ref #4604)

* prettier

* test cleanup

* fix tests

* also consider opengraph image for local thumbnail generation
2024-04-17 10:36:45 -04:00
TechVest b0370ae2fd
chore: fix some comments (#4637)
Signed-off-by: TechVest <techdashen@qq.com>
2024-04-17 14:35:54 +02:00
Dessalines 6efab9aab1
Creating a LocalImageView, so that front ends have the Person struct. (#4631)
* Creating a LocalImageView, so that front ends have the Person struct.

* Removing local_user from LocalImageView.

* Add uploader check.
2024-04-16 19:20:44 -04:00
Dessalines d075acce43
Make all single-fetch database calls return an Option. (#4617)
- Diesel ordinarily throws an error when no results are returned for a
  single fetch, which is a bit confusing. This PR ensures that the
  missing value cases are all caught, and wrapped with new LemmyErrors,
  rather than diesel errors.
- Fixes #4601
2024-04-16 14:48:15 +02:00
Nutomic 3a0c1dca90
Avoid overwriting local objects via federation (#4611)
* Dont allow federation to overwrite local objects

* is_local check in apub lib

* use imports

* fix check, update lib

* use verify_is_remote_object()

* submodule
2024-04-11 10:05:49 -04:00
dullbananas 0f6b13a4ec
Test coverage (#4596)
* update .gitignore

* add test-with-coverage.sh

* coverage gutters extension comment

* move lcov.info to target folder

---------

Co-authored-by: SleeplessOne1917 <28871516+SleeplessOne1917@users.noreply.github.com>
2024-04-11 10:32:07 +02:00
294 changed files with 10369 additions and 6012 deletions

View file

@ -3,3 +3,5 @@ edition = "2021"
imports_layout = "HorizontalVertical" imports_layout = "HorizontalVertical"
imports_granularity = "Crate" imports_granularity = "Crate"
group_imports = "One" group_imports = "One"
wrap_comments = true
comment_width = 100

View file

@ -2,7 +2,8 @@
# See https://github.com/woodpecker-ci/woodpecker/issues/1677 # See https://github.com/woodpecker-ci/woodpecker/issues/1677
variables: variables:
- &rust_image "rust:1.77" - &rust_image "rust:1.78"
- &rust_nightly_image "rustlang/rust:nightly"
- &install_pnpm "corepack enable pnpm" - &install_pnpm "corepack enable pnpm"
- &slow_check_paths - &slow_check_paths
- event: pull_request - event: pull_request
@ -24,15 +25,17 @@ variables:
"diesel.toml", "diesel.toml",
".gitmodules", ".gitmodules",
] ]
- install_binstall: &install_binstall
# Broken for cron jobs currently, see - wget https://github.com/cargo-bins/cargo-binstall/releases/latest/download/cargo-binstall-x86_64-unknown-linux-musl.tgz
# https://github.com/woodpecker-ci/woodpecker/issues/1716 - tar -xvf cargo-binstall-x86_64-unknown-linux-musl.tgz
# clone: - cp cargo-binstall /usr/local/cargo/bin
# git: - install_diesel_cli: &install_diesel_cli
# image: woodpeckerci/plugin-git - apt update && apt install -y lsb-release build-essential
# settings: - sh -c 'echo "deb https://apt.postgresql.org/pub/repos/apt $(lsb_release -cs)-pgdg main" > /etc/apt/sources.list.d/pgdg.list'
# recursive: true - wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | apt-key add -
# submodule_update_remote: true - apt update && apt install -y postgresql-client-16
- cargo install diesel_cli --no-default-features --features postgres
- export PATH="$CARGO_HOME/bin:$PATH"
steps: steps:
prepare_repo: prepare_repo:
@ -66,7 +69,7 @@ steps:
- event: pull_request - event: pull_request
cargo_fmt: cargo_fmt:
image: rustlang/rust:nightly image: *rust_nightly_image
environment: environment:
# store cargo data in repo folder so that it gets cached between steps # store cargo data in repo folder so that it gets cached between steps
CARGO_HOME: .cargo_home CARGO_HOME: .cargo_home
@ -77,11 +80,9 @@ steps:
- event: pull_request - event: pull_request
cargo_machete: cargo_machete:
image: rustlang/rust:nightly image: *rust_nightly_image
commands: commands:
- wget https://github.com/cargo-bins/cargo-binstall/releases/latest/download/cargo-binstall-x86_64-unknown-linux-musl.tgz - <<: *install_binstall
- tar -xvf cargo-binstall-x86_64-unknown-linux-musl.tgz
- cp cargo-binstall /usr/local/cargo/bin
- cargo binstall -y cargo-machete - cargo binstall -y cargo-machete
- cargo machete - cargo machete
when: when:
@ -133,26 +134,17 @@ steps:
when: *slow_check_paths when: *slow_check_paths
check_diesel_schema: check_diesel_schema:
image: willsquire/diesel-cli image: *rust_image
environment: environment:
CARGO_HOME: .cargo_home CARGO_HOME: .cargo_home
DATABASE_URL: postgres://lemmy:password@database:5432/lemmy DATABASE_URL: postgres://lemmy:password@database:5432/lemmy
commands: commands:
- <<: *install_diesel_cli
- diesel migration run - diesel migration run
- diesel print-schema --config-file=diesel.toml > tmp.schema - diesel print-schema --config-file=diesel.toml > tmp.schema
- diff tmp.schema crates/db_schema/src/schema.rs - diff tmp.schema crates/db_schema/src/schema.rs
when: *slow_check_paths when: *slow_check_paths
check_diesel_migration_revertable:
image: willsquire/diesel-cli
environment:
CARGO_HOME: .cargo_home
DATABASE_URL: postgres://lemmy:password@database:5432/lemmy
commands:
- diesel migration run
- diesel migration redo
when: *slow_check_paths
check_db_perf_tool: check_db_perf_tool:
image: *rust_image image: *rust_image
environment: environment:
@ -194,6 +186,44 @@ steps:
- cargo test --workspace --no-fail-fast - cargo test --workspace --no-fail-fast
when: *slow_check_paths when: *slow_check_paths
check_diesel_migration:
# TODO: use willsquire/diesel-cli image when shared libraries become optional in lemmy_server
image: *rust_image
environment:
LEMMY_DATABASE_URL: postgres://lemmy:password@database:5432/lemmy
RUST_BACKTRACE: "1"
CARGO_HOME: .cargo_home
DATABASE_URL: postgres://lemmy:password@database:5432/lemmy
PGUSER: lemmy
PGPASSWORD: password
PGHOST: database
PGDATABASE: lemmy
commands:
# Install diesel_cli
- <<: *install_diesel_cli
# Run all migrations
- diesel migration run
# Dump schema to before.sqldump (PostgreSQL apt repo is used to prevent pg_dump version mismatch error)
- apt update && apt install -y lsb-release
- sh -c 'echo "deb https://apt.postgresql.org/pub/repos/apt $(lsb_release -cs)-pgdg main" > /etc/apt/sources.list.d/pgdg.list'
- wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | apt-key add -
- apt update && apt install -y postgresql-client-16
- psql -c "DROP SCHEMA IF EXISTS r CASCADE;"
- pg_dump --no-owner --no-privileges --no-table-access-method --schema-only --no-sync -f before.sqldump
# Make sure that the newest migration is revertable without the `r` schema
- diesel migration redo
# Run schema setup twice, which fails on the 2nd time if `DROP SCHEMA IF EXISTS r CASCADE` drops the wrong things
- alias lemmy_schema_setup="target/lemmy_server --disable-scheduled-tasks --disable-http-server --disable-activity-sending"
- lemmy_schema_setup
- lemmy_schema_setup
# Make sure that the newest migration is revertable with the `r` schema
- diesel migration redo
# Check for changes in the schema, which would be caused by an incorrect migration
- psql -c "DROP SCHEMA IF EXISTS r CASCADE;"
- pg_dump --no-owner --no-privileges --no-table-access-method --schema-only --no-sync -f after.sqldump
- diff before.sqldump after.sqldump
when: *slow_check_paths
run_federation_tests: run_federation_tests:
image: node:20-bookworm-slim image: node:20-bookworm-slim
environment: environment:
@ -248,10 +278,11 @@ steps:
publish_to_crates_io: publish_to_crates_io:
image: *rust_image image: *rust_image
commands: commands:
- cargo install cargo-workspaces - <<: *install_binstall
# Install cargo-workspaces
- cargo binstall -y cargo-workspaces
- cp -r migrations crates/db_schema/ - cp -r migrations crates/db_schema/
- cargo login "$CARGO_API_TOKEN" - cargo workspaces publish --token "$CARGO_API_TOKEN" --from-git --allow-dirty --no-verify --allow-branch "${CI_COMMIT_TAG}" --yes custom "${CI_COMMIT_TAG}"
- cargo workspaces publish --from-git --allow-dirty --no-verify --allow-branch "${CI_COMMIT_TAG}" --yes custom "${CI_COMMIT_TAG}"
secrets: [cargo_api_token] secrets: [cargo_api_token]
when: when:
- event: tag - event: tag

2377
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,5 +1,5 @@
[workspace.package] [workspace.package]
version = "0.19.4-beta.3" version = "0.19.4"
edition = "2021" edition = "2021"
description = "A link aggregator for the fediverse" description = "A link aggregator for the fediverse"
license = "AGPL-3.0" license = "AGPL-3.0"
@ -67,8 +67,8 @@ members = [
[workspace.lints.clippy] [workspace.lints.clippy]
cast_lossless = "deny" cast_lossless = "deny"
complexity = "deny" complexity = { level = "deny", priority = -1 }
correctness = "deny" correctness = { level = "deny", priority = -1 }
dbg_macro = "deny" dbg_macro = "deny"
explicit_into_iter_loop = "deny" explicit_into_iter_loop = "deny"
explicit_iter_loop = "deny" explicit_iter_loop = "deny"
@ -79,74 +79,74 @@ inefficient_to_string = "deny"
items-after-statements = "deny" items-after-statements = "deny"
manual_string_new = "deny" manual_string_new = "deny"
needless_collect = "deny" needless_collect = "deny"
perf = "deny" perf = { level = "deny", priority = -1 }
redundant_closure_for_method_calls = "deny" redundant_closure_for_method_calls = "deny"
style = "deny" style = { level = "deny", priority = -1 }
suspicious = "deny" suspicious = { level = "deny", priority = -1 }
uninlined_format_args = "allow" uninlined_format_args = "allow"
unused_self = "deny" unused_self = "deny"
unwrap_used = "deny" unwrap_used = "deny"
[workspace.dependencies] [workspace.dependencies]
lemmy_api = { version = "=0.19.4-beta.3", path = "./crates/api" } lemmy_api = { version = "=0.19.4", path = "./crates/api" }
lemmy_api_crud = { version = "=0.19.4-beta.3", path = "./crates/api_crud" } lemmy_api_crud = { version = "=0.19.4", path = "./crates/api_crud" }
lemmy_apub = { version = "=0.19.4-beta.3", path = "./crates/apub" } lemmy_apub = { version = "=0.19.4", path = "./crates/apub" }
lemmy_utils = { version = "=0.19.4-beta.3", path = "./crates/utils", default-features = false } lemmy_utils = { version = "=0.19.4", path = "./crates/utils", default-features = false }
lemmy_db_schema = { version = "=0.19.4-beta.3", path = "./crates/db_schema" } lemmy_db_schema = { version = "=0.19.4", path = "./crates/db_schema" }
lemmy_api_common = { version = "=0.19.4-beta.3", path = "./crates/api_common" } lemmy_api_common = { version = "=0.19.4", path = "./crates/api_common" }
lemmy_routes = { version = "=0.19.4-beta.3", path = "./crates/routes" } lemmy_routes = { version = "=0.19.4", path = "./crates/routes" }
lemmy_db_views = { version = "=0.19.4-beta.3", path = "./crates/db_views" } lemmy_db_views = { version = "=0.19.4", path = "./crates/db_views" }
lemmy_db_views_actor = { version = "=0.19.4-beta.3", path = "./crates/db_views_actor" } lemmy_db_views_actor = { version = "=0.19.4", path = "./crates/db_views_actor" }
lemmy_db_views_moderator = { version = "=0.19.4-beta.3", path = "./crates/db_views_moderator" } lemmy_db_views_moderator = { version = "=0.19.4", path = "./crates/db_views_moderator" }
lemmy_federate = { version = "=0.19.4-beta.3", path = "./crates/federate" } lemmy_federate = { version = "=0.19.4", path = "./crates/federate" }
activitypub_federation = { version = "0.5.3", default-features = false, features = [ activitypub_federation = { version = "0.5.6", default-features = false, features = [
"actix-web", "actix-web",
] } ] }
diesel = "2.1.4" diesel = "2.1.6"
diesel_migrations = "2.1.0" diesel_migrations = "2.1.0"
diesel-async = "0.4.1" diesel-async = "0.4.1"
serde = { version = "1.0.197", features = ["derive"] } serde = { version = "1.0.203", features = ["derive"] }
serde_with = "3.7.0" serde_with = "3.8.1"
actix-web = { version = "4.5.1", default-features = false, features = [ actix-web = { version = "4.6.0", default-features = false, features = [
"macros", "macros",
"rustls", "rustls-0_23",
"compress-brotli", "compress-brotli",
"compress-gzip", "compress-gzip",
"compress-zstd", "compress-zstd",
"cookies", "cookies",
] } ] }
tracing = "0.1.40" tracing = "0.1.40"
tracing-actix-web = { version = "0.7.10", default-features = false } tracing-actix-web = { version = "0.7.11", default-features = false }
tracing-error = "0.2.0" tracing-error = "0.2.0"
tracing-log = "0.2.0" tracing-log = "0.2.0"
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] } tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
url = { version = "2.5.0", features = ["serde"] } url = { version = "2.5.0", features = ["serde"] }
reqwest = { version = "0.11.26", features = ["json", "blocking", "gzip"] } reqwest = { version = "0.11.27", features = ["json", "blocking", "gzip"] }
reqwest-middleware = "0.2.4" reqwest-middleware = "0.2.5"
reqwest-tracing = "0.4.7" reqwest-tracing = "0.4.8"
clokwerk = "0.4.0" clokwerk = "0.4.0"
doku = { version = "0.21.1", features = ["url-2"] } doku = { version = "0.21.1", features = ["url-2"] }
bcrypt = "0.15.0" bcrypt = "0.15.1"
chrono = { version = "0.4.35", features = ["serde"], default-features = false } chrono = { version = "0.4.38", features = ["serde"], default-features = false }
serde_json = { version = "1.0.114", features = ["preserve_order"] } serde_json = { version = "1.0.117", features = ["preserve_order"] }
base64 = "0.21.7" base64 = "0.22.1"
uuid = { version = "1.7.0", features = ["serde", "v4"] } uuid = { version = "1.8.0", features = ["serde", "v4"] }
async-trait = "0.1.77" async-trait = "0.1.80"
captcha = "0.0.9" captcha = "0.0.9"
anyhow = { version = "1.0.81", features = [ anyhow = { version = "1.0.86", features = [
"backtrace", "backtrace",
] } # backtrace is on by default on nightly, but not stable rust ] } # backtrace is on by default on nightly, but not stable rust
diesel_ltree = "0.3.1" diesel_ltree = "0.3.1"
typed-builder = "0.18.1" typed-builder = "0.18.2"
serial_test = "2.0.0" serial_test = "3.1.1"
tokio = { version = "1.36.0", features = ["full"] } tokio = { version = "1.38.0", features = ["full"] }
regex = "1.10.3" regex = "1.10.4"
once_cell = "1.19.0" once_cell = "1.19.0"
diesel-derive-newtype = "2.1.0" diesel-derive-newtype = "2.1.2"
diesel-derive-enum = { version = "2.1.0", features = ["postgres"] } diesel-derive-enum = { version = "2.1.0", features = ["postgres"] }
strum = "0.25.0" strum = "0.26.2"
strum_macros = "0.25.3" strum_macros = "0.26.4"
itertools = "0.12.1" itertools = "0.13.0"
futures = "0.3.30" futures = "0.3.30"
http = "0.2.12" http = "0.2.12"
rosetta-i18n = "0.1.3" rosetta-i18n = "0.1.3"
@ -157,16 +157,17 @@ ts-rs = { version = "7.1.1", features = [
"chrono-impl", "chrono-impl",
"no-serde-warnings", "no-serde-warnings",
] } ] }
rustls = { version = "0.21.10", features = ["dangerous_configuration"] } rustls = { version = "0.23.9", features = ["ring"] }
futures-util = "0.3.30" futures-util = "0.3.30"
tokio-postgres = "0.7.10" tokio-postgres = "0.7.10"
tokio-postgres-rustls = "0.10.0" tokio-postgres-rustls = "0.12.0"
urlencoding = "2.1.3" urlencoding = "2.1.3"
enum-map = "2.7" enum-map = "2.7"
moka = { version = "0.12.5", features = ["future"] } moka = { version = "0.12.7", features = ["future"] }
i-love-jesus = { version = "0.1.0" } i-love-jesus = { version = "0.1.0" }
clap = { version = "4.5.2", features = ["derive"] } clap = { version = "4.5.6", features = ["derive", "env"] }
pretty_assertions = "1.4.0" pretty_assertions = "1.4.0"
derive-new = "0.6.0"
[dependencies] [dependencies]
lemmy_api = { workspace = true } lemmy_api = { workspace = true }
@ -194,17 +195,17 @@ clokwerk = { workspace = true }
serde_json = { workspace = true } serde_json = { workspace = true }
tracing-opentelemetry = { workspace = true, optional = true } tracing-opentelemetry = { workspace = true, optional = true }
opentelemetry = { workspace = true, optional = true } opentelemetry = { workspace = true, optional = true }
console-subscriber = { version = "0.1.10", optional = true } console-subscriber = { version = "0.2.0", optional = true }
opentelemetry-otlp = { version = "0.12.0", optional = true } opentelemetry-otlp = { version = "0.12.0", optional = true }
pict-rs = { version = "0.5.9", optional = true } pict-rs = { version = "0.5.15", optional = true }
tokio.workspace = true tokio.workspace = true
actix-cors = "0.6.5" actix-cors = "0.7.0"
futures-util = { workspace = true } futures-util = { workspace = true }
chrono = { workspace = true } chrono = { workspace = true }
prometheus = { version = "0.13.3", features = ["process"] } prometheus = { version = "0.13.4", features = ["process"] }
serial_test = { workspace = true } serial_test = { workspace = true }
clap = { workspace = true } clap = { workspace = true }
actix-web-prom = "0.7.0" actix-web-prom = "0.8.0"
[dev-dependencies] [dev-dependencies]
pretty_assertions = { workspace = true } pretty_assertions = { workspace = true }

1
api_tests/.npmrc Normal file
View file

@ -0,0 +1 @@
package-manager-strict=false

View file

@ -6,10 +6,11 @@
"repository": "https://github.com/LemmyNet/lemmy", "repository": "https://github.com/LemmyNet/lemmy",
"author": "Dessalines", "author": "Dessalines",
"license": "AGPL-3.0", "license": "AGPL-3.0",
"packageManager": "pnpm@9.1.4",
"scripts": { "scripts": {
"lint": "tsc --noEmit && eslint --report-unused-disable-directives --ext .js,.ts,.tsx src && prettier --check 'src/**/*.ts'", "lint": "tsc --noEmit && eslint --report-unused-disable-directives --ext .js,.ts,.tsx src && prettier --check 'src/**/*.ts'",
"fix": "prettier --write src && eslint --fix src", "fix": "prettier --write src && eslint --fix src",
"api-test": "jest -i follow.spec.ts && jest -i post.spec.ts && jest -i comment.spec.ts && jest -i private_message.spec.ts && jest -i user.spec.ts && jest -i community.spec.ts && jest -i image.spec.ts", "api-test": "jest -i follow.spec.ts && jest -i image.spec.ts && jest -i user.spec.ts && jest -i private_message.spec.ts && jest -i community.spec.ts && jest -i post.spec.ts && jest -i comment.spec.ts ",
"api-test-follow": "jest -i follow.spec.ts", "api-test-follow": "jest -i follow.spec.ts",
"api-test-comment": "jest -i comment.spec.ts", "api-test-comment": "jest -i comment.spec.ts",
"api-test-post": "jest -i post.spec.ts", "api-test-post": "jest -i post.spec.ts",
@ -27,7 +28,7 @@
"eslint": "^8.57.0", "eslint": "^8.57.0",
"eslint-plugin-prettier": "^5.1.3", "eslint-plugin-prettier": "^5.1.3",
"jest": "^29.5.0", "jest": "^29.5.0",
"lemmy-js-client": "0.19.4-alpha.16", "lemmy-js-client": "0.19.4",
"prettier": "^3.2.5", "prettier": "^3.2.5",
"ts-jest": "^29.1.0", "ts-jest": "^29.1.0",
"typescript": "^5.4.4" "typescript": "^5.4.4"

File diff suppressed because it is too large Load diff

View file

@ -3,19 +3,19 @@
# it is expected that this script is called by run-federation-test.sh script. # it is expected that this script is called by run-federation-test.sh script.
set -e set -e
if [ -n "$LEMMY_LOG_LEVEL" ]; if [ -z "$LEMMY_LOG_LEVEL" ];
then then
LEMMY_LOG_LEVEL=warn LEMMY_LOG_LEVEL=info
fi fi
export RUST_BACKTRACE=1 export RUST_BACKTRACE=1
#export RUST_LOG="warn,lemmy_server=$LEMMY_LOG_LEVEL,lemmy_federate=$LEMMY_LOG_LEVEL,lemmy_api=$LEMMY_LOG_LEVEL,lemmy_api_common=$LEMMY_LOG_LEVEL,lemmy_api_crud=$LEMMY_LOG_LEVEL,lemmy_apub=$LEMMY_LOG_LEVEL,lemmy_db_schema=$LEMMY_LOG_LEVEL,lemmy_db_views=$LEMMY_LOG_LEVEL,lemmy_db_views_actor=$LEMMY_LOG_LEVEL,lemmy_db_views_moderator=$LEMMY_LOG_LEVEL,lemmy_routes=$LEMMY_LOG_LEVEL,lemmy_utils=$LEMMY_LOG_LEVEL,lemmy_websocket=$LEMMY_LOG_LEVEL" export RUST_LOG="warn,lemmy_server=$LEMMY_LOG_LEVEL,lemmy_federate=$LEMMY_LOG_LEVEL,lemmy_api=$LEMMY_LOG_LEVEL,lemmy_api_common=$LEMMY_LOG_LEVEL,lemmy_api_crud=$LEMMY_LOG_LEVEL,lemmy_apub=$LEMMY_LOG_LEVEL,lemmy_db_schema=$LEMMY_LOG_LEVEL,lemmy_db_views=$LEMMY_LOG_LEVEL,lemmy_db_views_actor=$LEMMY_LOG_LEVEL,lemmy_db_views_moderator=$LEMMY_LOG_LEVEL,lemmy_routes=$LEMMY_LOG_LEVEL,lemmy_utils=$LEMMY_LOG_LEVEL,lemmy_websocket=$LEMMY_LOG_LEVEL"
export LEMMY_TEST_FAST_FEDERATION=1 # by default, the persistent federation queue has delays in the scale of 30s-5min export LEMMY_TEST_FAST_FEDERATION=1 # by default, the persistent federation queue has delays in the scale of 30s-5min
# pictrs setup # pictrs setup
if [ ! -f "api_tests/pict-rs" ]; then if [ ! -f "api_tests/pict-rs" ]; then
curl "https://git.asonix.dog/asonix/pict-rs/releases/download/v0.5.0-beta.2/pict-rs-linux-amd64" -o api_tests/pict-rs curl "https://git.asonix.dog/asonix/pict-rs/releases/download/v0.5.13/pict-rs-linux-amd64" -o api_tests/pict-rs
chmod +x api_tests/pict-rs chmod +x api_tests/pict-rs
fi fi
./api_tests/pict-rs \ ./api_tests/pict-rs \

View file

@ -37,15 +37,15 @@ import {
followCommunity, followCommunity,
blockCommunity, blockCommunity,
delay, delay,
saveUserSettings,
} from "./shared"; } from "./shared";
import { CommentView, CommunityView } from "lemmy-js-client"; import { CommentView, CommunityView, SaveUserSettings } from "lemmy-js-client";
let betaCommunity: CommunityView | undefined; let betaCommunity: CommunityView | undefined;
let postOnAlphaRes: PostResponse; let postOnAlphaRes: PostResponse;
beforeAll(async () => { beforeAll(async () => {
await setupLogins(); await setupLogins();
await unfollows();
await Promise.all([followBeta(alpha), followBeta(gamma)]); await Promise.all([followBeta(alpha), followBeta(gamma)]);
betaCommunity = (await resolveBetaCommunity(alpha)).community; betaCommunity = (await resolveBetaCommunity(alpha)).community;
if (betaCommunity) { if (betaCommunity) {
@ -444,6 +444,59 @@ test("Reply to a comment from another instance, get notification", async () => {
assertCommentFederation(alphaReply, replyRes.comment_view); assertCommentFederation(alphaReply, replyRes.comment_view);
}); });
test("Bot reply notifications are filtered when bots are hidden", async () => {
const newAlphaBot = await registerUser(alpha, alphaUrl);
let form: SaveUserSettings = {
bot_account: true,
};
await saveUserSettings(newAlphaBot, form);
const alphaCommunity = (
await resolveCommunity(alpha, "!main@lemmy-alpha:8541")
).community;
if (!alphaCommunity) {
throw "Missing alpha community";
}
await alpha.markAllAsRead();
form = {
show_bot_accounts: false,
};
await saveUserSettings(alpha, form);
const postOnAlphaRes = await createPost(alpha, alphaCommunity.community.id);
// Bot reply to alpha's post
let commentRes = await createComment(
newAlphaBot,
postOnAlphaRes.post_view.post.id,
);
expect(commentRes).toBeDefined();
let alphaUnreadCountRes = await getUnreadCount(alpha);
expect(alphaUnreadCountRes.replies).toBe(0);
let alphaUnreadRepliesRes = await getReplies(alpha, true);
expect(alphaUnreadRepliesRes.replies.length).toBe(0);
// This both restores the original state that may be expected by other tests
// implicitly and is used by the next steps to ensure replies are still
// returned when a user later decides to show bot accounts again.
form = {
show_bot_accounts: true,
};
await saveUserSettings(alpha, form);
alphaUnreadCountRes = await getUnreadCount(alpha);
expect(alphaUnreadCountRes.replies).toBe(1);
alphaUnreadRepliesRes = await getReplies(alpha, true);
expect(alphaUnreadRepliesRes.replies.length).toBe(1);
expect(alphaUnreadRepliesRes.replies[0].comment.id).toBe(
commentRes.comment_view.comment.id,
);
});
test("Mention beta from alpha", async () => { test("Mention beta from alpha", async () => {
if (!betaCommunity) throw Error("no community"); if (!betaCommunity) throw Error("no community");
const postOnAlphaRes = await createPost(alpha, betaCommunity.community.id); const postOnAlphaRes = await createPost(alpha, betaCommunity.community.id);

View file

@ -242,7 +242,7 @@ test("Admin actions in remote community are not federated to origin", async () =
); );
expect(banRes.banned).toBe(true); expect(banRes.banned).toBe(true);
// ban doesnt federate to community's origin instance alpha // ban doesn't federate to community's origin instance alpha
let alphaPost = (await resolvePost(alpha, gammaPost.post)).post; let alphaPost = (await resolvePost(alpha, gammaPost.post)).post;
expect(alphaPost?.creator_banned_from_community).toBe(false); expect(alphaPost?.creator_banned_from_community).toBe(false);
@ -380,8 +380,8 @@ test("User blocks instance, communities are hidden", async () => {
test("Community follower count is federated", async () => { test("Community follower count is federated", async () => {
// Follow the beta community from alpha // Follow the beta community from alpha
let community = await createCommunity(beta); let community = await createCommunity(beta);
let community_id = community.community_view.community.actor_id; let communityActorId = community.community_view.community.actor_id;
let resolved = await resolveCommunity(alpha, community_id); let resolved = await resolveCommunity(alpha, communityActorId);
if (!resolved.community) { if (!resolved.community) {
throw "Missing beta community"; throw "Missing beta community";
} }
@ -389,7 +389,7 @@ test("Community follower count is federated", async () => {
await followCommunity(alpha, true, resolved.community.community.id); await followCommunity(alpha, true, resolved.community.community.id);
let followed = ( let followed = (
await waitUntil( await waitUntil(
() => resolveCommunity(alpha, community_id), () => resolveCommunity(alpha, communityActorId),
c => c.community?.subscribed === "Subscribed", c => c.community?.subscribed === "Subscribed",
) )
).community; ).community;
@ -398,7 +398,7 @@ test("Community follower count is federated", async () => {
expect(followed?.counts.subscribers).toBe(1); expect(followed?.counts.subscribers).toBe(1);
// Follow the community from gamma // Follow the community from gamma
resolved = await resolveCommunity(gamma, community_id); resolved = await resolveCommunity(gamma, communityActorId);
if (!resolved.community) { if (!resolved.community) {
throw "Missing beta community"; throw "Missing beta community";
} }
@ -406,7 +406,7 @@ test("Community follower count is federated", async () => {
await followCommunity(gamma, true, resolved.community.community.id); await followCommunity(gamma, true, resolved.community.community.id);
followed = ( followed = (
await waitUntil( await waitUntil(
() => resolveCommunity(gamma, community_id), () => resolveCommunity(gamma, communityActorId),
c => c.community?.subscribed === "Subscribed", c => c.community?.subscribed === "Subscribed",
) )
).community; ).community;
@ -415,7 +415,7 @@ test("Community follower count is federated", async () => {
expect(followed?.counts?.subscribers).toBe(2); expect(followed?.counts?.subscribers).toBe(2);
// Follow the community from delta // Follow the community from delta
resolved = await resolveCommunity(delta, community_id); resolved = await resolveCommunity(delta, communityActorId);
if (!resolved.community) { if (!resolved.community) {
throw "Missing beta community"; throw "Missing beta community";
} }
@ -423,7 +423,7 @@ test("Community follower count is federated", async () => {
await followCommunity(delta, true, resolved.community.community.id); await followCommunity(delta, true, resolved.community.community.id);
followed = ( followed = (
await waitUntil( await waitUntil(
() => resolveCommunity(delta, community_id), () => resolveCommunity(delta, communityActorId),
c => c.community?.subscribed === "Subscribed", c => c.community?.subscribed === "Subscribed",
) )
).community; ).community;
@ -452,7 +452,7 @@ test("Dont receive community activities after unsubscribe", async () => {
); );
expect(communityRes1.community_view.counts.subscribers).toBe(2); expect(communityRes1.community_view.counts.subscribers).toBe(2);
// temporarily block alpha, so that it doesnt know about unfollow // temporarily block alpha, so that it doesn't know about unfollow
let editSiteForm: EditSite = {}; let editSiteForm: EditSite = {};
editSiteForm.allowed_instances = ["lemmy-epsilon"]; editSiteForm.allowed_instances = ["lemmy-epsilon"];
await beta.editSite(editSiteForm); await beta.editSite(editSiteForm);
@ -513,7 +513,7 @@ test("Fetch community, includes posts", async () => {
expect(post_listing.posts[0].post.ap_id).toBe(postRes.post_view.post.ap_id); expect(post_listing.posts[0].post.ap_id).toBe(postRes.post_view.post.ap_id);
}); });
test("Content in local-only community doesnt federate", async () => { test("Content in local-only community doesn't federate", async () => {
// create a community and set it local-only // create a community and set it local-only
let communityRes = (await createCommunity(alpha)).community_view.community; let communityRes = (await createCommunity(alpha)).community_view.community;
let form: EditCommunity = { let form: EditCommunity = {

View file

@ -27,21 +27,25 @@ import {
setupLogins, setupLogins,
waitForPost, waitForPost,
unfollows, unfollows,
editPostThumbnail,
getPost, getPost,
waitUntil, waitUntil,
createPostWithThumbnail,
sampleImage,
sampleSite,
} from "./shared"; } from "./shared";
const downloadFileSync = require("download-file-sync"); const downloadFileSync = require("download-file-sync");
beforeAll(setupLogins); beforeAll(setupLogins);
afterAll(unfollows); afterAll(async () => {
await Promise.all([unfollows(), deleteAllImages(alpha)]);
});
test("Upload image and delete it", async () => { test("Upload image and delete it", async () => {
// Before running this test, you need to delete all previous images in the DB // Before running this test, you need to delete all previous images in the DB
await deleteAllImages(alpha); await deleteAllImages(alpha);
// Upload test image. We use a simple string buffer as pictrs doesnt require an actual image // Upload test image. We use a simple string buffer as pictrs doesn't require an actual image
// in testing mode. // in testing mode.
const upload_form: UploadImage = { const upload_form: UploadImage = {
image: Buffer.from("test"), image: Buffer.from("test"),
@ -71,9 +75,14 @@ test("Upload image and delete it", async () => {
// The deleteUrl is a combination of the endpoint, delete token, and alias // The deleteUrl is a combination of the endpoint, delete token, and alias
let firstImage = listMediaRes.images[0]; let firstImage = listMediaRes.images[0];
let deleteUrl = `${alphaUrl}/pictrs/image/delete/${firstImage.pictrs_delete_token}/${firstImage.pictrs_alias}`; let deleteUrl = `${alphaUrl}/pictrs/image/delete/${firstImage.local_image.pictrs_delete_token}/${firstImage.local_image.pictrs_alias}`;
expect(deleteUrl).toBe(upload.delete_url); expect(deleteUrl).toBe(upload.delete_url);
// Make sure the uploader is correct
expect(firstImage.person.actor_id).toBe(
`http://lemmy-alpha:8541/u/lemmy_alpha`,
);
// delete image // delete image
const delete_form: DeleteImage = { const delete_form: DeleteImage = {
token: upload.files![0].delete_token, token: upload.files![0].delete_token,
@ -153,7 +162,6 @@ test("Purge post, linked image removed", async () => {
expect(post.post_view.post.url).toBe(upload.url); expect(post.post_view.post.url).toBe(upload.url);
// purge post // purge post
const purgeForm: PurgePost = { const purgeForm: PurgePost = {
post_id: post.post_view.post.id, post_id: post.post_view.post.id,
}; };
@ -165,48 +173,94 @@ test("Purge post, linked image removed", async () => {
expect(content2).toBe(""); expect(content2).toBe("");
}); });
test("Images in remote post are proxied if setting enabled", async () => { test("Images in remote image post are proxied if setting enabled", async () => {
let user = await registerUser(beta, betaUrl);
let community = await createCommunity(gamma); let community = await createCommunity(gamma);
let postRes = await createPost(
const upload_form: UploadImage = {
image: Buffer.from("test"),
};
const upload = await user.uploadImage(upload_form);
let post = await createPost(
gamma, gamma,
community.community_view.community.id, community.community_view.community.id,
upload.url, sampleImage,
"![](http://example.com/image2.png)", `![](${sampleImage})`,
); );
expect(post.post_view.post).toBeDefined(); const post = postRes.post_view.post;
expect(post).toBeDefined();
// remote image gets proxied after upload // remote image gets proxied after upload
expect( expect(
post.post_view.post.url?.startsWith( post.thumbnail_url?.startsWith(
"http://lemmy-gamma:8561/api/v3/image_proxy?url", "http://lemmy-gamma:8561/api/v3/image_proxy?url",
), ),
).toBeTruthy(); ).toBeTruthy();
expect( expect(
post.post_view.post.body?.startsWith( post.body?.startsWith("![](http://lemmy-gamma:8561/api/v3/image_proxy?url"),
"![](http://lemmy-gamma:8561/api/v3/image_proxy?url",
),
).toBeTruthy(); ).toBeTruthy();
let epsilonPost = await resolvePost(epsilon, post.post_view.post); // Make sure that it ends with jpg, to be sure its an image
expect(epsilonPost.post).toBeDefined(); expect(post.thumbnail_url?.endsWith(".jpg")).toBeTruthy();
let epsilonPostRes = await resolvePost(epsilon, postRes.post_view.post);
expect(epsilonPostRes.post).toBeDefined();
// Fetch the post again, the metadata should be backgrounded now
// Wait for the metadata to get fetched, since this is backgrounded now
let epsilonPostRes2 = await waitUntil(
() => getPost(epsilon, epsilonPostRes.post!.post.id),
p => p.post_view.post.thumbnail_url != undefined,
);
const epsilonPost = epsilonPostRes2.post_view.post;
// remote image gets proxied after federation
expect( expect(
epsilonPost.post!.post.url?.startsWith( epsilonPost.thumbnail_url?.startsWith(
"http://lemmy-epsilon:8581/api/v3/image_proxy?url", "http://lemmy-epsilon:8581/api/v3/image_proxy?url",
), ),
).toBeTruthy(); ).toBeTruthy();
expect( expect(
epsilonPost.post!.post.body?.startsWith( epsilonPost.body?.startsWith(
"![](http://lemmy-epsilon:8581/api/v3/image_proxy?url", "![](http://lemmy-epsilon:8581/api/v3/image_proxy?url",
), ),
).toBeTruthy(); ).toBeTruthy();
// Make sure that it ends with jpg, to be sure its an image
expect(epsilonPost.thumbnail_url?.endsWith(".jpg")).toBeTruthy();
});
test("Thumbnail of remote image link is proxied if setting enabled", async () => {
let community = await createCommunity(gamma);
let postRes = await createPost(
gamma,
community.community_view.community.id,
// The sample site metadata thumbnail ends in png
sampleSite,
);
const post = postRes.post_view.post;
expect(post).toBeDefined();
// remote image gets proxied after upload
expect(
post.thumbnail_url?.startsWith(
"http://lemmy-gamma:8561/api/v3/image_proxy?url",
),
).toBeTruthy();
// Make sure that it ends with png, to be sure its an image
expect(post.thumbnail_url?.endsWith(".png")).toBeTruthy();
let epsilonPostRes = await resolvePost(epsilon, postRes.post_view.post);
expect(epsilonPostRes.post).toBeDefined();
let epsilonPostRes2 = await waitUntil(
() => getPost(epsilon, epsilonPostRes.post!.post.id),
p => p.post_view.post.thumbnail_url != undefined,
);
const epsilonPost = epsilonPostRes2.post_view.post;
expect(
epsilonPost.thumbnail_url?.startsWith(
"http://lemmy-epsilon:8581/api/v3/image_proxy?url",
),
).toBeTruthy();
// Make sure that it ends with png, to be sure its an image
expect(epsilonPost.thumbnail_url?.endsWith(".png")).toBeTruthy();
}); });
test("No image proxying if setting is disabled", async () => { test("No image proxying if setting is disabled", async () => {
@ -226,15 +280,15 @@ test("No image proxying if setting is disabled", async () => {
alpha, alpha,
community.community_view.community.id, community.community_view.community.id,
upload.url, upload.url,
"![](http://example.com/image2.png)", `![](${sampleImage})`,
); );
expect(post.post_view.post).toBeDefined(); expect(post.post_view.post).toBeDefined();
// remote image doesnt get proxied after upload // remote image doesn't get proxied after upload
expect( expect(
post.post_view.post.url?.startsWith("http://127.0.0.1:8551/pictrs/image/"), post.post_view.post.url?.startsWith("http://127.0.0.1:8551/pictrs/image/"),
).toBeTruthy(); ).toBeTruthy();
expect(post.post_view.post.body).toBe("![](http://example.com/image2.png)"); expect(post.post_view.post.body).toBe(`![](${sampleImage})`);
let betaPost = await waitForPost( let betaPost = await waitForPost(
beta, beta,
@ -243,12 +297,11 @@ test("No image proxying if setting is disabled", async () => {
); );
expect(betaPost.post).toBeDefined(); expect(betaPost.post).toBeDefined();
// remote image doesnt get proxied after federation // remote image doesn't get proxied after federation
expect( expect(
betaPost.post.url?.startsWith("http://127.0.0.1:8551/pictrs/image/"), betaPost.post.url?.startsWith("http://127.0.0.1:8551/pictrs/image/"),
).toBeTruthy(); ).toBeTruthy();
expect(betaPost.post.body).toBe("![](http://example.com/image2.png)"); expect(betaPost.post.body).toBe(`![](${sampleImage})`);
// Make sure the alt text got federated // Make sure the alt text got federated
expect(post.post_view.post.alt_text).toBe(betaPost.post.alt_text); expect(post.post_view.post.alt_text).toBe(betaPost.post.alt_text);
}); });
@ -264,10 +317,11 @@ test("Make regular post, and give it a custom thumbnail", async () => {
// Use wikipedia since it has an opengraph image // Use wikipedia since it has an opengraph image
const wikipediaUrl = "https://wikipedia.org/"; const wikipediaUrl = "https://wikipedia.org/";
let post = await createPost( let post = await createPostWithThumbnail(
alphaImage, alphaImage,
community.community_view.community.id, community.community_view.community.id,
wikipediaUrl, wikipediaUrl,
upload1.url!,
); );
// Wait for the metadata to get fetched, since this is backgrounded now // Wait for the metadata to get fetched, since this is backgrounded now
@ -276,21 +330,11 @@ test("Make regular post, and give it a custom thumbnail", async () => {
p => p.post_view.post.thumbnail_url != undefined, p => p.post_view.post.thumbnail_url != undefined,
); );
expect(post.post_view.post.url).toBe(wikipediaUrl); expect(post.post_view.post.url).toBe(wikipediaUrl);
expect(post.post_view.post.thumbnail_url).toBeDefined(); // Make sure it uses custom thumbnail
// Edit the thumbnail
await editPostThumbnail(alphaImage, post.post_view.post, upload1.url!);
post = await waitUntil(
() => getPost(alphaImage, post.post_view.post.id),
p => p.post_view.post.thumbnail_url == upload1.url,
);
// Make sure the thumbnail got edited.
expect(post.post_view.post.thumbnail_url).toBe(upload1.url); expect(post.post_view.post.thumbnail_url).toBe(upload1.url);
}); });
test("Create an image post, and make sure a custom thumbnail doesnt overwrite it", async () => { test("Create an image post, and make sure a custom thumbnail doesn't overwrite it", async () => {
const uploadForm1: UploadImage = { const uploadForm1: UploadImage = {
image: Buffer.from("test1"), image: Buffer.from("test1"),
}; };
@ -303,23 +347,17 @@ test("Create an image post, and make sure a custom thumbnail doesnt overwrite it
const community = await createCommunity(alphaImage); const community = await createCommunity(alphaImage);
let post = await createPost( let post = await createPostWithThumbnail(
alphaImage, alphaImage,
community.community_view.community.id, community.community_view.community.id,
upload1.url, upload1.url!,
upload2.url!,
); );
expect(post.post_view.post.url).toBe(upload1.url);
// Edit the post
await editPostThumbnail(alphaImage, post.post_view.post, upload2.url!);
// Wait for the metadata to get fetched
post = await waitUntil( post = await waitUntil(
() => getPost(alphaImage, post.post_view.post.id), () => getPost(alphaImage, post.post_view.post.id),
p => p.post_view.post.thumbnail_url == upload1.url, p => p.post_view.post.thumbnail_url != undefined,
); );
// Make sure the new custom thumbnail is ignored, and doesn't overwrite the image post
expect(post.post_view.post.url).toBe(upload1.url); expect(post.post_view.post.url).toBe(upload1.url);
expect(post.post_view.post.thumbnail_url).toBe(upload1.url); // Make sure the custom thumbnail is ignored
expect(post.post_view.post.thumbnail_url == upload2.url).toBe(false);
}); });

View file

@ -48,7 +48,6 @@ beforeAll(async () => {
await setupLogins(); await setupLogins();
betaCommunity = (await resolveBetaCommunity(alpha)).community; betaCommunity = (await resolveBetaCommunity(alpha)).community;
expect(betaCommunity).toBeDefined(); expect(betaCommunity).toBeDefined();
await unfollows();
}); });
afterAll(unfollows); afterAll(unfollows);
@ -83,10 +82,7 @@ async function assertPostFederation(postOne: PostView, postTwo: PostView) {
test("Create a post", async () => { test("Create a post", async () => {
// Setup some allowlists and blocklists // Setup some allowlists and blocklists
let editSiteForm: EditSite = { const editSiteForm: EditSite = {};
allowed_instances: ["lemmy-beta"],
};
await delta.editSite(editSiteForm);
editSiteForm.allowed_instances = []; editSiteForm.allowed_instances = [];
editSiteForm.blocked_instances = ["lemmy-alpha"]; editSiteForm.blocked_instances = ["lemmy-alpha"];
@ -661,40 +657,60 @@ test("A and G subscribe to B (center) A posts, it gets announced to G", async ()
}); });
test("Report a post", async () => { test("Report a post", async () => {
// Note, this is a different one from the setup // Create post from alpha
let betaCommunity = (await resolveBetaCommunity(beta)).community; let alphaCommunity = (await resolveBetaCommunity(alpha)).community!;
if (!betaCommunity) {
throw "Missing beta community";
}
await followBeta(alpha); await followBeta(alpha);
let postRes = await createPost(beta, betaCommunity.community.id); let postRes = await createPost(alpha, alphaCommunity.community.id);
expect(postRes.post_view.post).toBeDefined(); expect(postRes.post_view.post).toBeDefined();
let alphaPost = (await resolvePost(alpha, postRes.post_view.post)).post; let alphaPost = (await resolvePost(alpha, postRes.post_view.post)).post;
if (!alphaPost) { if (!alphaPost) {
throw "Missing alpha post"; throw "Missing alpha post";
} }
let alphaReport = (
await reportPost(alpha, alphaPost.post.id, randomString(10))
).post_report_view.post_report;
// Send report from gamma
let gammaPost = (await resolvePost(gamma, alphaPost.post)).post!;
let gammaReport = (
await reportPost(gamma, gammaPost.post.id, randomString(10))
).post_report_view.post_report;
expect(gammaReport).toBeDefined();
// Report was federated to community instance
let betaReport = (await waitUntil( let betaReport = (await waitUntil(
() => () =>
listPostReports(beta).then(p => listPostReports(beta).then(p =>
p.post_reports.find( p.post_reports.find(
r => r =>
r.post_report.original_post_name === alphaReport.original_post_name, r.post_report.original_post_name === gammaReport.original_post_name,
), ),
), ),
res => !!res, res => !!res,
))!.post_report; ))!.post_report;
expect(betaReport).toBeDefined(); expect(betaReport).toBeDefined();
expect(betaReport.resolved).toBe(false); expect(betaReport.resolved).toBe(false);
expect(betaReport.original_post_name).toBe(alphaReport.original_post_name); expect(betaReport.original_post_name).toBe(gammaReport.original_post_name);
expect(betaReport.original_post_url).toBe(alphaReport.original_post_url); //expect(betaReport.original_post_url).toBe(gammaReport.original_post_url);
expect(betaReport.original_post_body).toBe(alphaReport.original_post_body); expect(betaReport.original_post_body).toBe(gammaReport.original_post_body);
expect(betaReport.reason).toBe(alphaReport.reason); expect(betaReport.reason).toBe(gammaReport.reason);
await unfollowRemotes(alpha); await unfollowRemotes(alpha);
// Report was federated to poster's instance
let alphaReport = (await waitUntil(
() =>
listPostReports(alpha).then(p =>
p.post_reports.find(
r =>
r.post_report.original_post_name === gammaReport.original_post_name,
),
),
res => !!res,
))!.post_report;
expect(alphaReport).toBeDefined();
expect(alphaReport.resolved).toBe(false);
expect(alphaReport.original_post_name).toBe(gammaReport.original_post_name);
//expect(alphaReport.original_post_url).toBe(gammaReport.original_post_url);
expect(alphaReport.original_post_body).toBe(gammaReport.original_post_body);
expect(alphaReport.reason).toBe(gammaReport.reason);
}); });
test("Fetch post via redirect", async () => { test("Fetch post via redirect", async () => {
@ -729,7 +745,7 @@ test("Block post that contains banned URL", async () => {
await epsilon.editSite(editSiteForm); await epsilon.editSite(editSiteForm);
await delay(500); await delay();
if (!betaCommunity) { if (!betaCommunity) {
throw "Missing beta community"; throw "Missing beta community";

View file

@ -81,21 +81,24 @@ import { ListingType } from "lemmy-js-client/dist/types/ListingType";
export const fetchFunction = fetch; export const fetchFunction = fetch;
export const imageFetchLimit = 50; export const imageFetchLimit = 50;
export const sampleImage =
"https://i.pinimg.com/originals/df/5f/5b/df5f5b1b174a2b4b6026cc6c8f9395c1.jpg";
export const sampleSite = "https://yahoo.com";
export let alphaUrl = "http://127.0.0.1:8541"; export const alphaUrl = "http://127.0.0.1:8541";
export let betaUrl = "http://127.0.0.1:8551"; export const betaUrl = "http://127.0.0.1:8551";
export let gammaUrl = "http://127.0.0.1:8561"; export const gammaUrl = "http://127.0.0.1:8561";
export let deltaUrl = "http://127.0.0.1:8571"; export const deltaUrl = "http://127.0.0.1:8571";
export let epsilonUrl = "http://127.0.0.1:8581"; export const epsilonUrl = "http://127.0.0.1:8581";
export let alpha = new LemmyHttp(alphaUrl, { fetchFunction }); export const alpha = new LemmyHttp(alphaUrl, { fetchFunction });
export let alphaImage = new LemmyHttp(alphaUrl); export const alphaImage = new LemmyHttp(alphaUrl);
export let beta = new LemmyHttp(betaUrl, { fetchFunction }); export const beta = new LemmyHttp(betaUrl, { fetchFunction });
export let gamma = new LemmyHttp(gammaUrl, { fetchFunction }); export const gamma = new LemmyHttp(gammaUrl, { fetchFunction });
export let delta = new LemmyHttp(deltaUrl, { fetchFunction }); export const delta = new LemmyHttp(deltaUrl, { fetchFunction });
export let epsilon = new LemmyHttp(epsilonUrl, { fetchFunction }); export const epsilon = new LemmyHttp(epsilonUrl, { fetchFunction });
export let betaAllowedInstances = [ export const betaAllowedInstances = [
"lemmy-alpha", "lemmy-alpha",
"lemmy-gamma", "lemmy-gamma",
"lemmy-delta", "lemmy-delta",
@ -180,6 +183,10 @@ export async function setupLogins() {
]; ];
await gamma.editSite(editSiteForm); await gamma.editSite(editSiteForm);
// Setup delta allowed instance
editSiteForm.allowed_instances = ["lemmy-beta"];
await delta.editSite(editSiteForm);
// Create the main alpha/beta communities // Create the main alpha/beta communities
// Ignore thrown errors of duplicates // Ignore thrown errors of duplicates
try { try {
@ -203,6 +210,7 @@ export async function createPost(
// use example.com for consistent title and embed description // use example.com for consistent title and embed description
name: string = randomString(5), name: string = randomString(5),
alt_text = randomString(10), alt_text = randomString(10),
custom_thumbnail: string | undefined = undefined,
): Promise<PostResponse> { ): Promise<PostResponse> {
let form: CreatePost = { let form: CreatePost = {
name, name,
@ -210,6 +218,7 @@ export async function createPost(
body, body,
alt_text, alt_text,
community_id, community_id,
custom_thumbnail,
}; };
return api.createPost(form); return api.createPost(form);
} }
@ -226,16 +235,19 @@ export async function editPost(
return api.editPost(form); return api.editPost(form);
} }
export async function editPostThumbnail( export async function createPostWithThumbnail(
api: LemmyHttp, api: LemmyHttp,
post: Post, community_id: number,
customThumbnail: string, url: string,
custom_thumbnail: string,
): Promise<PostResponse> { ): Promise<PostResponse> {
let form: EditPost = { let form: CreatePost = {
post_id: post.id, name: randomString(10),
custom_thumbnail: customThumbnail, url,
community_id,
custom_thumbnail,
}; };
return api.editPost(form); return api.createPost(form);
} }
export async function deletePost( export async function deletePost(
@ -352,10 +364,13 @@ export async function getUnreadCount(
return api.getUnreadCount(); return api.getUnreadCount();
} }
export async function getReplies(api: LemmyHttp): Promise<GetRepliesResponse> { export async function getReplies(
api: LemmyHttp,
unread_only: boolean = false,
): Promise<GetRepliesResponse> {
let form: GetReplies = { let form: GetReplies = {
sort: "New", sort: "New",
unread_only: false, unread_only,
}; };
return api.getReplies(form); return api.getReplies(form);
} }
@ -688,8 +703,8 @@ export async function saveUserSettingsBio(
export async function saveUserSettingsFederated( export async function saveUserSettingsFederated(
api: LemmyHttp, api: LemmyHttp,
): Promise<SuccessResponse> { ): Promise<SuccessResponse> {
let avatar = "https://image.flaticon.com/icons/png/512/35/35896.png"; let avatar = sampleImage;
let banner = "https://image.flaticon.com/icons/png/512/36/35896.png"; let banner = sampleImage;
let bio = "a changed bio"; let bio = "a changed bio";
let form: SaveUserSettings = { let form: SaveUserSettings = {
show_nsfw: false, show_nsfw: false,
@ -755,6 +770,7 @@ export async function unfollowRemotes(
await Promise.all( await Promise.all(
remoteFollowed.map(cu => followCommunity(api, false, cu.community.id)), remoteFollowed.map(cu => followCommunity(api, false, cu.community.id)),
); );
let siteRes = await getSite(api); let siteRes = await getSite(api);
return siteRes; return siteRes;
} }
@ -884,14 +900,17 @@ export async function deleteAllImages(api: LemmyHttp) {
limit: imageFetchLimit, limit: imageFetchLimit,
}); });
imagesRes.images; imagesRes.images;
Promise.all(
for (const image of imagesRes.images) { imagesRes.images
const form: DeleteImage = { .map(image => {
token: image.pictrs_delete_token, const form: DeleteImage = {
filename: image.pictrs_alias, token: image.local_image.pictrs_delete_token,
}; filename: image.local_image.pictrs_alias,
await api.deleteImage(form); };
} return form;
})
.map(form => api.deleteImage(form)),
);
} }
export async function unfollows() { export async function unfollows() {
@ -902,6 +921,24 @@ export async function unfollows() {
unfollowRemotes(delta), unfollowRemotes(delta),
unfollowRemotes(epsilon), unfollowRemotes(epsilon),
]); ]);
await Promise.all([
purgeAllPosts(alpha),
purgeAllPosts(beta),
purgeAllPosts(gamma),
purgeAllPosts(delta),
purgeAllPosts(epsilon),
]);
}
export async function purgeAllPosts(api: LemmyHttp) {
// The best way to get all federated items, is to find the posts
let res = await api.getPosts({ type_: "All", limit: 50 });
await Promise.all(
Array.from(new Set(res.posts.map(p => p.post.id)))
.map(post_id => api.purgePost({ post_id }))
// Ignore errors
.map(p => p.catch(e => e)),
);
} }
export function getCommentParentId(comment: Comment): number | undefined { export function getCommentParentId(comment: Comment): number | undefined {

View file

@ -21,6 +21,7 @@ import {
fetchFunction, fetchFunction,
alphaImage, alphaImage,
unfollows, unfollows,
saveUserSettingsBio,
} from "./shared"; } from "./shared";
import { LemmyHttp, SaveUserSettings, UploadImage } from "lemmy-js-client"; import { LemmyHttp, SaveUserSettings, UploadImage } from "lemmy-js-client";
import { GetPosts } from "lemmy-js-client/dist/types/GetPosts"; import { GetPosts } from "lemmy-js-client/dist/types/GetPosts";
@ -186,10 +187,26 @@ test("Set a new avatar, old avatar is deleted", async () => {
expect(upload2.url).toBeDefined(); expect(upload2.url).toBeDefined();
let form2 = { let form2 = {
avatar: upload1.url, avatar: upload2.url,
}; };
await saveUserSettings(alpha, form2); await saveUserSettings(alpha, form2);
// make sure only the new avatar is kept // make sure only the new avatar is kept
const listMediaRes2 = await alphaImage.listMedia(); const listMediaRes2 = await alphaImage.listMedia();
expect(listMediaRes2.images.length).toBe(1); expect(listMediaRes2.images.length).toBe(1);
// Upload that same form2 avatar, make sure it isn't replaced / deleted
await saveUserSettings(alpha, form2);
// make sure only the new avatar is kept
const listMediaRes3 = await alphaImage.listMedia();
expect(listMediaRes3.images.length).toBe(1);
// Now try to save a user settings, with the icon missing,
// and make sure it doesn't clear the data, or delete the image
await saveUserSettingsBio(alpha);
let site = await getSite(alpha);
expect(site.my_user?.local_user_view.person.avatar).toBe(upload2.url);
// make sure only the new avatar is kept
const listMediaRes4 = await alphaImage.listMedia();
expect(listMediaRes4.images.length).toBe(1);
}); });

View file

@ -47,9 +47,10 @@
# #
# To be removed in 0.20 # To be removed in 0.20
cache_external_link_previews: true cache_external_link_previews: true
# Specifies how to handle remote images, so that users don't have to connect directly to remote servers. # Specifies how to handle remote images, so that users don't have to connect directly to remote
# servers.
image_mode: image_mode:
# Leave images unchanged, don't generate any local thumbnails for post urls. Instead the the # Leave images unchanged, don't generate any local thumbnails for post urls. Instead the
# Opengraph image is directly returned as thumbnail # Opengraph image is directly returned as thumbnail
"None" "None"
@ -64,10 +65,11 @@
# or # or
# If enabled, all images from remote domains are rewritten to pass through `/api/v3/image_proxy`, # If enabled, all images from remote domains are rewritten to pass through
# including embedded images in markdown. Images are stored temporarily in pict-rs for caching. # `/api/v3/image_proxy`, including embedded images in markdown. Images are stored temporarily
# This improves privacy as users don't expose their IP to untrusted servers, and decreases load # in pict-rs for caching. This improves privacy as users don't expose their IP to untrusted
# on other servers. However it increases bandwidth use for the local server. # servers, and decreases load on other servers. However it increases bandwidth use for the
# local server.
# #
# Requires pict-rs 0.5 # Requires pict-rs 0.5
"ProxyAllImages" "ProxyAllImages"

View file

@ -33,7 +33,7 @@ anyhow = { workspace = true }
tracing = { workspace = true } tracing = { workspace = true }
chrono = { workspace = true } chrono = { workspace = true }
url = { workspace = true } url = { workspace = true }
wav = "1.0.0" hound = "3.5.1"
sitemap-rs = "0.2.1" sitemap-rs = "0.2.1"
totp-rs = { version = "5.5.1", features = ["gen_secret", "otpauth"] } totp-rs = { version = "5.5.1", features = ["gen_secret", "otpauth"] }
actix-web-httpauth = "0.8.1" actix-web-httpauth = "0.8.1"

View file

@ -17,7 +17,9 @@ pub async fn distinguish_comment(
context: Data<LemmyContext>, context: Data<LemmyContext>,
local_user_view: LocalUserView, local_user_view: LocalUserView,
) -> LemmyResult<Json<CommentResponse>> { ) -> LemmyResult<Json<CommentResponse>> {
let orig_comment = CommentView::read(&mut context.pool(), data.comment_id, None).await?; let orig_comment = CommentView::read(&mut context.pool(), data.comment_id, None)
.await?
.ok_or(LemmyErrorType::CouldntFindComment)?;
check_community_user_action( check_community_user_action(
&local_user_view.person, &local_user_view.person,
@ -54,7 +56,8 @@ pub async fn distinguish_comment(
data.comment_id, data.comment_id,
Some(local_user_view.person.id), Some(local_user_view.person.id),
) )
.await?; .await?
.ok_or(LemmyErrorType::CouldntFindComment)?;
Ok(Json(CommentResponse { Ok(Json(CommentResponse {
comment_view, comment_view,

View file

@ -35,7 +35,9 @@ pub async fn like_comment(
check_bot_account(&local_user_view.person)?; check_bot_account(&local_user_view.person)?;
let comment_id = data.comment_id; let comment_id = data.comment_id;
let orig_comment = CommentView::read(&mut context.pool(), comment_id, None).await?; let orig_comment = CommentView::read(&mut context.pool(), comment_id, None)
.await?
.ok_or(LemmyErrorType::CouldntFindComment)?;
check_community_user_action( check_community_user_action(
&local_user_view.person, &local_user_view.person,
@ -46,9 +48,10 @@ pub async fn like_comment(
// Add parent poster or commenter to recipients // Add parent poster or commenter to recipients
let comment_reply = CommentReply::read_by_comment(&mut context.pool(), comment_id).await; let comment_reply = CommentReply::read_by_comment(&mut context.pool(), comment_id).await;
if let Ok(reply) = comment_reply { if let Ok(Some(reply)) = comment_reply {
let recipient_id = reply.recipient_id; let recipient_id = reply.recipient_id;
if let Ok(local_recipient) = LocalUserView::read_person(&mut context.pool(), recipient_id).await if let Ok(Some(local_recipient)) =
LocalUserView::read_person(&mut context.pool(), recipient_id).await
{ {
recipient_ids.push(local_recipient.local_user.id); recipient_ids.push(local_recipient.local_user.id);
} }

View file

@ -5,7 +5,7 @@ use lemmy_api_common::{
utils::is_mod_or_admin, utils::is_mod_or_admin,
}; };
use lemmy_db_views::structs::{CommentView, LocalUserView, VoteView}; use lemmy_db_views::structs::{CommentView, LocalUserView, VoteView};
use lemmy_utils::error::LemmyResult; use lemmy_utils::{error::LemmyResult, LemmyErrorType};
/// Lists likes for a comment /// Lists likes for a comment
#[tracing::instrument(skip(context))] #[tracing::instrument(skip(context))]
@ -19,7 +19,9 @@ pub async fn list_comment_likes(
data.comment_id, data.comment_id,
Some(local_user_view.person.id), Some(local_user_view.person.id),
) )
.await?; .await?
.ok_or(LemmyErrorType::CouldntFindComment)?;
is_mod_or_admin( is_mod_or_admin(
&mut context.pool(), &mut context.pool(),
&local_user_view.person, &local_user_view.person,

View file

@ -33,7 +33,9 @@ pub async fn save_comment(
let comment_id = data.comment_id; let comment_id = data.comment_id;
let person_id = local_user_view.person.id; let person_id = local_user_view.person.id;
let comment_view = CommentView::read(&mut context.pool(), comment_id, Some(person_id)).await?; let comment_view = CommentView::read(&mut context.pool(), comment_id, Some(person_id))
.await?
.ok_or(LemmyErrorType::CouldntFindComment)?;
Ok(Json(CommentResponse { Ok(Json(CommentResponse {
comment_view, comment_view,

View file

@ -35,7 +35,9 @@ pub async fn create_comment_report(
let person_id = local_user_view.person.id; let person_id = local_user_view.person.id;
let comment_id = data.comment_id; let comment_id = data.comment_id;
let comment_view = CommentView::read(&mut context.pool(), comment_id, None).await?; let comment_view = CommentView::read(&mut context.pool(), comment_id, None)
.await?
.ok_or(LemmyErrorType::CouldntFindComment)?;
check_community_user_action( check_community_user_action(
&local_user_view.person, &local_user_view.person,
@ -58,8 +60,9 @@ pub async fn create_comment_report(
.await .await
.with_lemmy_type(LemmyErrorType::CouldntCreateReport)?; .with_lemmy_type(LemmyErrorType::CouldntCreateReport)?;
let comment_report_view = let comment_report_view = CommentReportView::read(&mut context.pool(), report.id, person_id)
CommentReportView::read(&mut context.pool(), report.id, person_id).await?; .await?
.ok_or(LemmyErrorType::CouldntFindCommentReport)?;
// Email the admins // Email the admins
if local_site.reports_email_admins { if local_site.reports_email_admins {

View file

@ -17,7 +17,9 @@ pub async fn resolve_comment_report(
) -> LemmyResult<Json<CommentReportResponse>> { ) -> LemmyResult<Json<CommentReportResponse>> {
let report_id = data.report_id; let report_id = data.report_id;
let person_id = local_user_view.person.id; let person_id = local_user_view.person.id;
let report = CommentReportView::read(&mut context.pool(), report_id, person_id).await?; let report = CommentReportView::read(&mut context.pool(), report_id, person_id)
.await?
.ok_or(LemmyErrorType::CouldntFindCommentReport)?;
let person_id = local_user_view.person.id; let person_id = local_user_view.person.id;
check_community_mod_action( check_community_mod_action(
@ -39,8 +41,9 @@ pub async fn resolve_comment_report(
} }
let report_id = data.report_id; let report_id = data.report_id;
let comment_report_view = let comment_report_view = CommentReportView::read(&mut context.pool(), report_id, person_id)
CommentReportView::read(&mut context.pool(), report_id, person_id).await?; .await?
.ok_or(LemmyErrorType::CouldntFindCommentReport)?;
Ok(Json(CommentReportResponse { Ok(Json(CommentReportResponse {
comment_report_view, comment_report_view,

View file

@ -33,9 +33,23 @@ pub async fn add_mod_to_community(
&mut context.pool(), &mut context.pool(),
) )
.await?; .await?;
let community = Community::read(&mut context.pool(), community_id).await?; let community = Community::read(&mut context.pool(), community_id)
.await?
.ok_or(LemmyErrorType::CouldntFindCommunity)?;
// If user is admin and community is remote, explicitly check that he is a
// moderator. This is necessary because otherwise the action would be rejected
// by the community's home instance.
if local_user_view.local_user.admin && !community.local { if local_user_view.local_user.admin && !community.local {
Err(LemmyErrorType::NotAModerator)? let is_mod = CommunityModeratorView::is_community_moderator(
&mut context.pool(),
community.id,
local_user_view.person.id,
)
.await?;
if !is_mod {
Err(LemmyErrorType::NotAModerator)?
}
} }
// Update in local database // Update in local database

View file

@ -43,7 +43,10 @@ pub async fn ban_from_community(
&mut context.pool(), &mut context.pool(),
) )
.await?; .await?;
is_valid_body_field(&data.reason, false)?;
if let Some(reason) = &data.reason {
is_valid_body_field(reason, false)?;
}
let community_user_ban_form = CommunityPersonBanForm { let community_user_ban_form = CommunityPersonBanForm {
community_id: data.community_id, community_id: data.community_id,
@ -89,7 +92,9 @@ pub async fn ban_from_community(
ModBanFromCommunity::create(&mut context.pool(), &form).await?; ModBanFromCommunity::create(&mut context.pool(), &form).await?;
let person_view = PersonView::read(&mut context.pool(), data.person_id).await?; let person_view = PersonView::read(&mut context.pool(), data.person_id)
.await?
.ok_or(LemmyErrorType::CouldntFindPerson)?;
ActivityChannel::submit_activity( ActivityChannel::submit_activity(
SendActivityData::BanFromCommunity { SendActivityData::BanFromCommunity {

View file

@ -51,7 +51,9 @@ pub async fn block_community(
} }
let community_view = let community_view =
CommunityView::read(&mut context.pool(), community_id, Some(person_id), false).await?; CommunityView::read(&mut context.pool(), community_id, Some(person_id), false)
.await?
.ok_or(LemmyErrorType::CouldntFindCommunity)?;
ActivityChannel::submit_activity( ActivityChannel::submit_activity(
SendActivityData::FollowCommunity( SendActivityData::FollowCommunity(

View file

@ -23,7 +23,9 @@ pub async fn follow_community(
context: Data<LemmyContext>, context: Data<LemmyContext>,
local_user_view: LocalUserView, local_user_view: LocalUserView,
) -> LemmyResult<Json<CommunityResponse>> { ) -> LemmyResult<Json<CommunityResponse>> {
let community = Community::read(&mut context.pool(), data.community_id).await?; let community = Community::read(&mut context.pool(), data.community_id)
.await?
.ok_or(LemmyErrorType::CouldntFindCommunity)?;
let mut community_follower_form = CommunityFollowerForm { let mut community_follower_form = CommunityFollowerForm {
community_id: community.id, community_id: community.id,
person_id: local_user_view.person.id, person_id: local_user_view.person.id,
@ -62,7 +64,10 @@ pub async fn follow_community(
let community_id = data.community_id; let community_id = data.community_id;
let person_id = local_user_view.person.id; let person_id = local_user_view.person.id;
let community_view = let community_view =
CommunityView::read(&mut context.pool(), community_id, Some(person_id), false).await?; CommunityView::read(&mut context.pool(), community_id, Some(person_id), false)
.await?
.ok_or(LemmyErrorType::CouldntFindCommunity)?;
let discussion_languages = CommunityLanguage::read(&mut context.pool(), community_id).await?; let discussion_languages = CommunityLanguage::read(&mut context.pool(), community_id).await?;
Ok(Json(CommunityResponse { Ok(Json(CommunityResponse {

View file

@ -79,8 +79,8 @@ pub async fn transfer_community(
let person_id = local_user_view.person.id; let person_id = local_user_view.person.id;
let community_view = let community_view =
CommunityView::read(&mut context.pool(), community_id, Some(person_id), false) CommunityView::read(&mut context.pool(), community_id, Some(person_id), false)
.await .await?
.with_lemmy_type(LemmyErrorType::CouldntFindCommunity)?; .ok_or(LemmyErrorType::CouldntFindCommunity)?;
let community_id = data.community_id; let community_id = data.community_id;
let moderators = CommunityModeratorView::for_community(&mut context.pool(), community_id) let moderators = CommunityModeratorView::for_community(&mut context.pool(), community_id)

View file

@ -49,27 +49,33 @@ pub(crate) fn captcha_as_wav_base64(captcha: &Captcha) -> LemmyResult<String> {
// Decode each wav file, concatenate the samples // Decode each wav file, concatenate the samples
let mut concat_samples: Vec<i16> = Vec::new(); let mut concat_samples: Vec<i16> = Vec::new();
let mut any_header: Option<wav::Header> = None; let mut any_header: Option<hound::WavSpec> = None;
for letter in letters { for letter in letters {
let mut cursor = Cursor::new(letter.unwrap_or_default()); let mut cursor = Cursor::new(letter.unwrap_or_default());
let (header, samples) = wav::read(&mut cursor)?; let reader = hound::WavReader::new(&mut cursor)?;
any_header = Some(header); any_header = Some(reader.spec());
if let Some(samples16) = samples.as_sixteen() { let samples16 = reader
concat_samples.extend(samples16); .into_samples::<i16>()
} else { .collect::<Result<Vec<_>, _>>()
Err(LemmyErrorType::CouldntCreateAudioCaptcha)? .with_lemmy_type(LemmyErrorType::CouldntCreateAudioCaptcha)?;
} concat_samples.extend(samples16);
} }
// Encode the concatenated result as a wav file // Encode the concatenated result as a wav file
let mut output_buffer = Cursor::new(vec![]); let mut output_buffer = Cursor::new(vec![]);
if let Some(header) = any_header { if let Some(header) = any_header {
wav::write( let mut writer = hound::WavWriter::new(&mut output_buffer, header)
header, .with_lemmy_type(LemmyErrorType::CouldntCreateAudioCaptcha)?;
&wav::BitDepth::Sixteen(concat_samples), let mut writer16 = writer.get_i16_writer(concat_samples.len() as u32);
&mut output_buffer, for sample in concat_samples {
) writer16.write_sample(sample);
.with_lemmy_type(LemmyErrorType::CouldntCreateAudioCaptcha)?; }
writer16
.flush()
.with_lemmy_type(LemmyErrorType::CouldntCreateAudioCaptcha)?;
writer
.finalize()
.with_lemmy_type(LemmyErrorType::CouldntCreateAudioCaptcha)?;
Ok(base64.encode(output_buffer.into_inner())) Ok(base64.encode(output_buffer.into_inner()))
} else { } else {
@ -252,7 +258,9 @@ pub async fn local_user_view_from_jwt(
let local_user_id = Claims::validate(jwt, context) let local_user_id = Claims::validate(jwt, context)
.await .await
.with_lemmy_type(LemmyErrorType::NotLoggedIn)?; .with_lemmy_type(LemmyErrorType::NotLoggedIn)?;
let local_user_view = LocalUserView::read(&mut context.pool(), local_user_id).await?; let local_user_view = LocalUserView::read(&mut context.pool(), local_user_id)
.await?
.ok_or(LemmyErrorType::CouldntFindLocalUser)?;
check_user_valid(&local_user_view.person)?; check_user_valid(&local_user_view.person)?;
Ok(local_user_view) Ok(local_user_view)

View file

@ -26,10 +26,10 @@ pub async fn add_admin(
// Make sure that the person_id added is local // Make sure that the person_id added is local
let added_local_user = LocalUserView::read_person(&mut context.pool(), data.person_id) let added_local_user = LocalUserView::read_person(&mut context.pool(), data.person_id)
.await .await?
.with_lemmy_type(LemmyErrorType::ObjectNotLocal)?; .ok_or(LemmyErrorType::ObjectNotLocal)?;
let added_admin = LocalUser::update( LocalUser::update(
&mut context.pool(), &mut context.pool(),
added_local_user.local_user.id, added_local_user.local_user.id,
&LocalUserUpdateForm { &LocalUserUpdateForm {
@ -43,7 +43,7 @@ pub async fn add_admin(
// Mod tables // Mod tables
let form = ModAddForm { let form = ModAddForm {
mod_person_id: local_user_view.person.id, mod_person_id: local_user_view.person.id,
other_person_id: added_admin.person_id, other_person_id: added_local_user.person.id,
removed: Some(!data.added), removed: Some(!data.added),
}; };

View file

@ -31,7 +31,9 @@ pub async fn ban_from_site(
// Make sure user is an admin // Make sure user is an admin
is_admin(&local_user_view)?; is_admin(&local_user_view)?;
is_valid_body_field(&data.reason, false)?; if let Some(reason) = &data.reason {
is_valid_body_field(reason, false)?;
}
let expires = check_expire_time(data.expires)?; let expires = check_expire_time(data.expires)?;
@ -49,7 +51,7 @@ pub async fn ban_from_site(
// if its a local user, invalidate logins // if its a local user, invalidate logins
let local_user = LocalUserView::read_person(&mut context.pool(), person.id).await; let local_user = LocalUserView::read_person(&mut context.pool(), person.id).await;
if let Ok(local_user) = local_user { if let Ok(Some(local_user)) = local_user {
LoginToken::invalidate_all(&mut context.pool(), local_user.local_user.id).await?; LoginToken::invalidate_all(&mut context.pool(), local_user.local_user.id).await?;
} }
@ -70,7 +72,9 @@ pub async fn ban_from_site(
ModBan::create(&mut context.pool(), &form).await?; ModBan::create(&mut context.pool(), &form).await?;
let person_view = PersonView::read(&mut context.pool(), person.id).await?; let person_view = PersonView::read(&mut context.pool(), person.id)
.await?
.ok_or(LemmyErrorType::CouldntFindPerson)?;
ban_nonlocal_user_from_local_communities( ban_nonlocal_user_from_local_communities(
&local_user_view, &local_user_view,

View file

@ -30,8 +30,12 @@ pub async fn block_person(
target_id, target_id,
}; };
let target_user = LocalUserView::read_person(&mut context.pool(), target_id).await; let target_user = LocalUserView::read_person(&mut context.pool(), target_id)
if target_user.map(|t| t.local_user.admin) == Ok(true) { .await
.ok()
.flatten();
if target_user.is_some_and(|t| t.local_user.admin) {
Err(LemmyErrorType::CantBlockAdmin)? Err(LemmyErrorType::CantBlockAdmin)?
} }
@ -45,7 +49,9 @@ pub async fn block_person(
.with_lemmy_type(LemmyErrorType::PersonBlockAlreadyExists)?; .with_lemmy_type(LemmyErrorType::PersonBlockAlreadyExists)?;
} }
let person_view = PersonView::read(&mut context.pool(), target_id).await?; let person_view = PersonView::read(&mut context.pool(), target_id)
.await?
.ok_or(LemmyErrorType::CouldntFindPerson)?;
Ok(Json(BlockPersonResponse { Ok(Json(BlockPersonResponse {
person_view, person_view,
blocked: data.block, blocked: data.block,

View file

@ -19,9 +19,10 @@ pub async fn change_password_after_reset(
) -> LemmyResult<Json<SuccessResponse>> { ) -> LemmyResult<Json<SuccessResponse>> {
// Fetch the user_id from the token // Fetch the user_id from the token
let token = data.token.clone(); let token = data.token.clone();
let local_user_id = PasswordResetRequest::read_from_token(&mut context.pool(), &token) let local_user_id = PasswordResetRequest::read_and_delete(&mut context.pool(), &token)
.await .await?
.map(|p| p.local_user_id)?; .ok_or(LemmyErrorType::TokenNotFound)?
.local_user_id;
password_length_check(&data.password)?; password_length_check(&data.password)?;

View file

@ -1,11 +1,7 @@
use crate::{build_totp_2fa, generate_totp_2fa_secret}; use crate::{build_totp_2fa, generate_totp_2fa_secret};
use activitypub_federation::config::Data; use activitypub_federation::config::Data;
use actix_web::web::Json; use actix_web::web::Json;
use lemmy_api_common::{ use lemmy_api_common::{context::LemmyContext, person::GenerateTotpSecretResponse};
context::LemmyContext,
person::GenerateTotpSecretResponse,
sensitive::Sensitive,
};
use lemmy_db_schema::source::local_user::{LocalUser, LocalUserUpdateForm}; use lemmy_db_schema::source::local_user::{LocalUser, LocalUserUpdateForm};
use lemmy_db_views::structs::{LocalUserView, SiteView}; use lemmy_db_views::structs::{LocalUserView, SiteView};
use lemmy_utils::error::{LemmyErrorType, LemmyResult}; use lemmy_utils::error::{LemmyErrorType, LemmyResult};
@ -17,7 +13,9 @@ pub async fn generate_totp_secret(
local_user_view: LocalUserView, local_user_view: LocalUserView,
context: Data<LemmyContext>, context: Data<LemmyContext>,
) -> LemmyResult<Json<GenerateTotpSecretResponse>> { ) -> LemmyResult<Json<GenerateTotpSecretResponse>> {
let site_view = SiteView::read_local(&mut context.pool()).await?; let site_view = SiteView::read_local(&mut context.pool())
.await?
.ok_or(LemmyErrorType::LocalSiteNotSetup)?;
if local_user_view.local_user.totp_2fa_enabled { if local_user_view.local_user.totp_2fa_enabled {
return Err(LemmyErrorType::TotpAlreadyEnabled)?; return Err(LemmyErrorType::TotpAlreadyEnabled)?;
@ -39,6 +37,6 @@ pub async fn generate_totp_secret(
.await?; .await?;
Ok(Json(GenerateTotpSecretResponse { Ok(Json(GenerateTotpSecretResponse {
totp_secret_url: Sensitive::new(secret_url), totp_secret_url: secret_url.into(),
})) }))
} }

View file

@ -3,8 +3,7 @@ use lemmy_api_common::{
context::LemmyContext, context::LemmyContext,
person::{ListMedia, ListMediaResponse}, person::{ListMedia, ListMediaResponse},
}; };
use lemmy_db_schema::source::images::LocalImage; use lemmy_db_views::structs::{LocalImageView, LocalUserView};
use lemmy_db_views::structs::LocalUserView;
use lemmy_utils::error::LemmyResult; use lemmy_utils::error::LemmyResult;
#[tracing::instrument(skip(context))] #[tracing::instrument(skip(context))]
@ -15,7 +14,7 @@ pub async fn list_media(
) -> LemmyResult<Json<ListMediaResponse>> { ) -> LemmyResult<Json<ListMediaResponse>> {
let page = data.page; let page = data.page;
let limit = data.limit; let limit = data.limit;
let images = LocalImage::get_all_paged_by_local_user_id( let images = LocalImageView::get_all_paged_by_local_user_id(
&mut context.pool(), &mut context.pool(),
local_user_view.local_user.id, local_user_view.local_user.id,
page, page,

View file

@ -16,7 +16,7 @@ use lemmy_db_schema::{
RegistrationMode, RegistrationMode,
}; };
use lemmy_db_views::structs::{LocalUserView, SiteView}; use lemmy_db_views::structs::{LocalUserView, SiteView};
use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult}; use lemmy_utils::error::{LemmyErrorType, LemmyResult};
#[tracing::instrument(skip(context))] #[tracing::instrument(skip(context))]
pub async fn login( pub async fn login(
@ -24,14 +24,16 @@ pub async fn login(
req: HttpRequest, req: HttpRequest,
context: Data<LemmyContext>, context: Data<LemmyContext>,
) -> LemmyResult<Json<LoginResponse>> { ) -> LemmyResult<Json<LoginResponse>> {
let site_view = SiteView::read_local(&mut context.pool()).await?; let site_view = SiteView::read_local(&mut context.pool())
.await?
.ok_or(LemmyErrorType::LocalSiteNotSetup)?;
// Fetch that username / email // Fetch that username / email
let username_or_email = data.username_or_email.clone(); let username_or_email = data.username_or_email.clone();
let local_user_view = let local_user_view =
LocalUserView::find_by_email_or_name(&mut context.pool(), &username_or_email) LocalUserView::find_by_email_or_name(&mut context.pool(), &username_or_email)
.await .await?
.with_lemmy_type(LemmyErrorType::IncorrectLogin)?; .ok_or(LemmyErrorType::IncorrectLogin)?;
// Verify the password // Verify the password
let valid: bool = verify( let valid: bool = verify(
@ -79,7 +81,9 @@ async fn check_registration_application(
// Fetch the registration application. If no admin id is present its still pending. Otherwise it // Fetch the registration application. If no admin id is present its still pending. Otherwise it
// was processed (either accepted or denied). // was processed (either accepted or denied).
let local_user_id = local_user_view.local_user.id; let local_user_id = local_user_view.local_user.id;
let registration = RegistrationApplication::find_by_local_user_id(pool, local_user_id).await?; let registration = RegistrationApplication::find_by_local_user_id(pool, local_user_id)
.await?
.ok_or(LemmyErrorType::CouldntFindRegistrationApplication)?;
if registration.admin_id.is_some() { if registration.admin_id.is_some() {
Err(LemmyErrorType::RegistrationDenied(registration.deny_reason))? Err(LemmyErrorType::RegistrationDenied(registration.deny_reason))?
} else { } else {

View file

@ -18,7 +18,9 @@ pub async fn mark_person_mention_as_read(
local_user_view: LocalUserView, local_user_view: LocalUserView,
) -> LemmyResult<Json<PersonMentionResponse>> { ) -> LemmyResult<Json<PersonMentionResponse>> {
let person_mention_id = data.person_mention_id; let person_mention_id = data.person_mention_id;
let read_person_mention = PersonMention::read(&mut context.pool(), person_mention_id).await?; let read_person_mention = PersonMention::read(&mut context.pool(), person_mention_id)
.await?
.ok_or(LemmyErrorType::CouldntFindPersonMention)?;
if local_user_view.person.id != read_person_mention.recipient_id { if local_user_view.person.id != read_person_mention.recipient_id {
Err(LemmyErrorType::CouldntUpdateComment)? Err(LemmyErrorType::CouldntUpdateComment)?
@ -37,7 +39,9 @@ pub async fn mark_person_mention_as_read(
let person_mention_id = read_person_mention.id; let person_mention_id = read_person_mention.id;
let person_id = local_user_view.person.id; let person_id = local_user_view.person.id;
let person_mention_view = let person_mention_view =
PersonMentionView::read(&mut context.pool(), person_mention_id, Some(person_id)).await?; PersonMentionView::read(&mut context.pool(), person_mention_id, Some(person_id))
.await?
.ok_or(LemmyErrorType::CouldntFindPersonMention)?;
Ok(Json(PersonMentionResponse { Ok(Json(PersonMentionResponse {
person_mention_view, person_mention_view,

View file

@ -18,7 +18,9 @@ pub async fn mark_reply_as_read(
local_user_view: LocalUserView, local_user_view: LocalUserView,
) -> LemmyResult<Json<CommentReplyResponse>> { ) -> LemmyResult<Json<CommentReplyResponse>> {
let comment_reply_id = data.comment_reply_id; let comment_reply_id = data.comment_reply_id;
let read_comment_reply = CommentReply::read(&mut context.pool(), comment_reply_id).await?; let read_comment_reply = CommentReply::read(&mut context.pool(), comment_reply_id)
.await?
.ok_or(LemmyErrorType::CouldntFindCommentReply)?;
if local_user_view.person.id != read_comment_reply.recipient_id { if local_user_view.person.id != read_comment_reply.recipient_id {
Err(LemmyErrorType::CouldntUpdateComment)? Err(LemmyErrorType::CouldntUpdateComment)?
@ -38,7 +40,9 @@ pub async fn mark_reply_as_read(
let comment_reply_id = read_comment_reply.id; let comment_reply_id = read_comment_reply.id;
let person_id = local_user_view.person.id; let person_id = local_user_view.person.id;
let comment_reply_view = let comment_reply_view =
CommentReplyView::read(&mut context.pool(), comment_reply_id, Some(person_id)).await?; CommentReplyView::read(&mut context.pool(), comment_reply_id, Some(person_id))
.await?
.ok_or(LemmyErrorType::CouldntFindCommentReply)?;
Ok(Json(CommentReplyResponse { comment_reply_view })) Ok(Json(CommentReplyResponse { comment_reply_view }))
} }

View file

@ -11,9 +11,12 @@ pub async fn unread_count(
) -> LemmyResult<Json<GetUnreadCountResponse>> { ) -> LemmyResult<Json<GetUnreadCountResponse>> {
let person_id = local_user_view.person.id; let person_id = local_user_view.person.id;
let replies = CommentReplyView::get_unread_replies(&mut context.pool(), person_id).await?; let replies =
CommentReplyView::get_unread_replies(&mut context.pool(), &local_user_view.local_user).await?;
let mentions = PersonMentionView::get_unread_mentions(&mut context.pool(), person_id).await?; let mentions =
PersonMentionView::get_unread_mentions(&mut context.pool(), &local_user_view.local_user)
.await?;
let private_messages = let private_messages =
PrivateMessageView::get_unread_messages(&mut context.pool(), person_id).await?; PrivateMessageView::get_unread_messages(&mut context.pool(), person_id).await?;

View file

@ -6,9 +6,8 @@ use lemmy_api_common::{
utils::send_password_reset_email, utils::send_password_reset_email,
SuccessResponse, SuccessResponse,
}; };
use lemmy_db_schema::source::password_reset_request::PasswordResetRequest;
use lemmy_db_views::structs::{LocalUserView, SiteView}; use lemmy_db_views::structs::{LocalUserView, SiteView};
use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult}; use lemmy_utils::error::{LemmyErrorType, LemmyResult};
#[tracing::instrument(skip(context))] #[tracing::instrument(skip(context))]
pub async fn reset_password( pub async fn reset_password(
@ -18,19 +17,12 @@ pub async fn reset_password(
// Fetch that email // Fetch that email
let email = data.email.to_lowercase(); let email = data.email.to_lowercase();
let local_user_view = LocalUserView::find_by_email(&mut context.pool(), &email) let local_user_view = LocalUserView::find_by_email(&mut context.pool(), &email)
.await .await?
.with_lemmy_type(LemmyErrorType::IncorrectLogin)?; .ok_or(LemmyErrorType::IncorrectLogin)?;
// Check for too many attempts (to limit potential abuse) let site_view = SiteView::read_local(&mut context.pool())
let recent_resets_count = PasswordResetRequest::get_recent_password_resets_count( .await?
&mut context.pool(), .ok_or(LemmyErrorType::LocalSiteNotSetup)?;
local_user_view.local_user.id,
)
.await?;
if recent_resets_count >= 3 {
Err(LemmyErrorType::PasswordResetLimitReached)?
}
let site_view = SiteView::read_local(&mut context.pool()).await?;
check_email_verified(&local_user_view, &site_view)?; check_email_verified(&local_user_view, &site_view)?;
// Email the pure token to the user. // Email the pure token to the user.

View file

@ -21,13 +21,14 @@ use lemmy_db_schema::{
person::{Person, PersonUpdateForm}, person::{Person, PersonUpdateForm},
}, },
traits::Crud, traits::Crud,
utils::diesel_option_overwrite, utils::{diesel_string_update, diesel_url_update},
}; };
use lemmy_db_views::structs::{LocalUserView, SiteView}; use lemmy_db_views::structs::{LocalUserView, SiteView};
use lemmy_utils::{ use lemmy_utils::{
error::{LemmyErrorType, LemmyResult}, error::{LemmyErrorType, LemmyResult},
utils::validation::{is_valid_bio_field, is_valid_display_name, is_valid_matrix_id}, utils::validation::{is_valid_bio_field, is_valid_display_name, is_valid_matrix_id},
}; };
use std::ops::Deref;
#[tracing::instrument(skip(context))] #[tracing::instrument(skip(context))]
pub async fn save_user_settings( pub async fn save_user_settings(
@ -35,27 +36,35 @@ pub async fn save_user_settings(
context: Data<LemmyContext>, context: Data<LemmyContext>,
local_user_view: LocalUserView, local_user_view: LocalUserView,
) -> LemmyResult<Json<SuccessResponse>> { ) -> LemmyResult<Json<SuccessResponse>> {
let site_view = SiteView::read_local(&mut context.pool()).await?; let site_view = SiteView::read_local(&mut context.pool())
.await?
.ok_or(LemmyErrorType::LocalSiteNotSetup)?;
let slur_regex = local_site_to_slur_regex(&site_view.local_site); let slur_regex = local_site_to_slur_regex(&site_view.local_site);
let url_blocklist = get_url_blocklist(&context).await?; let url_blocklist = get_url_blocklist(&context).await?;
let bio = diesel_option_overwrite( let bio = diesel_string_update(
process_markdown_opt(&data.bio, &slur_regex, &url_blocklist, &context).await?, process_markdown_opt(&data.bio, &slur_regex, &url_blocklist, &context)
.await?
.as_deref(),
); );
replace_image(&data.avatar, &local_user_view.person.avatar, &context).await?;
replace_image(&data.banner, &local_user_view.person.banner, &context).await?;
let avatar = proxy_image_link_opt_api(&data.avatar, &context).await?; let avatar = diesel_url_update(data.avatar.as_deref())?;
let banner = proxy_image_link_opt_api(&data.banner, &context).await?; replace_image(&avatar, &local_user_view.person.avatar, &context).await?;
let display_name = diesel_option_overwrite(data.display_name.clone()); let avatar = proxy_image_link_opt_api(avatar, &context).await?;
let matrix_user_id = diesel_option_overwrite(data.matrix_user_id.clone());
let banner = diesel_url_update(data.banner.as_deref())?;
replace_image(&banner, &local_user_view.person.banner, &context).await?;
let banner = proxy_image_link_opt_api(banner, &context).await?;
let display_name = diesel_string_update(data.display_name.as_deref());
let matrix_user_id = diesel_string_update(data.matrix_user_id.as_deref());
let email_deref = data.email.as_deref().map(str::to_lowercase); let email_deref = data.email.as_deref().map(str::to_lowercase);
let email = diesel_option_overwrite(email_deref.clone()); let email = diesel_string_update(email_deref.as_deref());
if let Some(Some(email)) = &email { if let Some(Some(email)) = &email {
let previous_email = local_user_view.local_user.email.clone().unwrap_or_default(); let previous_email = local_user_view.local_user.email.clone().unwrap_or_default();
// if email was changed, check that it is not taken and send verification mail // if email was changed, check that it is not taken and send verification mail
if &previous_email != email { if previous_email.deref() != email {
if LocalUser::is_email_taken(&mut context.pool(), email).await? { if LocalUser::is_email_taken(&mut context.pool(), email).await? {
return Err(LemmyErrorType::EmailAlreadyExists)?; return Err(LemmyErrorType::EmailAlreadyExists)?;
} }
@ -69,7 +78,8 @@ pub async fn save_user_settings(
} }
} }
// When the site requires email, make sure email is not Some(None). IE, an overwrite to a None value // When the site requires email, make sure email is not Some(None). IE, an overwrite to a None
// value
if let Some(email) = &email { if let Some(email) = &email {
if email.is_none() && site_view.local_site.require_email_verification { if email.is_none() && site_view.local_site.require_email_verification {
Err(LemmyErrorType::EmailRequired)? Err(LemmyErrorType::EmailRequired)?
@ -139,11 +149,7 @@ pub async fn save_user_settings(
..Default::default() ..Default::default()
}; };
// Ignore errors, because 'no fields updated' will return an error. LocalUser::update(&mut context.pool(), local_user_id, &local_user_form).await?;
// https://github.com/LemmyNet/lemmy/issues/4076
LocalUser::update(&mut context.pool(), local_user_id, &local_user_form)
.await
.ok();
// Update the vote display modes // Update the vote display modes
let vote_display_modes_form = LocalUserVoteDisplayModeUpdateForm { let vote_display_modes_form = LocalUserVoteDisplayModeUpdateForm {

View file

@ -9,23 +9,23 @@ use lemmy_db_schema::{
source::{ source::{
email_verification::EmailVerification, email_verification::EmailVerification,
local_user::{LocalUser, LocalUserUpdateForm}, local_user::{LocalUser, LocalUserUpdateForm},
person::Person,
}, },
traits::Crud,
RegistrationMode, RegistrationMode,
}; };
use lemmy_db_views::structs::SiteView; use lemmy_db_views::structs::{LocalUserView, SiteView};
use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult}; use lemmy_utils::error::{LemmyErrorType, LemmyResult};
pub async fn verify_email( pub async fn verify_email(
data: Json<VerifyEmail>, data: Json<VerifyEmail>,
context: Data<LemmyContext>, context: Data<LemmyContext>,
) -> LemmyResult<Json<SuccessResponse>> { ) -> LemmyResult<Json<SuccessResponse>> {
let site_view = SiteView::read_local(&mut context.pool()).await?; let site_view = SiteView::read_local(&mut context.pool())
.await?
.ok_or(LemmyErrorType::LocalSiteNotSetup)?;
let token = data.token.clone(); let token = data.token.clone();
let verification = EmailVerification::read_for_token(&mut context.pool(), &token) let verification = EmailVerification::read_for_token(&mut context.pool(), &token)
.await .await?
.with_lemmy_type(LemmyErrorType::TokenNotFound)?; .ok_or(LemmyErrorType::TokenNotFound)?;
let form = LocalUserUpdateForm { let form = LocalUserUpdateForm {
// necessary in case this is a new signup // necessary in case this is a new signup
@ -36,7 +36,7 @@ pub async fn verify_email(
}; };
let local_user_id = verification.local_user_id; let local_user_id = verification.local_user_id;
let local_user = LocalUser::update(&mut context.pool(), local_user_id, &form).await?; LocalUser::update(&mut context.pool(), local_user_id, &form).await?;
EmailVerification::delete_old_tokens_for_local_user(&mut context.pool(), local_user_id).await?; EmailVerification::delete_old_tokens_for_local_user(&mut context.pool(), local_user_id).await?;
@ -44,9 +44,16 @@ pub async fn verify_email(
if site_view.local_site.registration_mode == RegistrationMode::RequireApplication if site_view.local_site.registration_mode == RegistrationMode::RequireApplication
&& site_view.local_site.application_email_admins && site_view.local_site.application_email_admins
{ {
let person = Person::read(&mut context.pool(), local_user.person_id).await?; let local_user = LocalUserView::read(&mut context.pool(), local_user_id)
send_new_applicant_email_to_admins(&person.name, &mut context.pool(), context.settings()) .await?
.await?; .ok_or(LemmyErrorType::CouldntFindPerson)?;
send_new_applicant_email_to_admins(
&local_user.person.name,
&mut context.pool(),
context.settings(),
)
.await?;
} }
Ok(Json(SuccessResponse::default())) Ok(Json(SuccessResponse::default()))

View file

@ -16,7 +16,7 @@ use lemmy_db_schema::{
PostFeatureType, PostFeatureType,
}; };
use lemmy_db_views::structs::LocalUserView; use lemmy_db_views::structs::LocalUserView;
use lemmy_utils::error::LemmyResult; use lemmy_utils::{error::LemmyResult, LemmyErrorType};
#[tracing::instrument(skip(context))] #[tracing::instrument(skip(context))]
pub async fn feature_post( pub async fn feature_post(
@ -25,7 +25,9 @@ pub async fn feature_post(
local_user_view: LocalUserView, local_user_view: LocalUserView,
) -> LemmyResult<Json<PostResponse>> { ) -> LemmyResult<Json<PostResponse>> {
let post_id = data.post_id; let post_id = data.post_id;
let orig_post = Post::read(&mut context.pool(), post_id).await?; let orig_post = Post::read(&mut context.pool(), post_id)
.await?
.ok_or(LemmyErrorType::CouldntFindPost)?;
check_community_mod_action( check_community_mod_action(
&local_user_view.person, &local_user_view.person,

View file

@ -4,14 +4,19 @@ use lemmy_api_common::{
post::{GetSiteMetadata, GetSiteMetadataResponse}, post::{GetSiteMetadata, GetSiteMetadataResponse},
request::fetch_link_metadata, request::fetch_link_metadata,
}; };
use lemmy_utils::error::LemmyResult; use lemmy_utils::{
error::{LemmyErrorExt, LemmyResult},
LemmyErrorType,
};
use url::Url;
#[tracing::instrument(skip(context))] #[tracing::instrument(skip(context))]
pub async fn get_link_metadata( pub async fn get_link_metadata(
data: Query<GetSiteMetadata>, data: Query<GetSiteMetadata>,
context: Data<LemmyContext>, context: Data<LemmyContext>,
) -> LemmyResult<Json<GetSiteMetadataResponse>> { ) -> LemmyResult<Json<GetSiteMetadataResponse>> {
let metadata = fetch_link_metadata(&data.url, false, &context).await?; let url = Url::parse(&data.url).with_lemmy_type(LemmyErrorType::InvalidUrl)?;
let metadata = fetch_link_metadata(&url, &context).await?;
Ok(Json(GetSiteMetadataResponse { metadata })) Ok(Json(GetSiteMetadataResponse { metadata }))
} }

View file

@ -38,7 +38,9 @@ pub async fn like_post(
// Check for a community ban // Check for a community ban
let post_id = data.post_id; let post_id = data.post_id;
let post = Post::read(&mut context.pool(), post_id).await?; let post = Post::read(&mut context.pool(), post_id)
.await?
.ok_or(LemmyErrorType::CouldntFindPost)?;
check_community_user_action( check_community_user_action(
&local_user_view.person, &local_user_view.person,
@ -66,14 +68,17 @@ pub async fn like_post(
.with_lemmy_type(LemmyErrorType::CouldntLikePost)?; .with_lemmy_type(LemmyErrorType::CouldntLikePost)?;
} }
// Mark the post as read
mark_post_as_read(person_id, post_id, &mut context.pool()).await?; mark_post_as_read(person_id, post_id, &mut context.pool()).await?;
let community = Community::read(&mut context.pool(), post.community_id)
.await?
.ok_or(LemmyErrorType::CouldntFindCommunity)?;
ActivityChannel::submit_activity( ActivityChannel::submit_activity(
SendActivityData::LikePostOrComment { SendActivityData::LikePostOrComment {
object_id: post.ap_id, object_id: post.ap_id,
actor: local_user_view.person.clone(), actor: local_user_view.person.clone(),
community: Community::read(&mut context.pool(), post.community_id).await?, community,
score: data.score, score: data.score,
}, },
&context, &context,

View file

@ -6,7 +6,7 @@ use lemmy_api_common::{
}; };
use lemmy_db_schema::{source::post::Post, traits::Crud}; use lemmy_db_schema::{source::post::Post, traits::Crud};
use lemmy_db_views::structs::{LocalUserView, VoteView}; use lemmy_db_views::structs::{LocalUserView, VoteView};
use lemmy_utils::error::LemmyResult; use lemmy_utils::{error::LemmyResult, LemmyErrorType};
/// Lists likes for a post /// Lists likes for a post
#[tracing::instrument(skip(context))] #[tracing::instrument(skip(context))]
@ -15,7 +15,9 @@ pub async fn list_post_likes(
context: Data<LemmyContext>, context: Data<LemmyContext>,
local_user_view: LocalUserView, local_user_view: LocalUserView,
) -> LemmyResult<Json<ListPostLikesResponse>> { ) -> LemmyResult<Json<ListPostLikesResponse>> {
let post = Post::read(&mut context.pool(), data.post_id).await?; let post = Post::read(&mut context.pool(), data.post_id)
.await?
.ok_or(LemmyErrorType::CouldntFindPost)?;
is_mod_or_admin( is_mod_or_admin(
&mut context.pool(), &mut context.pool(),
&local_user_view.person, &local_user_view.person,

View file

@ -15,7 +15,7 @@ use lemmy_db_schema::{
traits::Crud, traits::Crud,
}; };
use lemmy_db_views::structs::LocalUserView; use lemmy_db_views::structs::LocalUserView;
use lemmy_utils::error::LemmyResult; use lemmy_utils::{error::LemmyResult, LemmyErrorType};
#[tracing::instrument(skip(context))] #[tracing::instrument(skip(context))]
pub async fn lock_post( pub async fn lock_post(
@ -24,7 +24,9 @@ pub async fn lock_post(
local_user_view: LocalUserView, local_user_view: LocalUserView,
) -> LemmyResult<Json<PostResponse>> { ) -> LemmyResult<Json<PostResponse>> {
let post_id = data.post_id; let post_id = data.post_id;
let orig_post = Post::read(&mut context.pool(), post_id).await?; let orig_post = Post::read(&mut context.pool(), post_id)
.await?
.ok_or(LemmyErrorType::CouldntFindPost)?;
check_community_mod_action( check_community_mod_action(
&local_user_view.person, &local_user_view.person,

View file

@ -34,9 +34,10 @@ pub async fn save_post(
let post_id = data.post_id; let post_id = data.post_id;
let person_id = local_user_view.person.id; let person_id = local_user_view.person.id;
let post_view = PostView::read(&mut context.pool(), post_id, Some(person_id), false).await?; let post_view = PostView::read(&mut context.pool(), post_id, Some(person_id), false)
.await?
.ok_or(LemmyErrorType::CouldntFindPost)?;
// Mark the post as read
mark_post_as_read(person_id, post_id, &mut context.pool()).await?; mark_post_as_read(person_id, post_id, &mut context.pool()).await?;
Ok(Json(PostResponse { post_view })) Ok(Json(PostResponse { post_view }))

View file

@ -35,7 +35,9 @@ pub async fn create_post_report(
let person_id = local_user_view.person.id; let person_id = local_user_view.person.id;
let post_id = data.post_id; let post_id = data.post_id;
let post_view = PostView::read(&mut context.pool(), post_id, None, false).await?; let post_view = PostView::read(&mut context.pool(), post_id, None, false)
.await?
.ok_or(LemmyErrorType::CouldntFindPost)?;
check_community_user_action( check_community_user_action(
&local_user_view.person, &local_user_view.person,
@ -59,7 +61,9 @@ pub async fn create_post_report(
.await .await
.with_lemmy_type(LemmyErrorType::CouldntCreateReport)?; .with_lemmy_type(LemmyErrorType::CouldntCreateReport)?;
let post_report_view = PostReportView::read(&mut context.pool(), report.id, person_id).await?; let post_report_view = PostReportView::read(&mut context.pool(), report.id, person_id)
.await?
.ok_or(LemmyErrorType::CouldntFindPostReport)?;
// Email the admins // Email the admins
if local_site.reports_email_admins { if local_site.reports_email_admins {

View file

@ -17,7 +17,9 @@ pub async fn resolve_post_report(
) -> LemmyResult<Json<PostReportResponse>> { ) -> LemmyResult<Json<PostReportResponse>> {
let report_id = data.report_id; let report_id = data.report_id;
let person_id = local_user_view.person.id; let person_id = local_user_view.person.id;
let report = PostReportView::read(&mut context.pool(), report_id, person_id).await?; let report = PostReportView::read(&mut context.pool(), report_id, person_id)
.await?
.ok_or(LemmyErrorType::CouldntFindPostReport)?;
let person_id = local_user_view.person.id; let person_id = local_user_view.person.id;
check_community_mod_action( check_community_mod_action(
@ -38,7 +40,9 @@ pub async fn resolve_post_report(
.with_lemmy_type(LemmyErrorType::CouldntResolveReport)?; .with_lemmy_type(LemmyErrorType::CouldntResolveReport)?;
} }
let post_report_view = PostReportView::read(&mut context.pool(), report_id, person_id).await?; let post_report_view = PostReportView::read(&mut context.pool(), report_id, person_id)
.await?
.ok_or(LemmyErrorType::CouldntFindPostReport)?;
Ok(Json(PostReportResponse { post_report_view })) Ok(Json(PostReportResponse { post_report_view }))
} }

View file

@ -18,7 +18,9 @@ pub async fn mark_pm_as_read(
) -> LemmyResult<Json<PrivateMessageResponse>> { ) -> LemmyResult<Json<PrivateMessageResponse>> {
// Checking permissions // Checking permissions
let private_message_id = data.private_message_id; let private_message_id = data.private_message_id;
let orig_private_message = PrivateMessage::read(&mut context.pool(), private_message_id).await?; let orig_private_message = PrivateMessage::read(&mut context.pool(), private_message_id)
.await?
.ok_or(LemmyErrorType::CouldntFindPrivateMessage)?;
if local_user_view.person.id != orig_private_message.recipient_id { if local_user_view.person.id != orig_private_message.recipient_id {
Err(LemmyErrorType::CouldntUpdatePrivateMessage)? Err(LemmyErrorType::CouldntUpdatePrivateMessage)?
} }
@ -37,7 +39,9 @@ pub async fn mark_pm_as_read(
.await .await
.with_lemmy_type(LemmyErrorType::CouldntUpdatePrivateMessage)?; .with_lemmy_type(LemmyErrorType::CouldntUpdatePrivateMessage)?;
let view = PrivateMessageView::read(&mut context.pool(), private_message_id).await?; let view = PrivateMessageView::read(&mut context.pool(), private_message_id)
.await?
.ok_or(LemmyErrorType::CouldntFindPrivateMessage)?;
Ok(Json(PrivateMessageResponse { Ok(Json(PrivateMessageResponse {
private_message_view: view, private_message_view: view,
})) }))

View file

@ -29,7 +29,9 @@ pub async fn create_pm_report(
let person_id = local_user_view.person.id; let person_id = local_user_view.person.id;
let private_message_id = data.private_message_id; let private_message_id = data.private_message_id;
let private_message = PrivateMessage::read(&mut context.pool(), private_message_id).await?; let private_message = PrivateMessage::read(&mut context.pool(), private_message_id)
.await?
.ok_or(LemmyErrorType::CouldntFindPrivateMessage)?;
// Make sure that only the recipient of the private message can create a report // Make sure that only the recipient of the private message can create a report
if person_id != private_message.recipient_id { if person_id != private_message.recipient_id {
@ -47,8 +49,9 @@ pub async fn create_pm_report(
.await .await
.with_lemmy_type(LemmyErrorType::CouldntCreateReport)?; .with_lemmy_type(LemmyErrorType::CouldntCreateReport)?;
let private_message_report_view = let private_message_report_view = PrivateMessageReportView::read(&mut context.pool(), report.id)
PrivateMessageReportView::read(&mut context.pool(), report.id).await?; .await?
.ok_or(LemmyErrorType::CouldntFindPrivateMessageReport)?;
// Email the admins // Email the admins
if local_site.reports_email_admins { if local_site.reports_email_admins {

View file

@ -28,8 +28,9 @@ pub async fn resolve_pm_report(
.with_lemmy_type(LemmyErrorType::CouldntResolveReport)?; .with_lemmy_type(LemmyErrorType::CouldntResolveReport)?;
} }
let private_message_report_view = let private_message_report_view = PrivateMessageReportView::read(&mut context.pool(), report_id)
PrivateMessageReportView::read(&mut context.pool(), report_id).await?; .await?
.ok_or(LemmyErrorType::CouldntFindPrivateMessageReport)?;
Ok(Json(PrivateMessageReportResponse { Ok(Json(PrivateMessageReportResponse {
private_message_report_view, private_message_report_view,

View file

@ -5,13 +5,15 @@ use lemmy_api_common::{
utils::build_federated_instances, utils::build_federated_instances,
}; };
use lemmy_db_views::structs::SiteView; use lemmy_db_views::structs::SiteView;
use lemmy_utils::error::LemmyResult; use lemmy_utils::{error::LemmyResult, LemmyErrorType};
#[tracing::instrument(skip(context))] #[tracing::instrument(skip(context))]
pub async fn get_federated_instances( pub async fn get_federated_instances(
context: Data<LemmyContext>, context: Data<LemmyContext>,
) -> LemmyResult<Json<GetFederatedInstancesResponse>> { ) -> LemmyResult<Json<GetFederatedInstancesResponse>> {
let site_view = SiteView::read_local(&mut context.pool()).await?; let site_view = SiteView::read_local(&mut context.pool())
.await?
.ok_or(LemmyErrorType::LocalSiteNotSetup)?;
let federated_instances = let federated_instances =
build_federated_instances(&site_view.local_site, &mut context.pool()).await?; build_federated_instances(&site_view.local_site, &mut context.pool()).await?;

View file

@ -55,7 +55,9 @@ pub async fn leave_admin(
ModAdd::create(&mut context.pool(), &form).await?; ModAdd::create(&mut context.pool(), &form).await?;
// Reread site and admins // Reread site and admins
let site_view = SiteView::read_local(&mut context.pool()).await?; let site_view = SiteView::read_local(&mut context.pool())
.await?
.ok_or(LemmyErrorType::LocalSiteNotSetup)?;
let admins = PersonView::admins(&mut context.pool()).await?; let admins = PersonView::admins(&mut context.pool()).await?;
let all_languages = Language::read_all(&mut context.pool()).await?; let all_languages = Language::read_all(&mut context.pool()).await?;

View file

@ -4,8 +4,7 @@ use lemmy_api_common::{
person::{ListMedia, ListMediaResponse}, person::{ListMedia, ListMediaResponse},
utils::is_admin, utils::is_admin,
}; };
use lemmy_db_schema::source::images::LocalImage; use lemmy_db_views::structs::{LocalImageView, LocalUserView};
use lemmy_db_views::structs::LocalUserView;
use lemmy_utils::error::LemmyResult; use lemmy_utils::error::LemmyResult;
#[tracing::instrument(skip(context))] #[tracing::instrument(skip(context))]
@ -19,6 +18,6 @@ pub async fn list_all_media(
let page = data.page; let page = data.page;
let limit = data.limit; let limit = data.limit;
let images = LocalImage::get_all(&mut context.pool(), page, limit).await?; let images = LocalImageView::get_all(&mut context.pool(), page, limit).await?;
Ok(Json(ListMediaResponse { images })) Ok(Json(ListMediaResponse { images }))
} }

View file

@ -15,7 +15,7 @@ use lemmy_db_schema::{
traits::Crud, traits::Crud,
}; };
use lemmy_db_views::structs::{CommentView, LocalUserView}; use lemmy_db_views::structs::{CommentView, LocalUserView};
use lemmy_utils::error::LemmyResult; use lemmy_utils::{error::LemmyResult, LemmyErrorType};
#[tracing::instrument(skip(context))] #[tracing::instrument(skip(context))]
pub async fn purge_comment( pub async fn purge_comment(
@ -29,7 +29,9 @@ pub async fn purge_comment(
let comment_id = data.comment_id; let comment_id = data.comment_id;
// Read the comment to get the post_id and community // Read the comment to get the post_id and community
let comment_view = CommentView::read(&mut context.pool(), comment_id, None).await?; let comment_view = CommentView::read(&mut context.pool(), comment_id, None)
.await?
.ok_or(LemmyErrorType::CouldntFindComment)?;
let post_id = comment_view.comment.post_id; let post_id = comment_view.comment.post_id;

View file

@ -16,7 +16,7 @@ use lemmy_db_schema::{
traits::Crud, traits::Crud,
}; };
use lemmy_db_views::structs::LocalUserView; use lemmy_db_views::structs::LocalUserView;
use lemmy_utils::error::LemmyResult; use lemmy_utils::{error::LemmyResult, LemmyErrorType};
#[tracing::instrument(skip(context))] #[tracing::instrument(skip(context))]
pub async fn purge_community( pub async fn purge_community(
@ -28,7 +28,9 @@ pub async fn purge_community(
is_admin(&local_user_view)?; is_admin(&local_user_view)?;
// Read the community to get its images // Read the community to get its images
let community = Community::read(&mut context.pool(), data.community_id).await?; let community = Community::read(&mut context.pool(), data.community_id)
.await?
.ok_or(LemmyErrorType::CouldntFindCommunity)?;
if let Some(banner) = &community.banner { if let Some(banner) = &community.banner {
purge_image_from_pictrs(banner, &context).await.ok(); purge_image_from_pictrs(banner, &context).await.ok();

View file

@ -16,7 +16,7 @@ use lemmy_db_schema::{
traits::Crud, traits::Crud,
}; };
use lemmy_db_views::structs::LocalUserView; use lemmy_db_views::structs::LocalUserView;
use lemmy_utils::error::LemmyResult; use lemmy_utils::{error::LemmyResult, LemmyErrorType};
#[tracing::instrument(skip(context))] #[tracing::instrument(skip(context))]
pub async fn purge_person( pub async fn purge_person(
@ -27,7 +27,9 @@ pub async fn purge_person(
// Only let admin purge an item // Only let admin purge an item
is_admin(&local_user_view)?; is_admin(&local_user_view)?;
let person = Person::read(&mut context.pool(), data.person_id).await?; let person = Person::read(&mut context.pool(), data.person_id)
.await?
.ok_or(LemmyErrorType::CouldntFindPerson)?;
ban_nonlocal_user_from_local_communities( ban_nonlocal_user_from_local_communities(
&local_user_view, &local_user_view,
&person, &person,

View file

@ -16,7 +16,7 @@ use lemmy_db_schema::{
traits::Crud, traits::Crud,
}; };
use lemmy_db_views::structs::LocalUserView; use lemmy_db_views::structs::LocalUserView;
use lemmy_utils::error::LemmyResult; use lemmy_utils::{error::LemmyResult, LemmyErrorType};
#[tracing::instrument(skip(context))] #[tracing::instrument(skip(context))]
pub async fn purge_post( pub async fn purge_post(
@ -28,7 +28,9 @@ pub async fn purge_post(
is_admin(&local_user_view)?; is_admin(&local_user_view)?;
// Read the post to get the community_id // Read the post to get the community_id
let post = Post::read(&mut context.pool(), data.post_id).await?; let post = Post::read(&mut context.pool(), data.post_id)
.await?
.ok_or(LemmyErrorType::CouldntFindPost)?;
// Purge image // Purge image
if let Some(url) = &post.url { if let Some(url) = &post.url {

View file

@ -10,10 +10,10 @@ use lemmy_db_schema::{
registration_application::{RegistrationApplication, RegistrationApplicationUpdateForm}, registration_application::{RegistrationApplication, RegistrationApplicationUpdateForm},
}, },
traits::Crud, traits::Crud,
utils::diesel_option_overwrite, utils::diesel_string_update,
}; };
use lemmy_db_views::structs::{LocalUserView, RegistrationApplicationView}; use lemmy_db_views::structs::{LocalUserView, RegistrationApplicationView};
use lemmy_utils::error::LemmyResult; use lemmy_utils::{error::LemmyResult, LemmyErrorType};
pub async fn approve_registration_application( pub async fn approve_registration_application(
data: Json<ApproveRegistrationApplication>, data: Json<ApproveRegistrationApplication>,
@ -26,7 +26,7 @@ pub async fn approve_registration_application(
is_admin(&local_user_view)?; is_admin(&local_user_view)?;
// Update the registration with reason, admin_id // Update the registration with reason, admin_id
let deny_reason = diesel_option_overwrite(data.deny_reason.clone()); let deny_reason = diesel_string_update(data.deny_reason.as_deref());
let app_form = RegistrationApplicationUpdateForm { let app_form = RegistrationApplicationUpdateForm {
admin_id: Some(Some(local_user_view.person.id)), admin_id: Some(Some(local_user_view.person.id)),
deny_reason, deny_reason,
@ -45,8 +45,9 @@ pub async fn approve_registration_application(
LocalUser::update(&mut context.pool(), approved_user_id, &local_user_form).await?; LocalUser::update(&mut context.pool(), approved_user_id, &local_user_form).await?;
if data.approve { if data.approve {
let approved_local_user_view = let approved_local_user_view = LocalUserView::read(&mut context.pool(), approved_user_id)
LocalUserView::read(&mut context.pool(), approved_user_id).await?; .await?
.ok_or(LemmyErrorType::CouldntFindLocalUser)?;
if approved_local_user_view.local_user.email.is_some() { if approved_local_user_view.local_user.email.is_some() {
send_application_approved_email(&approved_local_user_view, context.settings()).await?; send_application_approved_email(&approved_local_user_view, context.settings()).await?;
@ -54,8 +55,9 @@ pub async fn approve_registration_application(
} }
// Read the view // Read the view
let registration_application = let registration_application = RegistrationApplicationView::read(&mut context.pool(), app_id)
RegistrationApplicationView::read(&mut context.pool(), app_id).await?; .await?
.ok_or(LemmyErrorType::CouldntFindRegistrationApplication)?;
Ok(Json(RegistrationApplicationResponse { Ok(Json(RegistrationApplicationResponse {
registration_application, registration_application,

View file

@ -25,7 +25,7 @@ full = [
"lemmy_db_views_moderator/full", "lemmy_db_views_moderator/full",
"lemmy_utils/full", "lemmy_utils/full",
"activitypub_federation", "activitypub_federation",
"encoding", "encoding_rs",
"reqwest-middleware", "reqwest-middleware",
"webpage", "webpage",
"ts-rs", "ts-rs",
@ -66,13 +66,13 @@ actix-web = { workspace = true, optional = true }
enum-map = { workspace = true } enum-map = { workspace = true }
urlencoding = { workspace = true } urlencoding = { workspace = true }
mime = { version = "0.3.17", optional = true } mime = { version = "0.3.17", optional = true }
webpage = { version = "1.6", default-features = false, features = [ webpage = { version = "2.0", default-features = false, features = [
"serde", "serde",
], optional = true } ], optional = true }
encoding = { version = "0.2.33", optional = true } encoding_rs = { version = "0.8.34", optional = true }
jsonwebtoken = { version = "8.3.0", optional = true } jsonwebtoken = { version = "9.3.0", optional = true }
# necessary for wasmt compilation # necessary for wasmt compilation
getrandom = { version = "0.2.12", features = ["js"] } getrandom = { version = "0.2.15", features = ["js"] }
[package.metadata.cargo-machete] [package.metadata.cargo-machete]
ignored = ["getrandom"] ignored = ["getrandom"]

View file

@ -27,6 +27,7 @@ use lemmy_db_views_actor::structs::CommunityView;
use lemmy_utils::{ use lemmy_utils::{
error::LemmyResult, error::LemmyResult,
utils::{markdown::markdown_to_html, mention::MentionData}, utils::{markdown::markdown_to_html, mention::MentionData},
LemmyErrorType,
}; };
pub async fn build_comment_response( pub async fn build_comment_response(
@ -36,7 +37,9 @@ pub async fn build_comment_response(
recipient_ids: Vec<LocalUserId>, recipient_ids: Vec<LocalUserId>,
) -> LemmyResult<CommentResponse> { ) -> LemmyResult<CommentResponse> {
let person_id = local_user_view.map(|l| l.person.id); let person_id = local_user_view.map(|l| l.person.id);
let comment_view = CommentView::read(&mut context.pool(), comment_id, person_id).await?; let comment_view = CommentView::read(&mut context.pool(), comment_id, person_id)
.await?
.ok_or(LemmyErrorType::CouldntFindComment)?;
Ok(CommentResponse { Ok(CommentResponse {
comment_view, comment_view,
recipient_ids, recipient_ids,
@ -58,7 +61,8 @@ pub async fn build_community_response(
Some(person_id), Some(person_id),
is_mod_or_admin, is_mod_or_admin,
) )
.await?; .await?
.ok_or(LemmyErrorType::CouldntFindCommunity)?;
let discussion_languages = CommunityLanguage::read(&mut context.pool(), community_id).await?; let discussion_languages = CommunityLanguage::read(&mut context.pool(), community_id).await?;
Ok(Json(CommunityResponse { Ok(Json(CommunityResponse {
@ -82,7 +86,8 @@ pub async fn build_post_response(
Some(person.id), Some(person.id),
is_mod_or_admin, is_mod_or_admin,
) )
.await?; .await?
.ok_or(LemmyErrorType::CouldntFindPost)?;
Ok(Json(PostResponse { post_view })) Ok(Json(PostResponse { post_view }))
} }
@ -99,7 +104,9 @@ pub async fn send_local_notifs(
let inbox_link = format!("{}/inbox", context.settings().get_protocol_and_hostname()); let inbox_link = format!("{}/inbox", context.settings().get_protocol_and_hostname());
// Read the comment view to get extra info // Read the comment view to get extra info
let comment_view = CommentView::read(&mut context.pool(), comment_id, None).await?; let comment_view = CommentView::read(&mut context.pool(), comment_id, None)
.await?
.ok_or(LemmyErrorType::CouldntFindComment)?;
let comment = comment_view.comment; let comment = comment_view.comment;
let post = comment_view.post; let post = comment_view.post;
let community = comment_view.community; let community = comment_view.community;
@ -111,10 +118,11 @@ pub async fn send_local_notifs(
{ {
let mention_name = mention.name.clone(); let mention_name = mention.name.clone();
let user_view = LocalUserView::read_from_name(&mut context.pool(), &mention_name).await; let user_view = LocalUserView::read_from_name(&mut context.pool(), &mention_name).await;
if let Ok(mention_user_view) = user_view { if let Ok(Some(mention_user_view)) = user_view {
// TODO // TODO
// At some point, make it so you can't tag the parent creator either // At some point, make it so you can't tag the parent creator either
// Potential duplication of notifications, one for reply and the other for mention, is handled below by checking recipient ids // Potential duplication of notifications, one for reply and the other for mention, is handled
// below by checking recipient ids
recipient_ids.push(mention_user_view.local_user.id); recipient_ids.push(mention_user_view.local_user.id);
let user_mention_form = PersonMentionInsertForm { let user_mention_form = PersonMentionInsertForm {
@ -146,7 +154,9 @@ pub async fn send_local_notifs(
// Send comment_reply to the parent commenter / poster // Send comment_reply to the parent commenter / poster
if let Some(parent_comment_id) = comment.parent_comment_id() { if let Some(parent_comment_id) = comment.parent_comment_id() {
let parent_comment = Comment::read(&mut context.pool(), parent_comment_id).await?; let parent_comment = Comment::read(&mut context.pool(), parent_comment_id)
.await?
.ok_or(LemmyErrorType::CouldntFindComment)?;
// Get the parent commenter local_user // Get the parent commenter local_user
let parent_creator_id = parent_comment.creator_id; let parent_creator_id = parent_comment.creator_id;
@ -165,7 +175,7 @@ pub async fn send_local_notifs(
// Don't send a notif to yourself // Don't send a notif to yourself
if parent_comment.creator_id != person.id && !check_blocks { if parent_comment.creator_id != person.id && !check_blocks {
let user_view = LocalUserView::read_person(&mut context.pool(), parent_creator_id).await; let user_view = LocalUserView::read_person(&mut context.pool(), parent_creator_id).await;
if let Ok(parent_user_view) = user_view { if let Ok(Some(parent_user_view)) = user_view {
// Don't duplicate notif if already mentioned by checking recipient ids // Don't duplicate notif if already mentioned by checking recipient ids
if !recipient_ids.contains(&parent_user_view.local_user.id) { if !recipient_ids.contains(&parent_user_view.local_user.id) {
recipient_ids.push(parent_user_view.local_user.id); recipient_ids.push(parent_user_view.local_user.id);
@ -212,7 +222,7 @@ pub async fn send_local_notifs(
if post.creator_id != person.id && !check_blocks { if post.creator_id != person.id && !check_blocks {
let creator_id = post.creator_id; let creator_id = post.creator_id;
let parent_user = LocalUserView::read_person(&mut context.pool(), creator_id).await; let parent_user = LocalUserView::read_person(&mut context.pool(), creator_id).await;
if let Ok(parent_user_view) = parent_user { if let Ok(Some(parent_user_view)) = parent_user {
if !recipient_ids.contains(&parent_user_view.local_user.id) { if !recipient_ids.contains(&parent_user_view.local_user.id) {
recipient_ids.push(parent_user_view.local_user.id); recipient_ids.push(parent_user_view.local_user.id);

View file

@ -1,9 +1,10 @@
use crate::{context::LemmyContext, sensitive::Sensitive}; use crate::context::LemmyContext;
use actix_web::{http::header::USER_AGENT, HttpRequest}; use actix_web::{http::header::USER_AGENT, HttpRequest};
use chrono::Utc; use chrono::Utc;
use jsonwebtoken::{decode, encode, DecodingKey, EncodingKey, Header, Validation}; use jsonwebtoken::{decode, encode, DecodingKey, EncodingKey, Header, Validation};
use lemmy_db_schema::{ use lemmy_db_schema::{
newtypes::LocalUserId, newtypes::LocalUserId,
sensitive::SensitiveString,
source::login_token::{LoginToken, LoginTokenCreateForm}, source::login_token::{LoginToken, LoginTokenCreateForm},
}; };
use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult}; use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult};
@ -40,7 +41,7 @@ impl Claims {
user_id: LocalUserId, user_id: LocalUserId,
req: HttpRequest, req: HttpRequest,
context: &LemmyContext, context: &LemmyContext,
) -> LemmyResult<Sensitive<String>> { ) -> LemmyResult<SensitiveString> {
let hostname = context.settings().hostname.clone(); let hostname = context.settings().hostname.clone();
let my_claims = Claims { let my_claims = Claims {
sub: user_id.0.to_string(), sub: user_id.0.to_string(),
@ -50,7 +51,7 @@ impl Claims {
let secret = &context.secret().jwt_secret; let secret = &context.secret().jwt_secret;
let key = EncodingKey::from_secret(secret.as_ref()); let key = EncodingKey::from_secret(secret.as_ref());
let token = encode(&Header::default(), &my_claims, &key)?; let token: SensitiveString = encode(&Header::default(), &my_claims, &key)?.into();
let ip = req let ip = req
.connection_info() .connection_info()
.realip_remote_addr() .realip_remote_addr()
@ -67,7 +68,7 @@ impl Claims {
user_agent, user_agent,
}; };
LoginToken::create(&mut context.pool(), form).await?; LoginToken::create(&mut context.pool(), form).await?;
Ok(Sensitive::new(token)) Ok(token)
} }
} }
@ -99,7 +100,7 @@ mod tests {
async fn test_should_not_validate_user_token_after_password_change() { async fn test_should_not_validate_user_token_after_password_change() {
let pool_ = build_db_pool_for_tests().await; let pool_ = build_db_pool_for_tests().await;
let pool = &mut (&pool_).into(); let pool = &mut (&pool_).into();
let secret = Secret::init(pool).await.unwrap(); let secret = Secret::init(pool).await.unwrap().unwrap();
let context = LemmyContext::create( let context = LemmyContext::create(
pool_.clone(), pool_.clone(),
ClientBuilder::new(Client::default()).build(), ClientBuilder::new(Client::default()).build(),
@ -111,11 +112,7 @@ mod tests {
.await .await
.unwrap(); .unwrap();
let new_person = PersonInsertForm::builder() let new_person = PersonInsertForm::test_form(inserted_instance.id, "Gerry9812");
.name("Gerry9812".into())
.public_key("pubkey".to_string())
.instance_id(inserted_instance.id)
.build();
let inserted_person = Person::create(pool, &new_person).await.unwrap(); let inserted_person = Person::create(pool, &new_person).await.unwrap();

View file

@ -64,7 +64,7 @@ impl LemmyContext {
let client = ClientBuilder::new(client).build(); let client = ClientBuilder::new(client).build();
let secret = Secret { let secret = Secret {
id: 0, id: 0,
jwt_secret: String::new(), jwt_secret: String::new().into(),
}; };
let rate_limit_cell = RateLimitCell::with_test_config(); let rate_limit_cell = RateLimitCell::with_test_config();

View file

@ -14,7 +14,6 @@ pub mod private_message;
pub mod request; pub mod request;
#[cfg(feature = "full")] #[cfg(feature = "full")]
pub mod send_activity; pub mod send_activity;
pub mod sensitive;
pub mod site; pub mod site;
#[cfg(feature = "full")] #[cfg(feature = "full")]
pub mod utils; pub mod utils;

View file

@ -1,13 +1,13 @@
use crate::sensitive::Sensitive;
use lemmy_db_schema::{ use lemmy_db_schema::{
newtypes::{CommentReplyId, CommunityId, LanguageId, PersonId, PersonMentionId}, newtypes::{CommentReplyId, CommunityId, LanguageId, PersonId, PersonMentionId},
source::{images::LocalImage, site::Site}, sensitive::SensitiveString,
source::site::Site,
CommentSortType, CommentSortType,
ListingType, ListingType,
PostListingMode, PostListingMode,
SortType, SortType,
}; };
use lemmy_db_views::structs::{CommentView, PostView}; use lemmy_db_views::structs::{CommentView, LocalImageView, PostView};
use lemmy_db_views_actor::structs::{ use lemmy_db_views_actor::structs::{
CommentReplyView, CommentReplyView,
CommunityModeratorView, CommunityModeratorView,
@ -25,8 +25,8 @@ use ts_rs::TS;
#[cfg_attr(feature = "full", ts(export))] #[cfg_attr(feature = "full", ts(export))]
/// Logging into lemmy. /// Logging into lemmy.
pub struct Login { pub struct Login {
pub username_or_email: Sensitive<String>, pub username_or_email: SensitiveString,
pub password: Sensitive<String>, pub password: SensitiveString,
/// May be required, if totp is enabled for their account. /// May be required, if totp is enabled for their account.
pub totp_2fa_token: Option<String>, pub totp_2fa_token: Option<String>,
} }
@ -38,11 +38,11 @@ pub struct Login {
/// Register / Sign up to lemmy. /// Register / Sign up to lemmy.
pub struct Register { pub struct Register {
pub username: String, pub username: String,
pub password: Sensitive<String>, pub password: SensitiveString,
pub password_verify: Sensitive<String>, pub password_verify: SensitiveString,
pub show_nsfw: bool, pub show_nsfw: Option<bool>,
/// email is mandatory if email verification is enabled on the server /// email is mandatory if email verification is enabled on the server
pub email: Option<Sensitive<String>>, pub email: Option<SensitiveString>,
/// The UUID of the captcha item. /// The UUID of the captcha item.
pub captcha_uuid: Option<String>, pub captcha_uuid: Option<String>,
/// Your captcha answer. /// Your captcha answer.
@ -99,7 +99,7 @@ pub struct SaveUserSettings {
/// Your display name, which can contain strange characters, and does not need to be unique. /// Your display name, which can contain strange characters, and does not need to be unique.
pub display_name: Option<String>, pub display_name: Option<String>,
/// Your email. /// Your email.
pub email: Option<Sensitive<String>>, pub email: Option<SensitiveString>,
/// Your bio / info, in markdown. /// Your bio / info, in markdown.
pub bio: Option<String>, pub bio: Option<String>,
/// Your matrix user id. Ex: @my_user:matrix.org /// Your matrix user id. Ex: @my_user:matrix.org
@ -124,7 +124,8 @@ pub struct SaveUserSettings {
pub post_listing_mode: Option<PostListingMode>, pub post_listing_mode: Option<PostListingMode>,
/// Whether to allow keyboard navigation (for browsing and interacting with posts and comments). /// Whether to allow keyboard navigation (for browsing and interacting with posts and comments).
pub enable_keyboard_navigation: Option<bool>, pub enable_keyboard_navigation: Option<bool>,
/// Whether user avatars or inline images in the UI that are gifs should be allowed to play or should be paused /// Whether user avatars or inline images in the UI that are gifs should be allowed to play or
/// should be paused
pub enable_animated_images: Option<bool>, pub enable_animated_images: Option<bool>,
/// Whether to auto-collapse bot comments. /// Whether to auto-collapse bot comments.
pub collapse_bot_comments: Option<bool>, pub collapse_bot_comments: Option<bool>,
@ -140,9 +141,9 @@ pub struct SaveUserSettings {
#[cfg_attr(feature = "full", ts(export))] #[cfg_attr(feature = "full", ts(export))]
/// Changes your account password. /// Changes your account password.
pub struct ChangePassword { pub struct ChangePassword {
pub new_password: Sensitive<String>, pub new_password: SensitiveString,
pub new_password_verify: Sensitive<String>, pub new_password_verify: SensitiveString,
pub old_password: Sensitive<String>, pub old_password: SensitiveString,
} }
#[skip_serializing_none] #[skip_serializing_none]
@ -151,8 +152,9 @@ pub struct ChangePassword {
#[cfg_attr(feature = "full", ts(export))] #[cfg_attr(feature = "full", ts(export))]
/// A response for your login. /// A response for your login.
pub struct LoginResponse { pub struct LoginResponse {
/// This is None in response to `Register` if email verification is enabled, or the server requires registration applications. /// This is None in response to `Register` if email verification is enabled, or the server
pub jwt: Option<Sensitive<String>>, /// requires registration applications.
pub jwt: Option<SensitiveString>,
/// If registration applications are required, this will return true for a signup response. /// If registration applications are required, this will return true for a signup response.
pub registration_created: bool, pub registration_created: bool,
/// If email verifications are required, this will return true for a signup response. /// If email verifications are required, this will return true for a signup response.
@ -340,7 +342,7 @@ pub struct CommentReplyResponse {
#[cfg_attr(feature = "full", ts(export))] #[cfg_attr(feature = "full", ts(export))]
/// Delete your account. /// Delete your account.
pub struct DeleteAccount { pub struct DeleteAccount {
pub password: Sensitive<String>, pub password: SensitiveString,
pub delete_content: bool, pub delete_content: bool,
} }
@ -349,7 +351,7 @@ pub struct DeleteAccount {
#[cfg_attr(feature = "full", ts(export))] #[cfg_attr(feature = "full", ts(export))]
/// Reset your password via email. /// Reset your password via email.
pub struct PasswordReset { pub struct PasswordReset {
pub email: Sensitive<String>, pub email: SensitiveString,
} }
#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq, Eq, Hash)] #[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq, Eq, Hash)]
@ -357,9 +359,9 @@ pub struct PasswordReset {
#[cfg_attr(feature = "full", ts(export))] #[cfg_attr(feature = "full", ts(export))]
/// Change your password after receiving a reset request. /// Change your password after receiving a reset request.
pub struct PasswordChangeAfterReset { pub struct PasswordChangeAfterReset {
pub token: Sensitive<String>, pub token: SensitiveString,
pub password: Sensitive<String>, pub password: SensitiveString,
pub password_verify: Sensitive<String>, pub password_verify: SensitiveString,
} }
#[skip_serializing_none] #[skip_serializing_none]
@ -405,7 +407,7 @@ pub struct VerifyEmail {
#[cfg_attr(feature = "full", derive(TS))] #[cfg_attr(feature = "full", derive(TS))]
#[cfg_attr(feature = "full", ts(export))] #[cfg_attr(feature = "full", ts(export))]
pub struct GenerateTotpSecretResponse { pub struct GenerateTotpSecretResponse {
pub totp_secret_url: Sensitive<String>, pub totp_secret_url: SensitiveString,
} }
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Hash)] #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Hash)]
@ -437,5 +439,5 @@ pub struct ListMedia {
#[cfg_attr(feature = "full", derive(TS))] #[cfg_attr(feature = "full", derive(TS))]
#[cfg_attr(feature = "full", ts(export))] #[cfg_attr(feature = "full", ts(export))]
pub struct ListMediaResponse { pub struct ListMediaResponse {
pub images: Vec<LocalImage>, pub images: Vec<LocalImageView>,
} }

View file

@ -10,7 +10,6 @@ use serde::{Deserialize, Serialize};
use serde_with::skip_serializing_none; use serde_with::skip_serializing_none;
#[cfg(feature = "full")] #[cfg(feature = "full")]
use ts_rs::TS; use ts_rs::TS;
use url::Url;
#[skip_serializing_none] #[skip_serializing_none]
#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq, Eq, Hash)] #[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq, Eq, Hash)]
@ -20,8 +19,7 @@ use url::Url;
pub struct CreatePost { pub struct CreatePost {
pub name: String, pub name: String,
pub community_id: CommunityId, pub community_id: CommunityId,
#[cfg_attr(feature = "full", ts(type = "string"))] pub url: Option<String>,
pub url: Option<Url>,
/// An optional body for the post in markdown. /// An optional body for the post in markdown.
pub body: Option<String>, pub body: Option<String>,
/// An optional alt_text, usable for image posts. /// An optional alt_text, usable for image posts.
@ -30,9 +28,8 @@ pub struct CreatePost {
pub honeypot: Option<String>, pub honeypot: Option<String>,
pub nsfw: Option<bool>, pub nsfw: Option<bool>,
pub language_id: Option<LanguageId>, pub language_id: Option<LanguageId>,
#[cfg_attr(feature = "full", ts(type = "string"))]
/// Instead of fetching a thumbnail, use a custom one. /// Instead of fetching a thumbnail, use a custom one.
pub custom_thumbnail: Option<Url>, pub custom_thumbnail: Option<String>,
} }
#[derive(Debug, Serialize, Deserialize, Clone)] #[derive(Debug, Serialize, Deserialize, Clone)]
@ -114,17 +111,15 @@ pub struct CreatePostLike {
pub struct EditPost { pub struct EditPost {
pub post_id: PostId, pub post_id: PostId,
pub name: Option<String>, pub name: Option<String>,
#[cfg_attr(feature = "full", ts(type = "string"))] pub url: Option<String>,
pub url: Option<Url>,
/// An optional body for the post in markdown. /// An optional body for the post in markdown.
pub body: Option<String>, pub body: Option<String>,
/// An optional alt_text, usable for image posts. /// An optional alt_text, usable for image posts.
pub alt_text: Option<String>, pub alt_text: Option<String>,
pub nsfw: Option<bool>, pub nsfw: Option<bool>,
pub language_id: Option<LanguageId>, pub language_id: Option<LanguageId>,
#[cfg_attr(feature = "full", ts(type = "string"))]
/// Instead of fetching a thumbnail, use a custom one. /// Instead of fetching a thumbnail, use a custom one.
pub custom_thumbnail: Option<Url>, pub custom_thumbnail: Option<String>,
} }
#[derive(Debug, Serialize, Deserialize, Clone, Copy, Default, PartialEq, Eq, Hash)] #[derive(Debug, Serialize, Deserialize, Clone, Copy, Default, PartialEq, Eq, Hash)]
@ -249,8 +244,7 @@ pub struct ListPostReportsResponse {
#[cfg_attr(feature = "full", ts(export))] #[cfg_attr(feature = "full", ts(export))]
/// Get metadata for a given site. /// Get metadata for a given site.
pub struct GetSiteMetadata { pub struct GetSiteMetadata {
#[cfg_attr(feature = "full", ts(type = "string"))] pub url: String,
pub url: Url,
} }
#[derive(Debug, Serialize, Deserialize, Clone)] #[derive(Debug, Serialize, Deserialize, Clone)]
@ -270,8 +264,6 @@ pub struct LinkMetadata {
#[serde(flatten)] #[serde(flatten)]
pub opengraph_data: OpenGraphData, pub opengraph_data: OpenGraphData,
pub content_type: Option<String>, pub content_type: Option<String>,
#[serde(skip)]
pub thumbnail: Option<DbUrl>,
} }
#[skip_serializing_none] #[skip_serializing_none]

View file

@ -3,10 +3,11 @@ use crate::{
lemmy_db_schema::traits::Crud, lemmy_db_schema::traits::Crud,
post::{LinkMetadata, OpenGraphData}, post::{LinkMetadata, OpenGraphData},
send_activity::{ActivityChannel, SendActivityData}, send_activity::{ActivityChannel, SendActivityData},
utils::{local_site_opt_to_sensitive, proxy_image_link, proxy_image_link_opt_apub}, utils::{local_site_opt_to_sensitive, proxy_image_link},
}; };
use activitypub_federation::config::Data; use activitypub_federation::config::Data;
use encoding::{all::encodings, DecoderTrap}; use chrono::{DateTime, Utc};
use encoding_rs::{Encoding, UTF_8};
use lemmy_db_schema::{ use lemmy_db_schema::{
newtypes::DbUrl, newtypes::DbUrl,
source::{ source::{
@ -18,14 +19,13 @@ use lemmy_db_schema::{
use lemmy_utils::{ use lemmy_utils::{
error::{LemmyError, LemmyErrorType, LemmyResult}, error::{LemmyError, LemmyErrorType, LemmyResult},
settings::structs::{PictrsImageMode, Settings}, settings::structs::{PictrsImageMode, Settings},
spawn_try_task,
REQWEST_TIMEOUT, REQWEST_TIMEOUT,
VERSION, VERSION,
}; };
use mime::Mime; use mime::Mime;
use reqwest::{header::CONTENT_TYPE, Client, ClientBuilder}; use reqwest::{header::CONTENT_TYPE, Client, ClientBuilder};
use reqwest_middleware::ClientWithMiddleware; use reqwest_middleware::ClientWithMiddleware;
use serde::Deserialize; use serde::{Deserialize, Serialize};
use tracing::info; use tracing::info;
use url::Url; use url::Url;
use urlencoding::encode; use urlencoding::encode;
@ -42,11 +42,7 @@ pub fn client_builder(settings: &Settings) -> ClientBuilder {
/// Fetches metadata for the given link and optionally generates thumbnail. /// Fetches metadata for the given link and optionally generates thumbnail.
#[tracing::instrument(skip_all)] #[tracing::instrument(skip_all)]
pub async fn fetch_link_metadata( pub async fn fetch_link_metadata(url: &Url, context: &LemmyContext) -> LemmyResult<LinkMetadata> {
url: &Url,
generate_thumbnail: bool,
context: &LemmyContext,
) -> LemmyResult<LinkMetadata> {
info!("Fetching site metadata for url: {}", url); info!("Fetching site metadata for url: {}", url);
let response = context.client().get(url.as_str()).send().await?; let response = context.client().get(url.as_str()).send().await?;
@ -63,118 +59,85 @@ pub async fn fetch_link_metadata(
let opengraph_data = extract_opengraph_data(&html_bytes, url) let opengraph_data = extract_opengraph_data(&html_bytes, url)
.map_err(|e| info!("{e}")) .map_err(|e| info!("{e}"))
.unwrap_or_default(); .unwrap_or_default();
let thumbnail =
extract_thumbnail_from_opengraph_data(url, &opengraph_data, generate_thumbnail, context).await;
Ok(LinkMetadata { Ok(LinkMetadata {
opengraph_data, opengraph_data,
content_type: content_type.map(|c| c.to_string()), content_type: content_type.map(|c| c.to_string()),
thumbnail,
}) })
} }
#[tracing::instrument(skip_all)] /// Generates and saves a post thumbnail and metadata.
pub async fn fetch_link_metadata_opt(
url: Option<&Url>,
generate_thumbnail: bool,
context: &LemmyContext,
) -> LinkMetadata {
match &url {
Some(url) => fetch_link_metadata(url, generate_thumbnail, context)
.await
.unwrap_or_default(),
_ => Default::default(),
}
}
/// Generate post thumbnail in background task, because some sites can be very slow to respond.
/// ///
/// Takes a callback to generate a send activity task, so that post can be federated with metadata. /// Takes a callback to generate a send activity task, so that post can be federated with metadata.
pub fn generate_post_link_metadata( ///
/// TODO: `federated_thumbnail` param can be removed once we federate full metadata and can
/// write it to db directly, without calling this function.
/// https://github.com/LemmyNet/lemmy/issues/4598
pub async fn generate_post_link_metadata(
post: Post, post: Post,
custom_thumbnail: Option<Url>, custom_thumbnail: Option<Url>,
send_activity: impl FnOnce(Post) -> Option<SendActivityData> + Send + 'static, send_activity: impl FnOnce(Post) -> Option<SendActivityData> + Send + 'static,
local_site: Option<LocalSite>, local_site: Option<LocalSite>,
context: Data<LemmyContext>, context: Data<LemmyContext>,
) { ) -> LemmyResult<()> {
spawn_try_task(async move { let metadata = match &post.url {
// Decide if the thumbnail should be generated Some(url) => fetch_link_metadata(url, &context).await.unwrap_or_default(),
let allow_sensitive = local_site_opt_to_sensitive(&local_site); _ => Default::default(),
let page_is_sensitive = post.nsfw; };
let allow_generate_thumbnail = allow_sensitive || !page_is_sensitive;
let do_generate_thumbnail =
allow_generate_thumbnail && custom_thumbnail.is_none() && post.thumbnail_url.is_none();
// Generate local thumbnail only if no thumbnail was federated and 'sensitive' attributes allow it. let is_image_post = metadata
let metadata = fetch_link_metadata_opt( .content_type
post.url.as_ref().map(DbUrl::inner), .as_ref()
do_generate_thumbnail, .is_some_and(|content_type| content_type.starts_with("image"));
&context,
)
.await;
// If its an image post, it needs to overwrite the thumbnail, and take precedence // Decide if we are allowed to generate local thumbnail
let image_url = if metadata let allow_sensitive = local_site_opt_to_sensitive(&local_site);
.content_type let allow_generate_thumbnail = allow_sensitive || !post.nsfw;
.as_ref()
.is_some_and(|content_type| content_type.starts_with("image"))
{
post.url.map(Into::into)
} else {
None
};
// Build the thumbnail url based on either the post image url, custom thumbnail, metadata fetch, or existing thumbnail. let image_url = if is_image_post {
let thumbnail_url = image_url post.url
.or(custom_thumbnail) } else {
.or(metadata.thumbnail.map(Into::into)) metadata.opengraph_data.image.clone()
.or(post.thumbnail_url.map(Into::into)); };
// Proxy the image fetch if necessary let thumbnail_url = if let (false, Some(url)) = (is_image_post, custom_thumbnail) {
let proxied_thumbnail_url = proxy_image_link_opt_apub(thumbnail_url, &context).await?; proxy_image_link(url, &context).await.ok()
} else if let (true, Some(url)) = (allow_generate_thumbnail, image_url) {
generate_pictrs_thumbnail(&url, &context)
.await
.ok()
.map(Into::into)
} else {
metadata.opengraph_data.image.clone()
};
let form = PostUpdateForm { let form = PostUpdateForm {
embed_title: Some(metadata.opengraph_data.title), embed_title: Some(metadata.opengraph_data.title),
embed_description: Some(metadata.opengraph_data.description), embed_description: Some(metadata.opengraph_data.description),
embed_video_url: Some(metadata.opengraph_data.embed_video_url), embed_video_url: Some(metadata.opengraph_data.embed_video_url),
thumbnail_url: Some(proxied_thumbnail_url), thumbnail_url: Some(thumbnail_url),
url_content_type: Some(metadata.content_type), url_content_type: Some(metadata.content_type),
..Default::default() ..Default::default()
}; };
let updated_post = Post::update(&mut context.pool(), post.id, &form).await?; let updated_post = Post::update(&mut context.pool(), post.id, &form).await?;
if let Some(send_activity) = send_activity(updated_post) { if let Some(send_activity) = send_activity(updated_post) {
ActivityChannel::submit_activity(send_activity, &context).await?; ActivityChannel::submit_activity(send_activity, &context).await?;
} }
Ok(()) Ok(())
});
} }
/// Extract site metadata from HTML Opengraph attributes. /// Extract site metadata from HTML Opengraph attributes.
fn extract_opengraph_data(html_bytes: &[u8], url: &Url) -> LemmyResult<OpenGraphData> { fn extract_opengraph_data(html_bytes: &[u8], url: &Url) -> LemmyResult<OpenGraphData> {
let html = String::from_utf8_lossy(html_bytes); let html = String::from_utf8_lossy(html_bytes);
// Make sure the first line is doctype html
let first_line = html
.trim_start()
.lines()
.next()
.ok_or(LemmyErrorType::NoLinesInHtml)?
.to_lowercase();
if !first_line.starts_with("<!doctype html") {
Err(LemmyErrorType::SiteMetadataPageIsNotDoctypeHtml)?
}
let mut page = HTML::from_string(html.to_string(), None)?; let mut page = HTML::from_string(html.to_string(), None)?;
// If the web page specifies that it isn't actually UTF-8, re-decode the received bytes with the // If the web page specifies that it isn't actually UTF-8, re-decode the received bytes with the
// proper encoding. If the specified encoding cannot be found, fall back to the original UTF-8 // proper encoding. If the specified encoding cannot be found, fall back to the original UTF-8
// version. // version.
if let Some(charset) = page.meta.get("charset") { if let Some(charset) = page.meta.get("charset") {
if charset.to_lowercase() != "utf-8" { if charset != UTF_8.name() {
if let Some(encoding_ref) = encodings().iter().find(|e| e.name() == charset) { if let Some(encoding) = Encoding::for_label(charset.as_bytes()) {
if let Ok(html_with_encoding) = encoding_ref.decode(html_bytes, DecoderTrap::Replace) { page = HTML::from_string(encoding.decode(html_bytes).0.into(), None)?;
page = HTML::from_string(html_with_encoding, None)?;
}
} }
} }
} }
@ -213,41 +176,40 @@ fn extract_opengraph_data(html_bytes: &[u8], url: &Url) -> LemmyResult<OpenGraph
}) })
} }
#[tracing::instrument(skip_all)] #[derive(Deserialize, Serialize, Debug)]
pub async fn extract_thumbnail_from_opengraph_data( pub struct PictrsResponse {
url: &Url, pub files: Option<Vec<PictrsFile>>,
opengraph_data: &OpenGraphData, pub msg: String,
generate_thumbnail: bool, }
context: &LemmyContext,
) -> Option<DbUrl> { #[derive(Deserialize, Serialize, Debug)]
if generate_thumbnail { pub struct PictrsFile {
let image_url = opengraph_data pub file: String,
.image pub delete_token: String,
.as_ref() pub details: PictrsFileDetails,
.map(DbUrl::inner) }
.unwrap_or(url);
generate_pictrs_thumbnail(image_url, context) impl PictrsFile {
.await pub fn thumbnail_url(&self, protocol_and_hostname: &str) -> Result<Url, url::ParseError> {
.ok() Url::parse(&format!(
.map(Into::into) "{protocol_and_hostname}/pictrs/image/{}",
} else { self.file
opengraph_data.image.clone() ))
} }
} }
#[derive(Deserialize, Debug)] /// Stores extra details about a Pictrs image.
struct PictrsResponse { #[derive(Deserialize, Serialize, Debug)]
files: Vec<PictrsFile>, pub struct PictrsFileDetails {
msg: String, /// In pixels
pub width: u16,
/// In pixels
pub height: u16,
pub content_type: String,
pub created_at: DateTime<Utc>,
} }
#[derive(Deserialize, Debug)] #[derive(Deserialize, Serialize, Debug)]
struct PictrsFile {
file: String,
delete_token: String,
}
#[derive(Deserialize, Debug)]
struct PictrsPurgeResponse { struct PictrsPurgeResponse {
msg: String, msg: String,
} }
@ -329,33 +291,34 @@ async fn generate_pictrs_thumbnail(image_url: &Url, context: &LemmyContext) -> L
encode(image_url.as_str()) encode(image_url.as_str())
); );
let response = context let res = context
.client() .client()
.get(&fetch_url) .get(&fetch_url)
.timeout(REQWEST_TIMEOUT) .timeout(REQWEST_TIMEOUT)
.send() .send()
.await?
.json::<PictrsResponse>()
.await?; .await?;
let response: PictrsResponse = response.json().await?; let files = res.files.unwrap_or_default();
if response.msg == "ok" { let image = files
let thumbnail_url = Url::parse(&format!( .first()
"{}/pictrs/image/{}", .ok_or(LemmyErrorType::PictrsResponseError(res.msg))?;
context.settings().get_protocol_and_hostname(),
response.files.first().expect("missing pictrs file").file let form = LocalImageForm {
))?; // This is none because its an internal request.
for uploaded_image in response.files { // IE, a local user shouldn't get to delete the thumbnails for their link posts
let form = LocalImageForm { local_user_id: None,
local_user_id: None, pictrs_alias: image.file.clone(),
pictrs_alias: uploaded_image.file.to_string(), pictrs_delete_token: image.delete_token.clone(),
pictrs_delete_token: uploaded_image.delete_token.to_string(), };
}; let protocol_and_hostname = context.settings().get_protocol_and_hostname();
LocalImage::create(&mut context.pool(), &form).await?; let thumbnail_url = image.thumbnail_url(&protocol_and_hostname)?;
}
Ok(thumbnail_url) LocalImage::create(&mut context.pool(), &form).await?;
} else {
Err(LemmyErrorType::PictrsResponseError(response.msg))? Ok(thumbnail_url)
}
} }
// TODO: get rid of this by reading content type from db // TODO: get rid of this by reading content type from db
@ -375,16 +338,19 @@ async fn is_image_content_type(client: &ClientWithMiddleware, url: &Url) -> Lemm
} }
} }
/// When adding a new avatar or similar image, delete the old one. /// When adding a new avatar, banner or similar image, delete the old one.
pub async fn replace_image( pub async fn replace_image(
new_image: &Option<String>, new_image: &Option<Option<DbUrl>>,
old_image: &Option<DbUrl>, old_image: &Option<DbUrl>,
context: &Data<LemmyContext>, context: &Data<LemmyContext>,
) -> LemmyResult<()> { ) -> LemmyResult<()> {
if new_image.is_some() { if let (Some(Some(new_image)), Some(old_image)) = (new_image, old_image) {
// Ignore errors because image may be stored externally. // Note: Oftentimes front ends will include the current image in the form.
if let Some(avatar) = &old_image { // In this case, deleting `old_image` would also be deletion of `new_image`,
let image = LocalImage::delete_by_url(&mut context.pool(), avatar) // so the deletion must be skipped for the image to be kept.
if new_image != old_image {
// Ignore errors because image may be stored externally.
let image = LocalImage::delete_by_url(&mut context.pool(), old_image)
.await .await
.ok(); .ok();
if let Some(image) = image { if let Some(image) = image {
@ -414,9 +380,7 @@ mod tests {
async fn test_link_metadata() { async fn test_link_metadata() {
let context = LemmyContext::init_test_context().await; let context = LemmyContext::init_test_context().await;
let sample_url = Url::parse("https://gitlab.com/IzzyOnDroid/repo/-/wikis/FAQ").unwrap(); let sample_url = Url::parse("https://gitlab.com/IzzyOnDroid/repo/-/wikis/FAQ").unwrap();
let sample_res = fetch_link_metadata(&sample_url, false, &context) let sample_res = fetch_link_metadata(&sample_url, &context).await.unwrap();
.await
.unwrap();
assert_eq!( assert_eq!(
Some("FAQ · Wiki · IzzyOnDroid / repo · GitLab".to_string()), Some("FAQ · Wiki · IzzyOnDroid / repo · GitLab".to_string()),
sample_res.opengraph_data.title sample_res.opengraph_data.title
@ -438,17 +402,8 @@ mod tests {
Some(mime::TEXT_HTML_UTF_8.to_string()), Some(mime::TEXT_HTML_UTF_8.to_string()),
sample_res.content_type sample_res.content_type
); );
assert!(sample_res.thumbnail.is_some());
} }
// #[test]
// fn test_pictshare() {
// let res = fetch_pictshare("https://upload.wikimedia.org/wikipedia/en/2/27/The_Mandalorian_logo.jpg");
// assert!(res.is_ok());
// let res_other = fetch_pictshare("https://upload.wikimedia.org/wikipedia/en/2/27/The_Mandalorian_logo.jpgaoeu");
// assert!(res_other.is_err());
// }
#[test] #[test]
fn test_resolve_image_url() { fn test_resolve_image_url() {
// url that lists the opengraph fields // url that lists the opengraph fields

View file

@ -1,116 +0,0 @@
use serde::{Deserialize, Serialize};
use std::{
borrow::Borrow,
ops::{Deref, DerefMut},
};
#[cfg(feature = "full")]
use ts_rs::TS;
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize, Serialize, Default)]
#[serde(transparent)]
pub struct Sensitive<T>(T);
impl<T> Sensitive<T> {
pub fn new(item: T) -> Self {
Sensitive(item)
}
pub fn into_inner(self) -> T {
self.0
}
}
impl<T> std::fmt::Debug for Sensitive<T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Sensitive").finish()
}
}
impl<T> AsRef<T> for Sensitive<T> {
fn as_ref(&self) -> &T {
&self.0
}
}
impl AsRef<str> for Sensitive<String> {
fn as_ref(&self) -> &str {
&self.0
}
}
impl AsRef<[u8]> for Sensitive<String> {
fn as_ref(&self) -> &[u8] {
self.0.as_ref()
}
}
impl AsRef<[u8]> for Sensitive<Vec<u8>> {
fn as_ref(&self) -> &[u8] {
self.0.as_ref()
}
}
impl<T> AsMut<T> for Sensitive<T> {
fn as_mut(&mut self) -> &mut T {
&mut self.0
}
}
impl AsMut<str> for Sensitive<String> {
fn as_mut(&mut self) -> &mut str {
&mut self.0
}
}
impl Deref for Sensitive<String> {
type Target = str;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl DerefMut for Sensitive<String> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
impl<T> From<T> for Sensitive<T> {
fn from(t: T) -> Self {
Sensitive(t)
}
}
impl From<&str> for Sensitive<String> {
fn from(s: &str) -> Self {
Sensitive(s.into())
}
}
impl<T> Borrow<T> for Sensitive<T> {
fn borrow(&self) -> &T {
&self.0
}
}
impl Borrow<str> for Sensitive<String> {
fn borrow(&self) -> &str {
&self.0
}
}
#[cfg(feature = "full")]
impl TS for Sensitive<String> {
fn name() -> String {
"string".to_string()
}
fn name_with_type_args(_args: Vec<String>) -> String {
"string".to_string()
}
fn dependencies() -> Vec<ts_rs::Dependency> {
Vec::new()
}
fn transparent() -> bool {
true
}
}

View file

@ -375,7 +375,8 @@ impl From<FederationQueueState> for ReadableFederationState {
pub struct InstanceWithFederationState { pub struct InstanceWithFederationState {
#[serde(flatten)] #[serde(flatten)]
pub instance: Instance, pub instance: Instance,
/// if federation to this instance is or was active, show state of outgoing federation to this instance /// if federation to this instance is or was active, show state of outgoing federation to this
/// instance
pub federation_state: Option<ReadableFederationState>, pub federation_state: Option<ReadableFederationState>,
} }

View file

@ -6,13 +6,14 @@ use crate::{
use chrono::{DateTime, Days, Local, TimeZone, Utc}; use chrono::{DateTime, Days, Local, TimeZone, Utc};
use enum_map::{enum_map, EnumMap}; use enum_map::{enum_map, EnumMap};
use lemmy_db_schema::{ use lemmy_db_schema::{
aggregates::structs::{PersonPostAggregates, PersonPostAggregatesForm},
newtypes::{CommunityId, DbUrl, InstanceId, PersonId, PostId}, newtypes::{CommunityId, DbUrl, InstanceId, PersonId, PostId},
source::{ source::{
comment::{Comment, CommentUpdateForm}, comment::{Comment, CommentUpdateForm},
community::{Community, CommunityModerator, CommunityUpdateForm}, community::{Community, CommunityModerator, CommunityUpdateForm},
community_block::CommunityBlock, community_block::CommunityBlock,
email_verification::{EmailVerification, EmailVerificationForm}, email_verification::{EmailVerification, EmailVerificationForm},
images::{LocalImage, RemoteImage}, images::RemoteImage,
instance::Instance, instance::Instance,
instance_block::InstanceBlock, instance_block::InstanceBlock,
local_site::LocalSite, local_site::LocalSite,
@ -27,7 +28,10 @@ use lemmy_db_schema::{
traits::Crud, traits::Crud,
utils::DbPool, utils::DbPool,
}; };
use lemmy_db_views::{comment_view::CommentQuery, structs::LocalUserView}; use lemmy_db_views::{
comment_view::CommentQuery,
structs::{LocalImageView, LocalUserView},
};
use lemmy_db_views_actor::structs::{ use lemmy_db_views_actor::structs::{
CommunityModeratorView, CommunityModeratorView,
CommunityPersonBanView, CommunityPersonBanView,
@ -136,13 +140,7 @@ pub fn is_top_mod(
} }
} }
#[tracing::instrument(skip_all)] /// Marks a post as read for a given person.
pub async fn get_post(post_id: PostId, pool: &mut DbPool<'_>) -> LemmyResult<Post> {
Post::read(pool, post_id)
.await
.with_lemmy_type(LemmyErrorType::CouldntFindPost)
}
#[tracing::instrument(skip_all)] #[tracing::instrument(skip_all)]
pub async fn mark_post_as_read( pub async fn mark_post_as_read(
person_id: PersonId, person_id: PersonId,
@ -155,6 +153,28 @@ pub async fn mark_post_as_read(
Ok(()) Ok(())
} }
/// Updates the read comment count for a post. Usually done when reading or creating a new comment.
#[tracing::instrument(skip_all)]
pub async fn update_read_comments(
person_id: PersonId,
post_id: PostId,
read_comments: i64,
pool: &mut DbPool<'_>,
) -> LemmyResult<()> {
let person_post_agg_form = PersonPostAggregatesForm {
person_id,
post_id,
read_comments,
..PersonPostAggregatesForm::default()
};
PersonPostAggregates::upsert(pool, &person_post_agg_form)
.await
.with_lemmy_type(LemmyErrorType::CouldntFindPost)?;
Ok(())
}
pub fn check_user_valid(person: &Person) -> LemmyResult<()> { pub fn check_user_valid(person: &Person) -> LemmyResult<()> {
// Check for a site ban // Check for a site ban
if person.banned { if person.banned {
@ -188,8 +208,8 @@ async fn check_community_deleted_removed(
pool: &mut DbPool<'_>, pool: &mut DbPool<'_>,
) -> LemmyResult<()> { ) -> LemmyResult<()> {
let community = Community::read(pool, community_id) let community = Community::read(pool, community_id)
.await .await?
.with_lemmy_type(LemmyErrorType::CouldntFindCommunity)?; .ok_or(LemmyErrorType::CouldntFindCommunity)?;
if community.deleted || community.removed { if community.deleted || community.removed {
Err(LemmyErrorType::Deleted)? Err(LemmyErrorType::Deleted)?
} }
@ -353,7 +373,8 @@ pub async fn build_federated_instances(
federation_state: federation_state.map(std::convert::Into::into), federation_state: federation_state.map(std::convert::Into::into),
}; };
if is_blocked { if is_blocked {
// blocked instances will only have an entry here if they had been federated with in the past. // blocked instances will only have an entry here if they had been federated with in the
// past.
blocked.push(i); blocked.push(i);
} else if is_allowed { } else if is_allowed {
allowed.push(i.clone()); allowed.push(i.clone());
@ -437,7 +458,7 @@ pub async fn send_password_reset_email(
// Insert the row after successful send, to avoid using daily reset limit while // Insert the row after successful send, to avoid using daily reset limit while
// email sending is broken. // email sending is broken.
let local_user_id = user.local_user.id; let local_user_id = user.local_user.id;
PasswordResetRequest::create_token(pool, local_user_id, token.clone()).await?; PasswordResetRequest::create(pool, local_user_id, token.clone()).await?;
Ok(()) Ok(())
} }
@ -533,25 +554,8 @@ pub async fn get_url_blocklist(context: &LemmyContext) -> LemmyResult<RegexSet>
.try_get_with::<_, LemmyError>((), async { .try_get_with::<_, LemmyError>((), async {
let urls = LocalSiteUrlBlocklist::get_all(&mut context.pool()).await?; let urls = LocalSiteUrlBlocklist::get_all(&mut context.pool()).await?;
let regexes = urls.iter().map(|url| { // The urls are already validated on saving, so just escape them.
let url = &url.url; let regexes = urls.iter().map(|url| escape(&url.url));
let parsed = Url::parse(url).expect("Coundln't parse URL.");
if url.ends_with('/') {
format!(
"({}://)?{}{}?",
parsed.scheme(),
escape(parsed.domain().expect("No domain.")),
escape(parsed.path())
)
} else {
format!(
"({}://)?{}{}",
parsed.scheme(),
escape(parsed.domain().expect("No domain.")),
escape(parsed.path())
)
}
});
let set = RegexSet::new(regexes)?; let set = RegexSet::new(regexes)?;
Ok(set) Ok(set)
@ -660,15 +664,20 @@ pub async fn purge_image_posts_for_person(
/// Delete a local_user's images /// Delete a local_user's images
async fn delete_local_user_images(person_id: PersonId, context: &LemmyContext) -> LemmyResult<()> { async fn delete_local_user_images(person_id: PersonId, context: &LemmyContext) -> LemmyResult<()> {
if let Ok(local_user) = LocalUserView::read_person(&mut context.pool(), person_id).await { if let Ok(Some(local_user)) = LocalUserView::read_person(&mut context.pool(), person_id).await {
let pictrs_uploads = let pictrs_uploads =
LocalImage::get_all_by_local_user_id(&mut context.pool(), local_user.local_user.id).await?; LocalImageView::get_all_by_local_user_id(&mut context.pool(), local_user.local_user.id)
.await?;
// Delete their images // Delete their images
for upload in pictrs_uploads { for upload in pictrs_uploads {
delete_image_from_pictrs(&upload.pictrs_alias, &upload.pictrs_delete_token, context) delete_image_from_pictrs(
.await &upload.local_image.pictrs_alias,
.ok(); &upload.local_image.pictrs_delete_token,
context,
)
.await
.ok();
} }
} }
Ok(()) Ok(())
@ -700,7 +709,9 @@ pub async fn remove_user_data(
) -> LemmyResult<()> { ) -> LemmyResult<()> {
let pool = &mut context.pool(); let pool = &mut context.pool();
// Purge user images // Purge user images
let person = Person::read(pool, banned_person_id).await?; let person = Person::read(pool, banned_person_id)
.await?
.ok_or(LemmyErrorType::CouldntFindPerson)?;
if let Some(avatar) = person.avatar { if let Some(avatar) = person.avatar {
purge_image_from_pictrs(&avatar, context).await.ok(); purge_image_from_pictrs(&avatar, context).await.ok();
} }
@ -813,7 +824,9 @@ pub async fn remove_user_data_in_community(
pub async fn purge_user_account(person_id: PersonId, context: &LemmyContext) -> LemmyResult<()> { pub async fn purge_user_account(person_id: PersonId, context: &LemmyContext) -> LemmyResult<()> {
let pool = &mut context.pool(); let pool = &mut context.pool();
let person = Person::read(pool, person_id).await?; let person = Person::read(pool, person_id)
.await?
.ok_or(LemmyErrorType::CouldntFindPerson)?;
// Delete their local images, if they're a local user // Delete their local images, if they're a local user
delete_local_user_images(person_id, context).await.ok(); delete_local_user_images(person_id, context).await.ok();
@ -959,8 +972,8 @@ pub async fn process_markdown_opt(
/// A wrapper for `proxy_image_link` for use in tests. /// A wrapper for `proxy_image_link` for use in tests.
/// ///
/// The parameter `force_image_proxy` is the config value of `pictrs.image_proxy`. Its necessary to pass /// The parameter `force_image_proxy` is the config value of `pictrs.image_proxy`. Its necessary to
/// as separate parameter so it can be changed in tests. /// pass as separate parameter so it can be changed in tests.
async fn proxy_image_link_internal( async fn proxy_image_link_internal(
link: Url, link: Url,
image_mode: PictrsImageMode, image_mode: PictrsImageMode,
@ -970,13 +983,10 @@ async fn proxy_image_link_internal(
if link.domain() == Some(&context.settings().hostname) { if link.domain() == Some(&context.settings().hostname) {
Ok(link.into()) Ok(link.into())
} else if image_mode == PictrsImageMode::ProxyAllImages { } else if image_mode == PictrsImageMode::ProxyAllImages {
let proxied = format!( let proxied = build_proxied_image_url(&link, &context.settings().get_protocol_and_hostname())?;
"{}/api/v3/image_proxy?url={}",
context.settings().get_protocol_and_hostname(),
encode(link.as_str())
);
RemoteImage::create(&mut context.pool(), vec![link]).await?; RemoteImage::create(&mut context.pool(), vec![link]).await?;
Ok(Url::parse(&proxied)?.into()) Ok(proxied.into())
} else { } else {
Ok(link.into()) Ok(link.into())
} }
@ -994,26 +1004,25 @@ pub(crate) async fn proxy_image_link(link: Url, context: &LemmyContext) -> Lemmy
} }
pub async fn proxy_image_link_opt_api( pub async fn proxy_image_link_opt_api(
link: &Option<String>, link: Option<Option<DbUrl>>,
context: &LemmyContext, context: &LemmyContext,
) -> LemmyResult<Option<Option<DbUrl>>> { ) -> LemmyResult<Option<Option<DbUrl>>> {
proxy_image_link_api(link, context).await.map(Some) if let Some(Some(link)) = link {
proxy_image_link(link.into(), context)
.await
.map(Some)
.map(Some)
} else {
Ok(link)
}
} }
pub async fn proxy_image_link_api( pub async fn proxy_image_link_api(
link: &Option<String>, link: Option<DbUrl>,
context: &LemmyContext, context: &LemmyContext,
) -> LemmyResult<Option<DbUrl>> { ) -> LemmyResult<Option<DbUrl>> {
let link: Option<DbUrl> = match link.as_ref().map(String::as_str) { if let Some(link) = link {
// An empty string is an erase proxy_image_link(link.into(), context).await.map(Some)
Some("") => None,
Some(str_url) => Url::parse(str_url)
.map(|u| Some(u.into()))
.with_lemmy_type(LemmyErrorType::InvalidUrl)?,
None => None,
};
if let Some(l) = link {
proxy_image_link(l.into(), context).await.map(Some)
} else { } else {
Ok(link) Ok(link)
} }
@ -1030,6 +1039,17 @@ pub async fn proxy_image_link_opt_apub(
} }
} }
fn build_proxied_image_url(
link: &Url,
protocol_and_hostname: &str,
) -> Result<Url, url::ParseError> {
Url::parse(&format!(
"{}/api/v3/image_proxy?url={}",
protocol_and_hostname,
encode(link.as_str())
))
}
#[cfg(test)] #[cfg(test)]
#[allow(clippy::unwrap_used)] #[allow(clippy::unwrap_used)]
#[allow(clippy::indexing_slicing)] #[allow(clippy::indexing_slicing)]
@ -1109,29 +1129,4 @@ mod tests {
.is_ok() .is_ok()
); );
} }
#[tokio::test]
#[serial]
async fn test_diesel_option_overwrite_to_url() {
let context = LemmyContext::init_test_context().await;
assert!(matches!(
proxy_image_link_api(&None, &context).await,
Ok(None)
));
assert!(matches!(
proxy_image_link_opt_api(&Some(String::new()), &context).await,
Ok(Some(None))
));
assert!(
proxy_image_link_opt_api(&Some("invalid_url".to_string()), &context)
.await
.is_err()
);
let example_url = "https://lemmy-alpha/image.png";
assert!(matches!(
proxy_image_link_opt_api(&Some(example_url.to_string()), &context).await,
Ok(Some(Some(url))) if url == Url::parse(example_url).unwrap().into()
));
}
} }

View file

@ -9,11 +9,11 @@ use lemmy_api_common::{
check_community_user_action, check_community_user_action,
check_post_deleted_or_removed, check_post_deleted_or_removed,
generate_local_apub_endpoint, generate_local_apub_endpoint,
get_post,
get_url_blocklist, get_url_blocklist,
is_mod_or_admin, is_mod_or_admin,
local_site_to_slur_regex, local_site_to_slur_regex,
process_markdown, process_markdown,
update_read_comments,
EndpointType, EndpointType,
}, },
}; };
@ -28,7 +28,7 @@ use lemmy_db_schema::{
}, },
traits::{Crud, Likeable}, traits::{Crud, Likeable},
}; };
use lemmy_db_views::structs::LocalUserView; use lemmy_db_views::structs::{LocalUserView, PostView};
use lemmy_utils::{ use lemmy_utils::{
error::{LemmyErrorExt, LemmyErrorType, LemmyResult}, error::{LemmyErrorExt, LemmyErrorType, LemmyResult},
utils::{mention::scrape_text_for_mentions, validation::is_valid_body_field}, utils::{mention::scrape_text_for_mentions, validation::is_valid_body_field},
@ -47,12 +47,23 @@ pub async fn create_comment(
let slur_regex = local_site_to_slur_regex(&local_site); let slur_regex = local_site_to_slur_regex(&local_site);
let url_blocklist = get_url_blocklist(&context).await?; let url_blocklist = get_url_blocklist(&context).await?;
let content = process_markdown(&data.content, &slur_regex, &url_blocklist, &context).await?; let content = process_markdown(&data.content, &slur_regex, &url_blocklist, &context).await?;
is_valid_body_field(&Some(content.clone()), false)?; is_valid_body_field(&content, false)?;
// Check for a community ban // Check for a community ban
let post_id = data.post_id; let post_id = data.post_id;
let post = get_post(post_id, &mut context.pool()).await?;
let community_id = post.community_id; // Read the full post view in order to get the comments count.
let post_view = PostView::read(
&mut context.pool(),
post_id,
Some(local_user_view.person.id),
true,
)
.await?
.ok_or(LemmyErrorType::CouldntFindPost)?;
let post = post_view.post;
let community_id = post_view.community.id;
check_community_user_action(&local_user_view.person, community_id, &mut context.pool()).await?; check_community_user_action(&local_user_view.person, community_id, &mut context.pool()).await?;
check_post_deleted_or_removed(&post)?; check_post_deleted_or_removed(&post)?;
@ -70,7 +81,8 @@ pub async fn create_comment(
Comment::read(&mut context.pool(), parent_id).await.ok() Comment::read(&mut context.pool(), parent_id).await.ok()
} else { } else {
None None
}; }
.flatten();
// If there's a parent_id, check to make sure that comment is in that post // If there's a parent_id, check to make sure that comment is in that post
// Strange issue where sometimes the post ID of the parent comment is incorrect // Strange issue where sometimes the post ID of the parent comment is incorrect
@ -163,6 +175,15 @@ pub async fn create_comment(
) )
.await?; .await?;
// Update the read comments, so your own new comment doesn't appear as a +1 unread
update_read_comments(
local_user_view.person.id,
post_id,
post_view.counts.comments + 1,
&mut context.pool(),
)
.await?;
// If we're responding to a comment where we're the recipient, // If we're responding to a comment where we're the recipient,
// (ie we're the grandparent, or the recipient of the parent comment_reply), // (ie we're the grandparent, or the recipient of the parent comment_reply),
// then mark the parent as read. // then mark the parent as read.
@ -172,7 +193,7 @@ pub async fn create_comment(
let parent_id = parent.id; let parent_id = parent.id;
let comment_reply = let comment_reply =
CommentReply::read_by_comment_and_person(&mut context.pool(), parent_id, person_id).await; CommentReply::read_by_comment_and_person(&mut context.pool(), parent_id, person_id).await;
if let Ok(reply) = comment_reply { if let Ok(Some(reply)) = comment_reply {
CommentReply::update( CommentReply::update(
&mut context.pool(), &mut context.pool(),
reply.id, reply.id,
@ -185,7 +206,7 @@ pub async fn create_comment(
// If the parent has PersonMentions mark them as read too // If the parent has PersonMentions mark them as read too
let person_mention = let person_mention =
PersonMention::read_by_comment_and_person(&mut context.pool(), parent_id, person_id).await; PersonMention::read_by_comment_and_person(&mut context.pool(), parent_id, person_id).await;
if let Ok(mention) = person_mention { if let Ok(Some(mention)) = person_mention {
PersonMention::update( PersonMention::update(
&mut context.pool(), &mut context.pool(),
mention.id, mention.id,

View file

@ -21,7 +21,9 @@ pub async fn delete_comment(
local_user_view: LocalUserView, local_user_view: LocalUserView,
) -> LemmyResult<Json<CommentResponse>> { ) -> LemmyResult<Json<CommentResponse>> {
let comment_id = data.comment_id; let comment_id = data.comment_id;
let orig_comment = CommentView::read(&mut context.pool(), comment_id, None).await?; let orig_comment = CommentView::read(&mut context.pool(), comment_id, None)
.await?
.ok_or(LemmyErrorType::CouldntFindComment)?;
// Dont delete it if its already been deleted. // Dont delete it if its already been deleted.
if orig_comment.comment.deleted == data.deleted { if orig_comment.comment.deleted == data.deleted {

View file

@ -25,7 +25,9 @@ pub async fn remove_comment(
local_user_view: LocalUserView, local_user_view: LocalUserView,
) -> LemmyResult<Json<CommentResponse>> { ) -> LemmyResult<Json<CommentResponse>> {
let comment_id = data.comment_id; let comment_id = data.comment_id;
let orig_comment = CommentView::read(&mut context.pool(), comment_id, None).await?; let orig_comment = CommentView::read(&mut context.pool(), comment_id, None)
.await?
.ok_or(LemmyErrorType::CouldntFindComment)?;
check_community_mod_action( check_community_mod_action(
&local_user_view.person, &local_user_view.person,
@ -35,6 +37,12 @@ pub async fn remove_comment(
) )
.await?; .await?;
// Don't allow removing or restoring comment which was deleted by user, as it would reveal
// the comment text in mod log.
if orig_comment.comment.deleted {
return Err(LemmyErrorType::CouldntUpdateComment.into());
}
// Do the remove // Do the remove
let removed = data.removed; let removed = data.removed;
let updated_comment = Comment::update( let updated_comment = Comment::update(

View file

@ -36,7 +36,9 @@ pub async fn update_comment(
let local_site = LocalSite::read(&mut context.pool()).await?; let local_site = LocalSite::read(&mut context.pool()).await?;
let comment_id = data.comment_id; let comment_id = data.comment_id;
let orig_comment = CommentView::read(&mut context.pool(), comment_id, None).await?; let orig_comment = CommentView::read(&mut context.pool(), comment_id, None)
.await?
.ok_or(LemmyErrorType::CouldntFindComment)?;
check_community_user_action( check_community_user_action(
&local_user_view.person, &local_user_view.person,
@ -61,7 +63,9 @@ pub async fn update_comment(
let slur_regex = local_site_to_slur_regex(&local_site); let slur_regex = local_site_to_slur_regex(&local_site);
let url_blocklist = get_url_blocklist(&context).await?; let url_blocklist = get_url_blocklist(&context).await?;
let content = process_markdown_opt(&data.content, &slur_regex, &url_blocklist, &context).await?; let content = process_markdown_opt(&data.content, &slur_regex, &url_blocklist, &context).await?;
is_valid_body_field(&content, false)?; if let Some(content) = &content {
is_valid_body_field(content, false)?;
}
let comment_id = data.comment_id; let comment_id = data.comment_id;
let form = CommentUpdateForm { let form = CommentUpdateForm {

View file

@ -30,6 +30,7 @@ use lemmy_db_schema::{
}, },
}, },
traits::{ApubActor, Crud, Followable, Joinable}, traits::{ApubActor, Crud, Followable, Joinable},
utils::diesel_url_create,
}; };
use lemmy_db_views::structs::{LocalUserView, SiteView}; use lemmy_db_views::structs::{LocalUserView, SiteView};
use lemmy_utils::{ use lemmy_utils::{
@ -46,7 +47,9 @@ pub async fn create_community(
context: Data<LemmyContext>, context: Data<LemmyContext>,
local_user_view: LocalUserView, local_user_view: LocalUserView,
) -> LemmyResult<Json<CommunityResponse>> { ) -> LemmyResult<Json<CommunityResponse>> {
let site_view = SiteView::read_local(&mut context.pool()).await?; let site_view = SiteView::read_local(&mut context.pool())
.await?
.ok_or(LemmyErrorType::LocalSiteNotSetup)?;
let local_site = site_view.local_site; let local_site = site_view.local_site;
if local_site.community_creation_admin_only && is_admin(&local_user_view).is_err() { if local_site.community_creation_admin_only && is_admin(&local_user_view).is_err() {
@ -59,11 +62,18 @@ pub async fn create_community(
check_slurs(&data.title, &slur_regex)?; check_slurs(&data.title, &slur_regex)?;
let description = let description =
process_markdown_opt(&data.description, &slur_regex, &url_blocklist, &context).await?; process_markdown_opt(&data.description, &slur_regex, &url_blocklist, &context).await?;
let icon = proxy_image_link_api(&data.icon, &context).await?;
let banner = proxy_image_link_api(&data.banner, &context).await?; let icon = diesel_url_create(data.icon.as_deref())?;
let icon = proxy_image_link_api(icon, &context).await?;
let banner = diesel_url_create(data.banner.as_deref())?;
let banner = proxy_image_link_api(banner, &context).await?;
is_valid_actor_name(&data.name, local_site.actor_name_max_length as usize)?; is_valid_actor_name(&data.name, local_site.actor_name_max_length as usize)?;
is_valid_body_field(&data.description, false)?;
if let Some(desc) = &data.description {
is_valid_body_field(desc, false)?;
}
// Double check for duplicate community actor_ids // Double check for duplicate community actor_ids
let community_actor_id = generate_local_apub_endpoint( let community_actor_id = generate_local_apub_endpoint(

View file

@ -6,7 +6,7 @@ use lemmy_api_common::{
}; };
use lemmy_db_views::structs::{LocalUserView, SiteView}; use lemmy_db_views::structs::{LocalUserView, SiteView};
use lemmy_db_views_actor::community_view::CommunityQuery; use lemmy_db_views_actor::community_view::CommunityQuery;
use lemmy_utils::error::LemmyResult; use lemmy_utils::{error::LemmyResult, LemmyErrorType};
#[tracing::instrument(skip(context))] #[tracing::instrument(skip(context))]
pub async fn list_communities( pub async fn list_communities(
@ -14,7 +14,9 @@ pub async fn list_communities(
context: Data<LemmyContext>, context: Data<LemmyContext>,
local_user_view: Option<LocalUserView>, local_user_view: Option<LocalUserView>,
) -> LemmyResult<Json<ListCommunitiesResponse>> { ) -> LemmyResult<Json<ListCommunitiesResponse>> {
let local_site = SiteView::read_local(&mut context.pool()).await?; let local_site = SiteView::read_local(&mut context.pool())
.await?
.ok_or(LemmyErrorType::LocalSiteNotSetup)?;
let is_admin = local_user_view let is_admin = local_user_view
.as_ref() .as_ref()
.map(|luv| is_admin(luv).is_ok()) .map(|luv| is_admin(luv).is_ok())

View file

@ -21,7 +21,7 @@ use lemmy_db_schema::{
local_site::LocalSite, local_site::LocalSite,
}, },
traits::Crud, traits::Crud,
utils::{diesel_option_overwrite, naive_now}, utils::{diesel_string_update, diesel_url_update, naive_now},
}; };
use lemmy_db_views::structs::LocalUserView; use lemmy_db_views::structs::LocalUserView;
use lemmy_utils::{ use lemmy_utils::{
@ -40,16 +40,28 @@ pub async fn update_community(
let slur_regex = local_site_to_slur_regex(&local_site); let slur_regex = local_site_to_slur_regex(&local_site);
let url_blocklist = get_url_blocklist(&context).await?; let url_blocklist = get_url_blocklist(&context).await?;
check_slurs_opt(&data.title, &slur_regex)?; check_slurs_opt(&data.title, &slur_regex)?;
let description =
process_markdown_opt(&data.description, &slur_regex, &url_blocklist, &context).await?;
is_valid_body_field(&data.description, false)?;
let old_community = Community::read(&mut context.pool(), data.community_id).await?;
replace_image(&data.icon, &old_community.icon, &context).await?;
replace_image(&data.banner, &old_community.banner, &context).await?;
let description = diesel_option_overwrite(description); let description = diesel_string_update(
let icon = proxy_image_link_opt_api(&data.icon, &context).await?; process_markdown_opt(&data.description, &slur_regex, &url_blocklist, &context)
let banner = proxy_image_link_opt_api(&data.banner, &context).await?; .await?
.as_deref(),
);
if let Some(Some(desc)) = &description {
is_valid_body_field(desc, false)?;
}
let old_community = Community::read(&mut context.pool(), data.community_id)
.await?
.ok_or(LemmyErrorType::CouldntFindCommunity)?;
let icon = diesel_url_update(data.icon.as_deref())?;
replace_image(&icon, &old_community.icon, &context).await?;
let icon = proxy_image_link_opt_api(icon, &context).await?;
let banner = diesel_url_update(data.banner.as_deref())?;
replace_image(&banner, &old_community.banner, &context).await?;
let banner = proxy_image_link_opt_api(banner, &context).await?;
// Verify its a mod (only mods can edit it) // Verify its a mod (only mods can edit it)
check_community_mod_action( check_community_mod_action(

View file

@ -14,7 +14,6 @@ use lemmy_api_common::{
local_site_to_slur_regex, local_site_to_slur_regex,
mark_post_as_read, mark_post_as_read,
process_markdown_opt, process_markdown_opt,
proxy_image_link_opt_apub,
EndpointType, EndpointType,
}, },
}; };
@ -27,6 +26,7 @@ use lemmy_db_schema::{
post::{Post, PostInsertForm, PostLike, PostLikeForm, PostUpdateForm}, post::{Post, PostInsertForm, PostLike, PostLikeForm, PostUpdateForm},
}, },
traits::{Crud, Likeable}, traits::{Crud, Likeable},
utils::diesel_url_create,
CommunityVisibility, CommunityVisibility,
}; };
use lemmy_db_views::structs::LocalUserView; use lemmy_db_views::structs::LocalUserView;
@ -38,7 +38,6 @@ use lemmy_utils::{
slurs::check_slurs, slurs::check_slurs,
validation::{ validation::{
check_url_scheme, check_url_scheme,
clean_url_params,
is_url_blocked, is_url_blocked,
is_valid_alt_text_field, is_valid_alt_text_field,
is_valid_body_field, is_valid_body_field,
@ -65,17 +64,27 @@ pub async fn create_post(
let url_blocklist = get_url_blocklist(&context).await?; let url_blocklist = get_url_blocklist(&context).await?;
let body = process_markdown_opt(&data.body, &slur_regex, &url_blocklist, &context).await?; let body = process_markdown_opt(&data.body, &slur_regex, &url_blocklist, &context).await?;
let data_url = data.url.as_ref(); let url = diesel_url_create(data.url.as_deref())?;
let url = data_url.map(clean_url_params); // TODO no good way to handle a "clear" let custom_thumbnail = diesel_url_create(data.custom_thumbnail.as_deref())?;
let custom_thumbnail = data.custom_thumbnail.as_ref().map(clean_url_params);
is_valid_post_title(&data.name)?; is_valid_post_title(&data.name)?;
is_valid_body_field(&body, true)?;
is_valid_alt_text_field(&data.alt_text)?; if let Some(url) = &url {
is_url_blocked(&url, &url_blocklist)?; is_url_blocked(url, &url_blocklist)?;
check_url_scheme(&url)?; check_url_scheme(url)?;
check_url_scheme(&custom_thumbnail)?; }
let url = proxy_image_link_opt_apub(url, &context).await?;
if let Some(custom_thumbnail) = &custom_thumbnail {
check_url_scheme(custom_thumbnail)?;
}
if let Some(alt_text) = &data.alt_text {
is_valid_alt_text_field(alt_text)?;
}
if let Some(body) = &body {
is_valid_body_field(body, true)?;
}
check_community_user_action( check_community_user_action(
&local_user_view.person, &local_user_view.person,
@ -85,7 +94,9 @@ pub async fn create_post(
.await?; .await?;
let community_id = data.community_id; let community_id = data.community_id;
let community = Community::read(&mut context.pool(), community_id).await?; let community = Community::read(&mut context.pool(), community_id)
.await?
.ok_or(LemmyErrorType::CouldntFindCommunity)?;
if community.posting_restricted_to_mods { if community.posting_restricted_to_mods {
let community_id = data.community_id; let community_id = data.community_id;
let is_mod = CommunityModeratorView::is_community_moderator( let is_mod = CommunityModeratorView::is_community_moderator(
@ -123,7 +134,7 @@ pub async fn create_post(
let post_form = PostInsertForm::builder() let post_form = PostInsertForm::builder()
.name(data.name.trim().to_string()) .name(data.name.trim().to_string())
.url(url) .url(url.map(Into::into))
.body(body) .body(body)
.alt_text(data.alt_text.clone()) .alt_text(data.alt_text.clone())
.community_id(data.community_id) .community_id(data.community_id)
@ -156,11 +167,12 @@ pub async fn create_post(
generate_post_link_metadata( generate_post_link_metadata(
updated_post.clone(), updated_post.clone(),
custom_thumbnail, custom_thumbnail.map(Into::into),
|post| Some(SendActivityData::CreatePost(post)), |post| Some(SendActivityData::CreatePost(post)),
Some(local_site), Some(local_site),
context.reset_request_count(), context.reset_request_count(),
); )
.await?;
// They like their own post by default // They like their own post by default
let person_id = local_user_view.person.id; let person_id = local_user_view.person.id;
@ -175,7 +187,6 @@ pub async fn create_post(
.await .await
.with_lemmy_type(LemmyErrorType::CouldntLikePost)?; .with_lemmy_type(LemmyErrorType::CouldntLikePost)?;
// Mark the post as read
mark_post_as_read(person_id, post_id, &mut context.pool()).await?; mark_post_as_read(person_id, post_id, &mut context.pool()).await?;
if let Some(url) = updated_post.url.clone() { if let Some(url) = updated_post.url.clone() {

View file

@ -21,7 +21,9 @@ pub async fn delete_post(
local_user_view: LocalUserView, local_user_view: LocalUserView,
) -> LemmyResult<Json<PostResponse>> { ) -> LemmyResult<Json<PostResponse>> {
let post_id = data.post_id; let post_id = data.post_id;
let orig_post = Post::read(&mut context.pool(), post_id).await?; let orig_post = Post::read(&mut context.pool(), post_id)
.await?
.ok_or(LemmyErrorType::CouldntFindPost)?;
// Dont delete it if its already been deleted. // Dont delete it if its already been deleted.
if orig_post.deleted == data.deleted { if orig_post.deleted == data.deleted {

View file

@ -2,10 +2,9 @@ use actix_web::web::{Data, Json, Query};
use lemmy_api_common::{ use lemmy_api_common::{
context::LemmyContext, context::LemmyContext,
post::{GetPost, GetPostResponse}, post::{GetPost, GetPostResponse},
utils::{check_private_instance, is_mod_or_admin_opt, mark_post_as_read}, utils::{check_private_instance, is_mod_or_admin_opt, mark_post_as_read, update_read_comments},
}; };
use lemmy_db_schema::{ use lemmy_db_schema::{
aggregates::structs::{PersonPostAggregates, PersonPostAggregatesForm},
source::{comment::Comment, post::Post}, source::{comment::Comment, post::Post},
traits::Crud, traits::Crud,
}; };
@ -14,7 +13,7 @@ use lemmy_db_views::{
structs::{LocalUserView, PostView, SiteView}, structs::{LocalUserView, PostView, SiteView},
}; };
use lemmy_db_views_actor::structs::{CommunityModeratorView, CommunityView}; use lemmy_db_views_actor::structs::{CommunityModeratorView, CommunityView};
use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult}; use lemmy_utils::error::{LemmyErrorType, LemmyResult};
#[tracing::instrument(skip(context))] #[tracing::instrument(skip(context))]
pub async fn get_post( pub async fn get_post(
@ -22,7 +21,9 @@ pub async fn get_post(
context: Data<LemmyContext>, context: Data<LemmyContext>,
local_user_view: Option<LocalUserView>, local_user_view: Option<LocalUserView>,
) -> LemmyResult<Json<GetPostResponse>> { ) -> LemmyResult<Json<GetPostResponse>> {
let local_site = SiteView::read_local(&mut context.pool()).await?; let local_site = SiteView::read_local(&mut context.pool())
.await?
.ok_or(LemmyErrorType::LocalSiteNotSetup)?;
check_private_instance(&local_user_view, &local_site.local_site)?; check_private_instance(&local_user_view, &local_site.local_site)?;
@ -33,15 +34,19 @@ pub async fn get_post(
id id
} else if let Some(comment_id) = data.comment_id { } else if let Some(comment_id) = data.comment_id {
Comment::read(&mut context.pool(), comment_id) Comment::read(&mut context.pool(), comment_id)
.await .await?
.with_lemmy_type(LemmyErrorType::CouldntFindPost)? .ok_or(LemmyErrorType::CouldntFindComment)?
.post_id .post_id
} else { } else {
Err(LemmyErrorType::CouldntFindPost)? Err(LemmyErrorType::CouldntFindPost)?
}; };
// Check to see if the person is a mod or admin, to show deleted / removed // Check to see if the person is a mod or admin, to show deleted / removed
let community_id = Post::read(&mut context.pool(), post_id).await?.community_id; let community_id = Post::read(&mut context.pool(), post_id)
.await?
.ok_or(LemmyErrorType::CouldntFindPost)?
.community_id;
let is_mod_or_admin = is_mod_or_admin_opt( let is_mod_or_admin = is_mod_or_admin_opt(
&mut context.pool(), &mut context.pool(),
local_user_view.as_ref(), local_user_view.as_ref(),
@ -51,13 +56,20 @@ pub async fn get_post(
.is_ok(); .is_ok();
let post_view = PostView::read(&mut context.pool(), post_id, person_id, is_mod_or_admin) let post_view = PostView::read(&mut context.pool(), post_id, person_id, is_mod_or_admin)
.await .await?
.with_lemmy_type(LemmyErrorType::CouldntFindPost)?; .ok_or(LemmyErrorType::CouldntFindPost)?;
// Mark the post as read
let post_id = post_view.post.id; let post_id = post_view.post.id;
if let Some(person_id) = person_id { if let Some(person_id) = person_id {
mark_post_as_read(person_id, post_id, &mut context.pool()).await?; mark_post_as_read(person_id, post_id, &mut context.pool()).await?;
update_read_comments(
person_id,
post_id,
post_view.counts.comments,
&mut context.pool(),
)
.await?;
} }
// Necessary for the sidebar subscribed // Necessary for the sidebar subscribed
@ -67,23 +79,8 @@ pub async fn get_post(
person_id, person_id,
is_mod_or_admin, is_mod_or_admin,
) )
.await .await?
.with_lemmy_type(LemmyErrorType::CouldntFindCommunity)?; .ok_or(LemmyErrorType::CouldntFindCommunity)?;
// Insert into PersonPostAggregates
// to update the read_comments count
if let Some(person_id) = person_id {
let read_comments = post_view.counts.comments;
let person_post_agg_form = PersonPostAggregatesForm {
person_id,
post_id,
read_comments,
..PersonPostAggregatesForm::default()
};
PersonPostAggregates::upsert(&mut context.pool(), &person_post_agg_form)
.await
.with_lemmy_type(LemmyErrorType::CouldntFindPost)?;
}
let moderators = CommunityModeratorView::for_community(&mut context.pool(), community_id).await?; let moderators = CommunityModeratorView::for_community(&mut context.pool(), community_id).await?;

View file

@ -16,7 +16,7 @@ use lemmy_db_schema::{
traits::{Crud, Reportable}, traits::{Crud, Reportable},
}; };
use lemmy_db_views::structs::LocalUserView; use lemmy_db_views::structs::LocalUserView;
use lemmy_utils::error::LemmyResult; use lemmy_utils::{error::LemmyResult, LemmyErrorType};
#[tracing::instrument(skip(context))] #[tracing::instrument(skip(context))]
pub async fn remove_post( pub async fn remove_post(
@ -25,7 +25,9 @@ pub async fn remove_post(
local_user_view: LocalUserView, local_user_view: LocalUserView,
) -> LemmyResult<Json<PostResponse>> { ) -> LemmyResult<Json<PostResponse>> {
let post_id = data.post_id; let post_id = data.post_id;
let orig_post = Post::read(&mut context.pool(), post_id).await?; let orig_post = Post::read(&mut context.pool(), post_id)
.await?
.ok_or(LemmyErrorType::CouldntFindPost)?;
check_community_mod_action( check_community_mod_action(
&local_user_view.person, &local_user_view.person,

View file

@ -11,7 +11,6 @@ use lemmy_api_common::{
get_url_blocklist, get_url_blocklist,
local_site_to_slur_regex, local_site_to_slur_regex,
process_markdown_opt, process_markdown_opt,
proxy_image_link_opt_apub,
}, },
}; };
use lemmy_db_schema::{ use lemmy_db_schema::{
@ -21,16 +20,15 @@ use lemmy_db_schema::{
post::{Post, PostUpdateForm}, post::{Post, PostUpdateForm},
}, },
traits::Crud, traits::Crud,
utils::{diesel_option_overwrite, naive_now}, utils::{diesel_string_update, diesel_url_update, naive_now},
}; };
use lemmy_db_views::structs::LocalUserView; use lemmy_db_views::structs::LocalUserView;
use lemmy_utils::{ use lemmy_utils::{
error::{LemmyErrorExt, LemmyErrorType, LemmyResult}, error::{LemmyErrorExt, LemmyErrorType, LemmyResult},
utils::{ utils::{
slurs::check_slurs_opt, slurs::check_slurs,
validation::{ validation::{
check_url_scheme, check_url_scheme,
clean_url_params,
is_url_blocked, is_url_blocked,
is_valid_alt_text_field, is_valid_alt_text_field,
is_valid_body_field, is_valid_body_field,
@ -48,29 +46,48 @@ pub async fn update_post(
) -> LemmyResult<Json<PostResponse>> { ) -> LemmyResult<Json<PostResponse>> {
let local_site = LocalSite::read(&mut context.pool()).await?; let local_site = LocalSite::read(&mut context.pool()).await?;
// TODO No good way to handle a clear. let url = diesel_url_update(data.url.as_deref())?;
// Issue link: https://github.com/LemmyNet/lemmy/issues/2287
let url = data.url.as_ref().map(clean_url_params); let custom_thumbnail = diesel_url_update(data.custom_thumbnail.as_deref())?;
let custom_thumbnail = data.custom_thumbnail.as_ref().map(clean_url_params);
let url_blocklist = get_url_blocklist(&context).await?; let url_blocklist = get_url_blocklist(&context).await?;
let slur_regex = local_site_to_slur_regex(&local_site); let slur_regex = local_site_to_slur_regex(&local_site);
check_slurs_opt(&data.name, &slur_regex)?;
let body = process_markdown_opt(&data.body, &slur_regex, &url_blocklist, &context).await?; let body = diesel_string_update(
process_markdown_opt(&data.body, &slur_regex, &url_blocklist, &context)
.await?
.as_deref(),
);
let alt_text = diesel_string_update(data.alt_text.as_deref());
if let Some(name) = &data.name { if let Some(name) = &data.name {
is_valid_post_title(name)?; is_valid_post_title(name)?;
check_slurs(name, &slur_regex)?;
} }
is_valid_body_field(&body, true)?; if let Some(Some(body)) = &body {
is_valid_alt_text_field(&data.alt_text)?; is_valid_body_field(body, true)?;
is_url_blocked(&url, &url_blocklist)?; }
check_url_scheme(&url)?;
check_url_scheme(&custom_thumbnail)?; if let Some(Some(alt_text)) = &alt_text {
is_valid_alt_text_field(alt_text)?;
}
if let Some(Some(url)) = &url {
is_url_blocked(url, &url_blocklist)?;
check_url_scheme(url)?;
}
if let Some(Some(custom_thumbnail)) = &custom_thumbnail {
check_url_scheme(custom_thumbnail)?;
}
let post_id = data.post_id; let post_id = data.post_id;
let orig_post = Post::read(&mut context.pool(), post_id).await?; let orig_post = Post::read(&mut context.pool(), post_id)
.await?
.ok_or(LemmyErrorType::CouldntFindPost)?;
check_community_user_action( check_community_user_action(
&local_user_view.person, &local_user_view.person,
@ -84,11 +101,6 @@ pub async fn update_post(
Err(LemmyErrorType::NoPostEditAllowed)? Err(LemmyErrorType::NoPostEditAllowed)?
} }
let url = match url {
Some(url) => Some(proxy_image_link_opt_apub(Some(url), &context).await?),
_ => Default::default(),
};
let language_id = data.language_id; let language_id = data.language_id;
CommunityLanguage::is_allowed_community_language( CommunityLanguage::is_allowed_community_language(
&mut context.pool(), &mut context.pool(),
@ -100,8 +112,8 @@ pub async fn update_post(
let post_form = PostUpdateForm { let post_form = PostUpdateForm {
name: data.name.clone(), name: data.name.clone(),
url, url,
body: diesel_option_overwrite(body), body,
alt_text: diesel_option_overwrite(data.alt_text.clone()), alt_text,
nsfw: data.nsfw, nsfw: data.nsfw,
language_id: data.language_id, language_id: data.language_id,
updated: Some(Some(naive_now())), updated: Some(Some(naive_now())),
@ -115,11 +127,12 @@ pub async fn update_post(
generate_post_link_metadata( generate_post_link_metadata(
updated_post.clone(), updated_post.clone(),
custom_thumbnail, custom_thumbnail.flatten().map(Into::into),
|post| Some(SendActivityData::UpdatePost(post)), |post| Some(SendActivityData::UpdatePost(post)),
Some(local_site), Some(local_site),
context.reset_request_count(), context.reset_request_count(),
); )
.await?;
build_post_response( build_post_response(
context.deref(), context.deref(),

View file

@ -39,7 +39,7 @@ pub async fn create_private_message(
let slur_regex = local_site_to_slur_regex(&local_site); let slur_regex = local_site_to_slur_regex(&local_site);
let url_blocklist = get_url_blocklist(&context).await?; let url_blocklist = get_url_blocklist(&context).await?;
let content = process_markdown(&data.content, &slur_regex, &url_blocklist, &context).await?; let content = process_markdown(&data.content, &slur_regex, &url_blocklist, &context).await?;
is_valid_body_field(&Some(content.clone()), false)?; is_valid_body_field(&content, false)?;
check_person_block( check_person_block(
local_user_view.person.id, local_user_view.person.id,
@ -76,12 +76,16 @@ pub async fn create_private_message(
.await .await
.with_lemmy_type(LemmyErrorType::CouldntCreatePrivateMessage)?; .with_lemmy_type(LemmyErrorType::CouldntCreatePrivateMessage)?;
let view = PrivateMessageView::read(&mut context.pool(), inserted_private_message.id).await?; let view = PrivateMessageView::read(&mut context.pool(), inserted_private_message.id)
.await?
.ok_or(LemmyErrorType::CouldntFindPrivateMessage)?;
// Send email to the local recipient, if one exists // Send email to the local recipient, if one exists
if view.recipient.local { if view.recipient.local {
let recipient_id = data.recipient_id; let recipient_id = data.recipient_id;
let local_recipient = LocalUserView::read_person(&mut context.pool(), recipient_id).await?; let local_recipient = LocalUserView::read_person(&mut context.pool(), recipient_id)
.await?
.ok_or(LemmyErrorType::CouldntFindPerson)?;
let lang = get_interface_language(&local_recipient); let lang = get_interface_language(&local_recipient);
let inbox_link = format!("{}/inbox", context.settings().get_protocol_and_hostname()); let inbox_link = format!("{}/inbox", context.settings().get_protocol_and_hostname());
let sender_name = &local_user_view.person.name; let sender_name = &local_user_view.person.name;

View file

@ -20,7 +20,9 @@ pub async fn delete_private_message(
) -> LemmyResult<Json<PrivateMessageResponse>> { ) -> LemmyResult<Json<PrivateMessageResponse>> {
// Checking permissions // Checking permissions
let private_message_id = data.private_message_id; let private_message_id = data.private_message_id;
let orig_private_message = PrivateMessage::read(&mut context.pool(), private_message_id).await?; let orig_private_message = PrivateMessage::read(&mut context.pool(), private_message_id)
.await?
.ok_or(LemmyErrorType::CouldntFindPrivateMessage)?;
if local_user_view.person.id != orig_private_message.creator_id { if local_user_view.person.id != orig_private_message.creator_id {
Err(LemmyErrorType::EditPrivateMessageNotAllowed)? Err(LemmyErrorType::EditPrivateMessageNotAllowed)?
} }
@ -45,7 +47,9 @@ pub async fn delete_private_message(
) )
.await?; .await?;
let view = PrivateMessageView::read(&mut context.pool(), private_message_id).await?; let view = PrivateMessageView::read(&mut context.pool(), private_message_id)
.await?
.ok_or(LemmyErrorType::CouldntFindPrivateMessage)?;
Ok(Json(PrivateMessageResponse { Ok(Json(PrivateMessageResponse {
private_message_view: view, private_message_view: view,
})) }))

View file

@ -30,7 +30,9 @@ pub async fn update_private_message(
// Checking permissions // Checking permissions
let private_message_id = data.private_message_id; let private_message_id = data.private_message_id;
let orig_private_message = PrivateMessage::read(&mut context.pool(), private_message_id).await?; let orig_private_message = PrivateMessage::read(&mut context.pool(), private_message_id)
.await?
.ok_or(LemmyErrorType::CouldntFindPrivateMessage)?;
if local_user_view.person.id != orig_private_message.creator_id { if local_user_view.person.id != orig_private_message.creator_id {
Err(LemmyErrorType::EditPrivateMessageNotAllowed)? Err(LemmyErrorType::EditPrivateMessageNotAllowed)?
} }
@ -39,7 +41,7 @@ pub async fn update_private_message(
let slur_regex = local_site_to_slur_regex(&local_site); let slur_regex = local_site_to_slur_regex(&local_site);
let url_blocklist = get_url_blocklist(&context).await?; let url_blocklist = get_url_blocklist(&context).await?;
let content = process_markdown(&data.content, &slur_regex, &url_blocklist, &context).await?; let content = process_markdown(&data.content, &slur_regex, &url_blocklist, &context).await?;
is_valid_body_field(&Some(content.clone()), false)?; is_valid_body_field(&content, false)?;
let private_message_id = data.private_message_id; let private_message_id = data.private_message_id;
PrivateMessage::update( PrivateMessage::update(
@ -54,7 +56,9 @@ pub async fn update_private_message(
.await .await
.with_lemmy_type(LemmyErrorType::CouldntUpdatePrivateMessage)?; .with_lemmy_type(LemmyErrorType::CouldntUpdatePrivateMessage)?;
let view = PrivateMessageView::read(&mut context.pool(), private_message_id).await?; let view = PrivateMessageView::read(&mut context.pool(), private_message_id)
.await?
.ok_or(LemmyErrorType::CouldntFindPrivateMessage)?;
ActivityChannel::submit_activity( ActivityChannel::submit_activity(
SendActivityData::UpdatePrivateMessage(view.clone()), SendActivityData::UpdatePrivateMessage(view.clone()),

View file

@ -11,7 +11,7 @@ use lemmy_api_common::{
local_site_rate_limit_to_rate_limit_config, local_site_rate_limit_to_rate_limit_config,
local_site_to_slur_regex, local_site_to_slur_regex,
process_markdown_opt, process_markdown_opt,
proxy_image_link_opt_api, proxy_image_link_api,
}, },
}; };
use lemmy_db_schema::{ use lemmy_db_schema::{
@ -23,7 +23,7 @@ use lemmy_db_schema::{
tagline::Tagline, tagline::Tagline,
}, },
traits::Crud, traits::Crud,
utils::{diesel_option_overwrite, naive_now}, utils::{diesel_string_update, diesel_url_create, naive_now},
}; };
use lemmy_db_views::structs::{LocalUserView, SiteView}; use lemmy_db_views::structs::{LocalUserView, SiteView};
use lemmy_utils::{ use lemmy_utils::{
@ -61,21 +61,25 @@ pub async fn create_site(
let slur_regex = local_site_to_slur_regex(&local_site); let slur_regex = local_site_to_slur_regex(&local_site);
let url_blocklist = get_url_blocklist(&context).await?; let url_blocklist = get_url_blocklist(&context).await?;
let sidebar = process_markdown_opt(&data.sidebar, &slur_regex, &url_blocklist, &context).await?; let sidebar = process_markdown_opt(&data.sidebar, &slur_regex, &url_blocklist, &context).await?;
let icon = proxy_image_link_opt_api(&data.icon, &context).await?;
let banner = proxy_image_link_opt_api(&data.banner, &context).await?; let icon = diesel_url_create(data.icon.as_deref())?;
let icon = proxy_image_link_api(icon, &context).await?;
let banner = diesel_url_create(data.banner.as_deref())?;
let banner = proxy_image_link_api(banner, &context).await?;
let site_form = SiteUpdateForm { let site_form = SiteUpdateForm {
name: Some(data.name.clone()), name: Some(data.name.clone()),
sidebar: diesel_option_overwrite(sidebar), sidebar: diesel_string_update(sidebar.as_deref()),
description: diesel_option_overwrite(data.description.clone()), description: diesel_string_update(data.description.as_deref()),
icon, icon: Some(icon),
banner, banner: Some(banner),
actor_id: Some(actor_id), actor_id: Some(actor_id),
last_refreshed_at: Some(naive_now()), last_refreshed_at: Some(naive_now()),
inbox_url, inbox_url,
private_key: Some(Some(keypair.private_key)), private_key: Some(Some(keypair.private_key)),
public_key: Some(keypair.public_key), public_key: Some(keypair.public_key),
content_warning: diesel_option_overwrite(data.content_warning.clone()), content_warning: diesel_string_update(data.content_warning.as_deref()),
..Default::default() ..Default::default()
}; };
@ -91,16 +95,16 @@ pub async fn create_site(
enable_nsfw: data.enable_nsfw, enable_nsfw: data.enable_nsfw,
community_creation_admin_only: data.community_creation_admin_only, community_creation_admin_only: data.community_creation_admin_only,
require_email_verification: data.require_email_verification, require_email_verification: data.require_email_verification,
application_question: diesel_option_overwrite(data.application_question.clone()), application_question: diesel_string_update(data.application_question.as_deref()),
private_instance: data.private_instance, private_instance: data.private_instance,
default_theme: data.default_theme.clone(), default_theme: data.default_theme.clone(),
default_post_listing_type: data.default_post_listing_type, default_post_listing_type: data.default_post_listing_type,
default_sort_type: data.default_sort_type, default_sort_type: data.default_sort_type,
legal_information: diesel_option_overwrite(data.legal_information.clone()), legal_information: diesel_string_update(data.legal_information.as_deref()),
application_email_admins: data.application_email_admins, application_email_admins: data.application_email_admins,
hide_modlog_mod_names: data.hide_modlog_mod_names, hide_modlog_mod_names: data.hide_modlog_mod_names,
updated: Some(Some(naive_now())), updated: Some(Some(naive_now())),
slur_filter_regex: diesel_option_overwrite(data.slur_filter_regex.clone()), slur_filter_regex: diesel_string_update(data.slur_filter_regex.as_deref()),
actor_name_max_length: data.actor_name_max_length, actor_name_max_length: data.actor_name_max_length,
federation_enabled: data.federation_enabled, federation_enabled: data.federation_enabled,
captcha_enabled: data.captcha_enabled, captcha_enabled: data.captcha_enabled,
@ -129,7 +133,9 @@ pub async fn create_site(
LocalSiteRateLimit::update(&mut context.pool(), &local_site_rate_limit_form).await?; LocalSiteRateLimit::update(&mut context.pool(), &local_site_rate_limit_form).await?;
let site_view = SiteView::read_local(&mut context.pool()).await?; let site_view = SiteView::read_local(&mut context.pool())
.await?
.ok_or(LemmyErrorType::LocalSiteNotSetup)?;
let new_taglines = data.taglines.clone(); let new_taglines = data.taglines.clone();
let taglines = Tagline::replace(&mut context.pool(), local_site.id, new_taglines).await?; let taglines = Tagline::replace(&mut context.pool(), local_site.id, new_taglines).await?;
@ -177,7 +183,9 @@ fn validate_create_payload(local_site: &LocalSite, create_site: &CreateSite) ->
)?; )?;
// Ensure that the sidebar has fewer than the max num characters... // Ensure that the sidebar has fewer than the max num characters...
is_valid_body_field(&create_site.sidebar, false)?; if let Some(body) = &create_site.sidebar {
is_valid_body_field(body, false)?;
}
application_question_check( application_question_check(
&local_site.application_question, &local_site.application_question,

View file

@ -41,7 +41,9 @@ pub async fn get_site(
// This data is independent from the user account so we can cache it across requests // This data is independent from the user account so we can cache it across requests
let mut site_response = CACHE let mut site_response = CACHE
.try_get_with::<_, LemmyError>((), async { .try_get_with::<_, LemmyError>((), async {
let site_view = SiteView::read_local(&mut context.pool()).await?; let site_view = SiteView::read_local(&mut context.pool())
.await?
.ok_or(LemmyErrorType::LocalSiteNotSetup)?;
let admins = PersonView::admins(&mut context.pool()).await?; let admins = PersonView::admins(&mut context.pool()).await?;
let all_languages = Language::read_all(&mut context.pool()).await?; let all_languages = Language::read_all(&mut context.pool()).await?;
let discussion_languages = SiteLanguage::read_local_raw(&mut context.pool()).await?; let discussion_languages = SiteLanguage::read_local_raw(&mut context.pool()).await?;

View file

@ -27,7 +27,7 @@ use lemmy_db_schema::{
tagline::Tagline, tagline::Tagline,
}, },
traits::Crud, traits::Crud,
utils::{diesel_option_overwrite, naive_now}, utils::{diesel_string_update, diesel_url_update, naive_now},
RegistrationMode, RegistrationMode,
}; };
use lemmy_db_views::structs::{LocalUserView, SiteView}; use lemmy_db_views::structs::{LocalUserView, SiteView};
@ -52,7 +52,9 @@ pub async fn update_site(
context: Data<LemmyContext>, context: Data<LemmyContext>,
local_user_view: LocalUserView, local_user_view: LocalUserView,
) -> LemmyResult<Json<SiteResponse>> { ) -> LemmyResult<Json<SiteResponse>> {
let site_view = SiteView::read_local(&mut context.pool()).await?; let site_view = SiteView::read_local(&mut context.pool())
.await?
.ok_or(LemmyErrorType::LocalSiteNotSetup)?;
let local_site = site_view.local_site; let local_site = site_view.local_site;
let site = site_view.site; let site = site_view.site;
@ -65,22 +67,29 @@ pub async fn update_site(
SiteLanguage::update(&mut context.pool(), discussion_languages.clone(), &site).await?; SiteLanguage::update(&mut context.pool(), discussion_languages.clone(), &site).await?;
} }
replace_image(&data.icon, &site.icon, &context).await?;
replace_image(&data.banner, &site.banner, &context).await?;
let slur_regex = local_site_to_slur_regex(&local_site); let slur_regex = local_site_to_slur_regex(&local_site);
let url_blocklist = get_url_blocklist(&context).await?; let url_blocklist = get_url_blocklist(&context).await?;
let sidebar = process_markdown_opt(&data.sidebar, &slur_regex, &url_blocklist, &context).await?; let sidebar = diesel_string_update(
let icon = proxy_image_link_opt_api(&data.icon, &context).await?; process_markdown_opt(&data.sidebar, &slur_regex, &url_blocklist, &context)
let banner = proxy_image_link_opt_api(&data.banner, &context).await?; .await?
.as_deref(),
);
let icon = diesel_url_update(data.icon.as_deref())?;
replace_image(&icon, &site.icon, &context).await?;
let icon = proxy_image_link_opt_api(icon, &context).await?;
let banner = diesel_url_update(data.banner.as_deref())?;
replace_image(&banner, &site.banner, &context).await?;
let banner = proxy_image_link_opt_api(banner, &context).await?;
let site_form = SiteUpdateForm { let site_form = SiteUpdateForm {
name: data.name.clone(), name: data.name.clone(),
sidebar: diesel_option_overwrite(sidebar), sidebar,
description: diesel_option_overwrite(data.description.clone()), description: diesel_string_update(data.description.as_deref()),
icon, icon,
banner, banner,
content_warning: diesel_option_overwrite(data.content_warning.clone()), content_warning: diesel_string_update(data.content_warning.as_deref()),
updated: Some(Some(naive_now())), updated: Some(Some(naive_now())),
..Default::default() ..Default::default()
}; };
@ -97,16 +106,16 @@ pub async fn update_site(
enable_nsfw: data.enable_nsfw, enable_nsfw: data.enable_nsfw,
community_creation_admin_only: data.community_creation_admin_only, community_creation_admin_only: data.community_creation_admin_only,
require_email_verification: data.require_email_verification, require_email_verification: data.require_email_verification,
application_question: diesel_option_overwrite(data.application_question.clone()), application_question: diesel_string_update(data.application_question.as_deref()),
private_instance: data.private_instance, private_instance: data.private_instance,
default_theme: data.default_theme.clone(), default_theme: data.default_theme.clone(),
default_post_listing_type: data.default_post_listing_type, default_post_listing_type: data.default_post_listing_type,
default_sort_type: data.default_sort_type, default_sort_type: data.default_sort_type,
legal_information: diesel_option_overwrite(data.legal_information.clone()), legal_information: diesel_string_update(data.legal_information.as_deref()),
application_email_admins: data.application_email_admins, application_email_admins: data.application_email_admins,
hide_modlog_mod_names: data.hide_modlog_mod_names, hide_modlog_mod_names: data.hide_modlog_mod_names,
updated: Some(Some(naive_now())), updated: Some(Some(naive_now())),
slur_filter_regex: diesel_option_overwrite(data.slur_filter_regex.clone()), slur_filter_regex: diesel_string_update(data.slur_filter_regex.as_deref()),
actor_name_max_length: data.actor_name_max_length, actor_name_max_length: data.actor_name_max_length,
federation_enabled: data.federation_enabled, federation_enabled: data.federation_enabled,
captcha_enabled: data.captcha_enabled, captcha_enabled: data.captcha_enabled,
@ -154,7 +163,8 @@ pub async fn update_site(
// TODO can't think of a better way to do this. // TODO can't think of a better way to do this.
// If the server suddenly requires email verification, or required applications, no old users // If the server suddenly requires email verification, or required applications, no old users
// will be able to log in. It really only wants this to be a requirement for NEW signups. // will be able to log in. It really only wants this to be a requirement for NEW signups.
// So if it was set from false, to true, you need to update all current users columns to be verified. // So if it was set from false, to true, you need to update all current users columns to be
// verified.
let old_require_application = let old_require_application =
local_site.registration_mode == RegistrationMode::RequireApplication; local_site.registration_mode == RegistrationMode::RequireApplication;
@ -181,7 +191,9 @@ pub async fn update_site(
let new_taglines = data.taglines.clone(); let new_taglines = data.taglines.clone();
let taglines = Tagline::replace(&mut context.pool(), local_site.id, new_taglines).await?; let taglines = Tagline::replace(&mut context.pool(), local_site.id, new_taglines).await?;
let site_view = SiteView::read_local(&mut context.pool()).await?; let site_view = SiteView::read_local(&mut context.pool())
.await?
.ok_or(LemmyErrorType::LocalSiteNotSetup)?;
let rate_limit_config = let rate_limit_config =
local_site_rate_limit_to_rate_limit_config(&site_view.local_site_rate_limit); local_site_rate_limit_to_rate_limit_config(&site_view.local_site_rate_limit);
@ -224,7 +236,9 @@ fn validate_update_payload(local_site: &LocalSite, edit_site: &EditSite) -> Lemm
)?; )?;
// Ensure that the sidebar has fewer than the max num characters... // Ensure that the sidebar has fewer than the max num characters...
is_valid_body_field(&edit_site.sidebar, false)?; if let Some(body) = &edit_site.sidebar {
is_valid_body_field(body, false)?;
}
application_question_check( application_question_check(
&local_site.application_question, &local_site.application_question,

View file

@ -45,7 +45,9 @@ pub async fn register(
req: HttpRequest, req: HttpRequest,
context: Data<LemmyContext>, context: Data<LemmyContext>,
) -> LemmyResult<Json<LoginResponse>> { ) -> LemmyResult<Json<LoginResponse>> {
let site_view = SiteView::read_local(&mut context.pool()).await?; let site_view = SiteView::read_local(&mut context.pool())
.await?
.ok_or(LemmyErrorType::LocalSiteNotSetup)?;
let local_site = site_view.local_site; let local_site = site_view.local_site;
let require_registration_application = let require_registration_application =
local_site.registration_mode == RegistrationMode::RequireApplication; local_site.registration_mode == RegistrationMode::RequireApplication;
@ -110,15 +112,17 @@ pub async fn register(
// We have to create both a person, and local_user // We have to create both a person, and local_user
// Register the new person // Register the new person
let person_form = PersonInsertForm::builder() let person_form = PersonInsertForm {
.name(data.username.clone()) actor_id: Some(actor_id.clone()),
.actor_id(Some(actor_id.clone())) inbox_url: Some(generate_inbox_url(&actor_id)?),
.private_key(Some(actor_keypair.private_key)) shared_inbox_url: Some(generate_shared_inbox_url(context.settings())?),
.public_key(actor_keypair.public_key) private_key: Some(actor_keypair.private_key),
.inbox_url(Some(generate_inbox_url(&actor_id)?)) ..PersonInsertForm::new(
.shared_inbox_url(Some(generate_shared_inbox_url(context.settings())?)) data.username.clone(),
.instance_id(site_view.site.instance_id) actor_keypair.public_key,
.build(); site_view.site.instance_id,
)
};
// insert the person // insert the person
let inserted_person = Person::create(&mut context.pool(), &person_form) let inserted_person = Person::create(&mut context.pool(), &person_form)
@ -140,12 +144,17 @@ pub async fn register(
.map(|lang_str| lang_str.split('-').next().unwrap_or_default().to_string()) .map(|lang_str| lang_str.split('-').next().unwrap_or_default().to_string())
.collect(); .collect();
// Show nsfw content if param is true, or if content_warning exists
let show_nsfw = data
.show_nsfw
.unwrap_or(site_view.site.content_warning.is_some());
// Create the local user // Create the local user
let local_user_form = LocalUserInsertForm::builder() let local_user_form = LocalUserInsertForm::builder()
.person_id(inserted_person.id) .person_id(inserted_person.id)
.email(data.email.as_deref().map(str::to_lowercase)) .email(data.email.as_deref().map(str::to_lowercase))
.password_encrypted(data.password.to_string()) .password_encrypted(data.password.to_string())
.show_nsfw(Some(data.show_nsfw)) .show_nsfw(Some(show_nsfw))
.accepted_application(accepted_application) .accepted_application(accepted_application)
.default_listing_type(Some(local_site.default_post_listing_type)) .default_listing_type(Some(local_site.default_post_listing_type))
.post_listing_mode(Some(local_site.default_post_listing_mode)) .post_listing_mode(Some(local_site.default_post_listing_mode))
@ -190,7 +199,8 @@ pub async fn register(
verify_email_sent: false, verify_email_sent: false,
}; };
// Log the user in directly if the site is not setup, or email verification and application aren't required // Log the user in directly if the site is not setup, or email verification and application aren't
// required
if !local_site.site_setup if !local_site.site_setup
|| (!require_registration_application && !local_site.require_email_verification) || (!require_registration_application && !local_site.require_email_verification)
{ {

View file

@ -44,7 +44,7 @@ once_cell = { workspace = true }
moka.workspace = true moka.workspace = true
serde_with.workspace = true serde_with.workspace = true
html2md = "0.2.14" html2md = "0.2.14"
html2text = "0.6.0" html2text = "0.12.5"
stringreader = "0.1.1" stringreader = "0.1.1"
enum_delegate = "0.2.0" enum_delegate = "0.2.0"

View file

@ -0,0 +1,22 @@
{
"id": "https://socialhub.activitypub.rocks/ap/actor/797217cf18c0e819dfafc52425590146",
"type": "Group",
"updated": "2024-04-05T12:49:51Z",
"url": "https://socialhub.activitypub.rocks/c/meeting/threadiverse-wg/88",
"name": "Threadiverse Working Group (SocialHub)",
"inbox": "https://socialhub.activitypub.rocks/ap/actor/797217cf18c0e819dfafc52425590146/inbox",
"outbox": "https://socialhub.activitypub.rocks/ap/actor/797217cf18c0e819dfafc52425590146/outbox",
"followers": "https://socialhub.activitypub.rocks/ap/actor/797217cf18c0e819dfafc52425590146/followers",
"preferredUsername": "threadiverse-wg",
"publicKey": {
"id": "https://socialhub.activitypub.rocks/ap/actor/797217cf18c0e819dfafc52425590146#main-key",
"owner": "https://socialhub.activitypub.rocks/ap/actor/797217cf18c0e819dfafc52425590146",
"publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEApJi4iAcW6bPiHVCxT9p0\n8DVnrDDO4QtLNy7bpRFdMFifmmmXprsuAi9D2MSwbhH49V54HtIkxBpKd2IR/UD8\nmhMDY4CNI9FHpjqLw0wtkzxcqF9urSqhn0/vWX+9oxyhIgQS5KMiIkYDMJiAc691\niEcZ8LCran23xIGl6Dk54Nr3TqTMLcjDhzQYUJbxMrLq5/knWqOKG3IF5OxK+9ZZ\n1wxDF872eJTxJLkmpag+WYNtHzvB2SGTp8j5IF1/pZ9J1c3cpYfaeolTch/B/GQn\najCB4l27U52rIIObxJqFXSY8wHyd0aAmNmxzPZ7cduRlBDhmI40cAmnCV1YQPvpk\nDwIDAQAB\n-----END PUBLIC KEY-----\n"
},
"icon": {
"type": "Image",
"mediaType": "image/png",
"url": "https://socialhub.activitypub.rocks/uploads/default/original/1X/8faac84234dc73d074dadaa2bcf24dc746b8647f.png"
},
"@context": "https://www.w3.org/ns/activitystreams"
}

View file

@ -0,0 +1,13 @@
{
"id": "https://socialhub.activitypub.rocks/ap/object/1899f65c062200daec50a4c89ed76dc9",
"type": "Note",
"audience": "https://socialhub.activitypub.rocks/ap/actor/797217cf18c0e819dfafc52425590146",
"published": "2024-04-13T14:36:19Z",
"updated": "2024-04-13T14:36:19Z",
"url": "https://socialhub.activitypub.rocks/t/our-next-meeting/4079/1",
"attributedTo": "https://socialhub.activitypub.rocks/ap/actor/495843076e9e469fbd35ccf467ae9fb1",
"name": "Our next meeting",
"context": "https://socialhub.activitypub.rocks/ap/collection/8850f6e85b57c490da915a5dfbbd5045",
"content": "<h3>Last Meeting</h3>\n<h4>Recording</h4>\n<a href=\"https://us06web.zoom.us/rec/share/4hGBTvgXJPlu8UkjkkxVARypNg5DH0eeaKlIBv71D4G3lokYyrCrg7cqBCJmL109.FsHYTZDlVvZXrgcn?startTime=1712254114000\">https://us06web.zoom.us/rec/share/4hGBTvgXJPlu8UkjkkxVARypNg5DH0eeaKlIBv71D4G3lokYyrCrg7cqBCJmL109.FsHYTZDlVvZXrgcn?startTime=1712254114000</a>\nPasscode: z+1*4pUB\n<h4>Minutes</h4>\nTo refresh your memory, you can read the minutes of last week's meeting <a href=\"https://community.nodebb.org/topic/17949/minutes&hellip;",
"@context": "https://www.w3.org/ns/activitystreams"
}

View file

@ -0,0 +1,23 @@
{
"id": "https://socialhub.activitypub.rocks/ap/actor/495843076e9e469fbd35ccf467ae9fb1",
"type": "Person",
"updated": "2024-01-15T12:27:03Z",
"url": "https://socialhub.activitypub.rocks/u/angus",
"name": "Angus McLeod",
"inbox": "https://socialhub.activitypub.rocks/ap/actor/495843076e9e469fbd35ccf467ae9fb1/inbox",
"outbox": "https://socialhub.activitypub.rocks/ap/actor/495843076e9e469fbd35ccf467ae9fb1/outbox",
"sharedInbox": "https://socialhub.activitypub.rocks/ap/users/inbox",
"followers": "https://socialhub.activitypub.rocks/ap/actor/495843076e9e469fbd35ccf467ae9fb1/followers",
"preferredUsername": "angus",
"publicKey": {
"id": "https://socialhub.activitypub.rocks/ap/actor/495843076e9e469fbd35ccf467ae9fb1#main-key",
"owner": "https://socialhub.activitypub.rocks/ap/actor/495843076e9e469fbd35ccf467ae9fb1",
"publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA3RpuFDuwXZzOeHO5fO2O\nHmP7Flc5JDXJ8OOEJYq5T/dzUKqREOF1ZT0WMww8/E3P6w+gfFsjzThraJb8nHuW\nP6798SUD35CWBclfhw9DapjVn99JyFcAWcH3b9fr6LYshc4y1BoeJagk1kcro2Dc\n+pX0vVXgNjwWnGfyucAgGIUWrNUjcvIvXmyVCBSQfXG3nCALV1JbI4KSgf/5KyBn\nza/QefaetxYiFV8wAisPKLsz3XQAaITsQmbSi+8gmwXt/9U808PK1KphCiClDOWg\noi0HPzJn0rn+mwFCfgNWenvribfeG40AHLG33OkWKvslufjifdWDCOcBYYzyCEV6\n+wIDAQAB\n-----END PUBLIC KEY-----\n"
},
"icon": {
"type": "Image",
"mediaType": "image/png",
"url": "https://socialhub.activitypub.rocks/user_avatar/socialhub.activitypub.rocks/angus/96/2295_2.png"
},
"@context": "https://www.w3.org/ns/activitystreams"
}

View file

@ -23,7 +23,6 @@
"href": "https://lemmy.ml/pictrs/image/xl8W7FZfk9.jpg" "href": "https://lemmy.ml/pictrs/image/xl8W7FZfk9.jpg"
} }
], ],
"commentsEnabled": true,
"sensitive": false, "sensitive": false,
"language": { "language": {
"identifier": "ko", "identifier": "ko",

View file

@ -23,7 +23,6 @@
"href": "https://lemmy.ml/pictrs/image/xl8W7FZfk9.jpg" "href": "https://lemmy.ml/pictrs/image/xl8W7FZfk9.jpg"
} }
], ],
"commentsEnabled": true,
"sensitive": false, "sensitive": false,
"published": "2021-10-29T15:10:51.557399Z", "published": "2021-10-29T15:10:51.557399Z",
"updated": "2021-10-29T15:11:35.976374Z" "updated": "2021-10-29T15:11:35.976374Z"

View file

@ -15,7 +15,6 @@
"cc": [], "cc": [],
"mediaType": "text/html", "mediaType": "text/html",
"attachment": [], "attachment": [],
"commentsEnabled": true,
"sensitive": false, "sensitive": false,
"published": "2023-02-06T06:42:41.939437Z", "published": "2023-02-06T06:42:41.939437Z",
"language": { "language": {
@ -36,7 +35,6 @@
"cc": [], "cc": [],
"mediaType": "text/html", "mediaType": "text/html",
"attachment": [], "attachment": [],
"commentsEnabled": true,
"sensitive": false, "sensitive": false,
"published": "2023-02-06T06:42:37.119567Z", "published": "2023-02-06T06:42:37.119567Z",
"language": { "language": {

View file

@ -22,7 +22,6 @@
], ],
"name": "another outbox test", "name": "another outbox test",
"mediaType": "text/html", "mediaType": "text/html",
"commentsEnabled": true,
"sensitive": false, "sensitive": false,
"stickied": false, "stickied": false,
"published": "2021-11-18T17:19:45.895163Z" "published": "2021-11-18T17:19:45.895163Z"
@ -51,7 +50,6 @@
], ],
"name": "outbox test", "name": "outbox test",
"mediaType": "text/html", "mediaType": "text/html",
"commentsEnabled": true,
"sensitive": false, "sensitive": false,
"stickied": false, "stickied": false,
"published": "2021-11-18T17:19:05.763109Z" "published": "2021-11-18T17:19:05.763109Z"

View file

@ -25,7 +25,6 @@
"url": "https://enterprise.lemmy.ml/pictrs/image/eOtYb9iEiB.png" "url": "https://enterprise.lemmy.ml/pictrs/image/eOtYb9iEiB.png"
}, },
"sensitive": false, "sensitive": false,
"commentsEnabled": true,
"language": { "language": {
"identifier": "fr", "identifier": "fr",
"name": "Français" "name": "Français"

View file

@ -0,0 +1,22 @@
{
"@context": "https://www.w3.org/ns/activitystreams",
"id": "https://community.nodebb.org/category/31",
"url": "https://community.nodebb.org/category/31/threadiverse-working-group",
"inbox": "https://community.nodebb.org/category/31/inbox",
"outbox": "https://community.nodebb.org/category/31/outbox",
"sharedInbox": "https://community.nodebb.org/inbox",
"type": "Group",
"name": "Threadiverse Working Group",
"preferredUsername": "swicg-threadiverse-wg",
"summary": "Discussion and announcements related to the SWICG Threadiverse task force",
"icon": {
"type": "Image",
"mediaType": "image/png",
"url": "https://community.nodebb.org/assets/uploads/system/site-logo.png"
},
"publicKey": {
"id": "https://community.nodebb.org/category/31#key",
"owner": "https://community.nodebb.org/category/31",
"publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0/Or3Ox2/jbhBZzF8W0Y\nWuS/4lgm5O5rxQk2nDRBXU/qNaZnMPkW2FxFPuPetndUVKSD2+vWF3SUlFyZ/vhT\nITzLkbRSILMiZCUg+0mvqi6va1WMBglMe5jLkc7wdfgNsosqBzKMdyMxqDZr++mJ\n8DjuqzWHENcjWcbMfSfAa9nkZHBIQUsHGGIwxEbKNlPqF0JIB66py7xmXbboDxpD\nPVF3EMkgZNnbmDGtlkZCKbztradyNRVl/u6KJpV3fbi+m/8CZ+POc4I5sKCQY1Hr\ndslHlm6tCkJQxIIKQtz0ZJ5yCUYmk48C2gFCndfJtYoEy9iR62xSemky6y04gWVc\naQIDAQAB\n-----END PUBLIC KEY-----\n"
}
}

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