Compare commits

...

184 commits

Author SHA1 Message Date
dc94e58cbf Merge branch 'master' into federation_merge_from_master_2 2020-06-23 21:11:38 -04:00
fd6a040568 Changing another blacklisted to blocklisted. 2020-06-17 21:29:10 -04:00
68bcc26ff6 Fix community description federation (ref #647)
Also disable the actor refetch interval in debug builds.
2020-06-16 13:35:26 +02:00
325ed2ec3b Rename "instance_whitelist" config option to "allowed_instances" 2020-06-16 12:49:51 +02:00
Dessalines
cfa40e482a
Parse out in reply to field. Fixes #694 (#799)
- When a comment or post doesn't exist locally, yet we receive
  an activitypub with it (for example, a nested comment update,
  for a community we just subscribed to, also with a post we
  don't have...), then fetch it.
2020-06-11 11:16:33 -04:00
0f1a8ec928 Merge branch 'master' into federation 2020-06-09 14:01:26 +02:00
5c6601cb2a Merge branch 'activitystreams-new' into federation 2020-06-07 09:12:15 -04:00
f40f74b20d Adding additional 3-instance integration test for community announce. 2020-06-03 17:55:32 -04:00
c1ef766125 migrate follow 2020-06-03 21:44:14 +02:00
3999e0485e migrate tombstone 2020-06-03 17:10:16 +02:00
1aa30d855e address review comments 2020-06-01 16:48:07 +02:00
f3aba6da92 fix comment notifications 2020-05-30 20:05:42 +02:00
c34cc46c2d get it working (mostly) 2020-05-30 19:44:50 +02:00
52206998aa more fixes 2020-05-30 15:38:01 +02:00
d6e2119277 make comments work (more or less) 2020-05-28 20:09:37 +02:00
8f9bd1fef7 get some more things working 2020-05-28 15:20:12 +02:00
ce0a37cdf1 get it working (mostly) 2020-05-28 13:51:48 +02:00
cac7011d53 move logic to sharedinbox 2020-05-28 13:44:52 +02:00
5753c4feaa Let community announce posts 2020-05-28 13:15:51 +02:00
Anthony Ettinger
b08574fd57 fix dimensions 2020-05-28 12:45:50 +02:00
Anthony Ettinger
baa402f82f add icon 2020-05-28 12:45:50 +02:00
a9af247f1e Merge branch 'federated_embeds' into federation 2020-05-25 16:15:23 -04:00
d1aca27126 Use activitystreams-ext 2020-05-18 18:15:26 +02:00
f15c3b4e1e Merge branch 'yerba_rework-imports' into federation 2020-05-16 21:09:26 -04:00
9e61c3be94 Rework imports 2020-05-16 20:04:17 +02:00
f88180650d Adding federated embeds via the preview field. 2020-05-15 23:40:36 -04:00
020b9b8cdd Post thumbnail and user icons federating. 2020-05-15 20:23:20 -04:00
Dessalines
940dc73f28 Federated mentions. Fixes #681 (#717)
* Federated mentions. Fixes #681

* Changing some todos, adding comments.
2020-05-15 12:36:11 -04:00
3a4973ad68 Merge branch 'federation' into yerba_refactoring 2020-05-14 18:35:29 -04:00
0fb8450e56 Simplify community_inbox 2020-05-14 17:44:01 +02:00
13ca47a3b4 Use ActorType for sign/verify, instead of passing raw privatekey/actor_id 2020-05-14 17:17:40 +02:00
11acc7225e Add helper function for Activity::create() 2020-05-14 14:26:44 +02:00
a1ad21ec56 remove outdated comments 2020-05-14 13:23:56 +02:00
bb1b4ee33e Comment search and apub endpoint 2020-05-14 12:42:26 +02:00
66142c546b Merge branch 'federated_private_messages' into federation 2020-05-06 13:51:39 -04:00
15f1920b25 Federated private messages. 2020-05-05 22:06:24 -04:00
dfd6629a6f Federate community category and nsfw 2020-05-05 16:30:13 +02:00
21407260a4 Merge branch 'yerba_page_extension' into federation 2020-05-04 21:24:53 -04:00
f1692a07fc Adding a comment on what comment.read is. 2020-05-04 21:14:24 -04:00
7485f1a5b4 Federate nsfw/locked info for posts 2020-05-05 02:09:27 +02:00
b177cbce1d Renaming ignore to unfollow in followable trait. 2020-05-04 14:26:16 -04:00
b8b2398d32 Adding undo follow community. 2020-05-03 22:41:45 -04:00
fab22e3d8a Add federated comment and post undo like. 2020-05-03 20:34:04 -04:00
dfc9637230 Merge branch 'federation' into federated_remove_actions 2020-05-03 13:27:53 -04:00
2c22e413eb Merge branch 'federation' into federated_remove_actions 2020-05-03 13:22:32 -04:00
Dessalines
67d4daa7a1 Merge pull request #687 from LemmyNet/undo_delete_community
Undo delete for community, comment, and post.
2020-05-03 13:21:01 -04:00
211ef795e9 Some additional notes, reorg. 2020-05-03 10:22:25 -04:00
a09c818746 Adding federated mod remove actions. 2020-05-03 10:00:59 -04:00
5366797a4b Add undos for delete community, post, and comment. 2020-05-01 15:01:29 -04:00
75c6c8521b Merge branch 'shorthand_apub_id_searching' into federation 2020-05-01 10:34:41 -04:00
2f1cd9976d Adding federated community, comment, and post deletes.
- Unit tests added too.
- No undeletes working yet.
2020-05-01 10:07:38 -04:00
461114c143 update activitystreams lib 2020-04-30 18:30:01 +02:00
38cdfdf7e0 Changing federated community and user links and searching.
- Search: !community@instance, @user@instance
- Representation: community@instance, user@instance
2020-04-30 11:45:12 -04:00
770dcbdc49 wip: add former_type to tombstone 2020-04-29 21:10:50 +02:00
ee4f923f60 Fix search form clearing out. 2020-04-29 13:05:38 -04:00
8cd68f56aa Adding shorthand federated object searching. 2020-04-29 12:55:54 -04:00
c43f06124a Address comments, implement delete for posts and comments 2020-04-29 16:51:25 +02:00
0c0c683986 Implement deleting communities 2020-04-28 19:46:25 +02:00
36d0e34668 Fix federated community link on search page. 2020-04-28 11:53:05 -04:00
59bba148ff Merge branch 'federation' into add_federated_post_likes 2020-04-28 10:12:07 -04:00
b60c7bbae7 Merge branch 'federation' into add_activity_table 2020-04-28 10:10:05 -04:00
07a9d84ed0 Removing some comments. 2020-04-28 09:57:28 -04:00
3b62f58dd2 Adding federated post and comment likes. 2020-04-28 00:16:02 -04:00
6eaa06ab02 Merge branch 'shared_inbox_1' into add_federated_post_likes 2020-04-27 22:48:20 -04:00
9721b77317 1/3rd done with post likes 2020-04-27 22:47:26 -04:00
4b741c3759 Adding a better switching for activity kinds for the shared inbox. 2020-04-27 19:21:41 -04:00
70060c27b2 Adding activity table inserts. 2020-04-27 18:17:02 -04:00
9c30b37d57 Merge branch 'federation' into shared_inbox_1 2020-04-27 13:01:45 -04:00
Dessalines
10877fd45f Merge pull request #663 from LemmyNet/federation_changes_1
Federation changes.
2020-04-27 13:00:21 -04:00
22abbebd41 Lots of additions to federation.
- Added a shared inbox.
- Added federated comments, comment updates, and tests.
- Abstracted ap object sends into a common trait.
2020-04-27 12:57:00 -04:00
3ce0618362 Making a trait function for follow and accept. 2020-04-26 13:20:42 -04:00
079ac091eb Adding back in post fetching. 2020-04-25 11:49:15 -04:00
b5a5b307a0 Adding get_public_key_ext() to ActorType trait. 2020-04-24 22:34:51 -04:00
df9135f410 Removing run-tests TODO. 2020-04-24 22:02:12 -04:00
8a25f0f816 Use an associated type instead of Generic. 2020-04-24 17:30:27 -04:00
33c5c21a57 Merge remote-tracking branch 'yerba/federation' into federation_changes_1 2020-04-24 15:57:48 -04:00
d846740839 Some more cleanup. 2020-04-24 15:55:54 -04:00
c3ac1649f2 Implement integration test for federation 2020-04-24 18:30:31 +02:00
66a2c4a2c3 Some fed fixes. 2020-04-24 10:04:36 -04:00
e5497edd5c Adding some more API testing examples. 2020-04-23 14:13:22 -04:00
ce800f75ad Adding a test bed for API testing. #658 2020-04-23 11:34:32 -04:00
b8aaf5c1f1 Also use correct header in fetcher.rs 2020-04-23 13:42:09 +02:00
70816a4779 Fix header guard for apub responses 2020-04-22 20:56:31 +02:00
92e30311ce Merge branch 'federation' of https://yerbamate.dev/LemmyNet/lemmy into federation 2020-04-21 20:41:50 -04:00
0425e8b114 Fixing nodeinfo error type. 2020-04-21 20:34:37 -04:00
18e570b021 Merge branch 'master' into merge_master_2 2020-04-21 20:29:52 -04:00
957e4a2611 Change apub IDs to be consistent with html urls 2020-04-21 22:45:01 +02:00
4e80543edb Update test_deploy.sh, supporting different branches 2020-04-21 19:19:10 +02:00
Felix Ableitner
a90b16a72c Merge pull request #649 from LemmyNet/federation_merge_from_master_1
Federation merge from master 1
2020-04-21 16:29:30 +00:00
f0026065f5 Merge branch 'master' into federation_merge_from_master_1 2020-04-21 10:25:29 -04:00
2f4b3a4f83 Merge branch 'remove_username_lower_unique' into federation 2020-04-21 09:25:02 -04:00
697c62fb64 Change local unique username constraints. 2020-04-21 09:21:29 -04:00
1e7c3841b2 Update federation dev instructions 2020-04-21 13:24:08 +02:00
7117b5ce32 Verifyt http signatures 2020-04-19 19:35:40 +02:00
5284dc0c52 Simplify signing code 2020-04-19 13:44:44 +02:00
8daf72278d Add http signature to outgoing apub requests 2020-04-18 20:54:20 +02:00
0199b5f169 Use debug logging 2020-04-18 17:24:55 +02:00
a49bd1d42a Fix bug in whitelist implementation 2020-04-18 17:17:25 +02:00
b1b97db11a Implement instance whitelist 2020-04-17 19:34:18 +02:00
c5ced6fa5e Added documentation for most functions 2020-04-17 17:33:55 +02:00
8908c8b184 Some code cleanup and better logging 2020-04-17 16:55:28 +02:00
9c974fbe50 Remove instance follows 2020-04-17 16:39:03 +02:00
86f172076b Implement search for activitypub IDs 2020-04-17 15:46:08 +02:00
9a85f1b25f Send activities to correct inbox, seperate community/user inboxes 2020-04-15 20:12:25 +02:00
a941c20024 Adding yarn run to run-federation-test.bash script. 2020-04-15 14:06:17 -04:00
7ba6ee8714 Redundant to_owned 2020-04-14 19:25:58 -04:00
fcf1c65fc1 Front end federation names and links for users, posts, and communities. 2020-04-14 19:18:13 -04:00
1336b4ed60 Merge branch 'dev' into federation 2020-04-14 16:07:20 -04:00
f040dac647 Initial post-listing community non-local. 2020-04-14 15:43:58 -04:00
26ad37a8c0 Updating views to add apub actor_id and local columns. 2020-04-14 15:12:19 -04:00
5c83cbc1ac Fixing .dockerignore. 2020-04-14 12:27:38 -04:00
9878a58452 Fixing unwrap crash with site_view.rs 2020-04-14 12:24:05 -04:00
9d2046d5a2 Disable nginx access logs for docker/federation/ 2020-04-14 17:47:55 +02:00
19c8461397 Implemented follow/accept 2020-04-14 17:37:23 +02:00
13e6c98e47 Auto-configure federation test instances during initial start 2020-04-13 19:55:43 +02:00
fdaf0b3364 Get inbox working properly 2020-04-13 15:06:41 +02:00
fac1cc7e1d Use summary field for post title 2020-04-13 14:13:06 +02:00
fc951d9295 Added comments about how to federate additional post/user fields 2020-04-12 16:53:55 +02:00
17d3d2492c Federate actor public keys 2020-04-10 15:50:40 +02:00
5e3902a3bc more todos 2020-04-10 14:45:48 +02:00
509005fa0c Rename federation-test to federation, puller.rs to fetcher.rs 2020-04-10 13:37:35 +02:00
492625f6d6 Add to/cc (and a bunch of todo) 2020-04-10 13:26:06 +02:00
483d11e772 Minor code cleanup 2020-04-09 21:26:22 +02:00
0b617377df Implement create activity 2020-04-09 21:04:31 +02:00
f5b58bcdaf Simplify fetch_posts code 2020-04-08 18:39:45 +02:00
5706b533fd Use instance struct instead of raw string 2020-04-08 18:22:44 +02:00
6962b9c433 Use Url instead of String 2020-04-08 14:37:05 +02:00
edd0ef5991 Some refactoring of puller.rs 2020-04-08 14:08:33 +02:00
d2bad5f79e Improve error handling 2020-04-08 13:23:59 +02:00
61c560c12c Get users federated 2020-04-07 23:02:32 +02:00
d3bd7771d2 remove debug log for post creation 2020-04-07 19:13:33 +02:00
b7103a7e14 Store remote communities/posts in db, federate posts! 2020-04-07 18:47:19 +02:00
1b0da74b57 Revert apub endpoint change (again) 2020-04-07 17:34:44 +02:00
095ccae616 Merge branch 'federation_add_fed_columns' of https://yerbamate.dev/dessalines/lemmy into federation 2020-04-07 17:33:50 +02:00
17bf6baa25 Set accept header and timeout for outgoing apub requests 2020-04-07 17:29:23 +02:00
56947e7710 Removing community name unique constraint. Removing useless fedi_name column from user_table. 2020-04-07 10:54:15 -04:00
4fadc4d072 Revert apub endpoint url changes 2020-04-07 13:21:30 +02:00
85ea1046f0 Adding post and comment ap_id columns. 2020-04-03 20:04:57 -04:00
cb7059f832 Move and rename some functions 2020-04-03 18:32:09 +02:00
c16458b728 Avoid using database views in federation code 2020-04-03 11:00:24 +02:00
6a7a262912 Merge branch 'federation_add_fed_columns' of https://yerbamate.dev/dessalines/lemmy into federation 2020-04-03 07:24:46 +02:00
96c3621a80 Share list of communities over apub, some refactoring 2020-04-03 07:02:43 +02:00
9197b39ed6 Federation DB Changes.
- Creating an activity table.
- Adding some federation-related columns to the user_ and community
  tables.
- Generating the actor_id and keys in code, updating the tables.
2020-04-03 00:12:05 -04:00
32b0275257 Merge branch 'nutomic-federation' into federation 2020-04-02 15:15:44 -04:00
31f835db86 Merge branch 'master' into federation 2020-04-02 15:11:11 -04:00
5ca466117d Merge branch 'master' into federation 2020-03-28 15:41:42 -04:00
0d369e6019 Get image uploads working for federation-test 2020-03-28 17:30:59 +01:00
945fd8331b Merge branch 'nutomic-federation' into federation 2020-03-24 15:18:08 -04:00
4354f868fd Merge branch 'federation' of ssh://yerbamate.dev:222/nutomic/lemmy into federation 2020-03-20 10:47:46 +01:00
bf52bc22e4 Replace reqwest with chttp 2020-03-20 01:42:07 +01:00
875545f7e1 Adjust for updated Rust version 2020-03-19 19:01:01 +01:00
672798e711 Populate post data from apub 2020-03-19 17:27:13 +01:00
nutomic
875ed79f3f Merge pull request 'Update to latest activitystreams' (#1) from asonix/lemmy:federation into federation 2020-03-19 01:23:15 +00:00
20a06ce3f2 Merge branch 'federation' of https://yerbamate.dev/nutomic/lemmy into federation 2020-03-18 20:16:40 -05:00
cfe0d9c9c2 Upgraded to latest activitystreams 2020-03-18 20:16:17 -05:00
33cce05300 Merge branch 'master' into federation 2020-03-18 22:51:34 +01:00
390b204272 Rewrite federation settings 2020-03-18 22:09:00 +01:00
bd030470b1 Read remote nodeinfo before doing anything 2020-03-18 16:08:08 +01:00
5043a52b88 Serve post data in apub format, some cleanup 2020-03-16 19:19:04 +01:00
05735b31c0 Remove boilerplate code 2020-03-16 18:30:25 +01:00
8ebcc7ac02 Implemented basics for post federation, plus a bunch of other stuff 2020-03-14 22:03:05 +01:00
5896a9d251 Move apub related code from websocket into api package 2020-03-14 13:15:23 +01:00
b01f4f75d6 WIP: federate posts between instances 2020-03-14 01:05:42 +01:00
8f67a3c634 Cleanup gitignore and dockerignore files 2020-03-12 20:25:14 +01:00
063811cb60 Merge branch 'master' into federation 2020-03-12 12:34:37 +01:00
27c07f1f84 Federate follower count, use string id for community 2020-03-12 03:35:32 +01:00
54172bd322 updated to activitystreams 0.4.0-alpha.3 2020-03-12 01:01:25 +01:00
8867fa1d52 use urls for id again, more comments 2020-03-11 18:26:58 +01:00
18be8b10f5 improved community federation (wip) 2020-03-11 12:29:10 +01:00
34a827a270 comment 2020-03-05 16:02:53 +01:00
91ae9a9d49 Revert "pull in activitypub library"
This reverts commit a52a954eb4.
2020-03-05 11:32:29 +01:00
1f29e91796 Various minor federation improvements 2020-02-29 18:38:47 +01:00
7cdf167e4b pull in activitypub library 2020-02-29 12:42:44 +01:00
b854d8f3a0 Some federation improvements 2020-02-29 03:11:39 +01:00
f9443dfbd3 Merge branch 'master' into federation 2020-02-29 00:47:37 +01:00
1d824ee293 Merge branch 'dev' into federation 2020-02-10 11:52:32 -05:00
8130535af4 Merge branch 'dev' into federation 2020-02-07 12:34:14 -05:00
f247b28262 Merge branch 'dev' into federation 2020-02-05 16:57:20 -05:00
ac4a62636b Merge branch 'dev' into federation 2020-02-05 14:19:25 -05:00
d932acad16 Merge branch 'federation' into dev_1 2020-02-05 12:51:03 -05:00
eaf548b5db Merge branch 'master' into federation 2020-01-14 16:30:54 +01:00
35489a706b Faster Docker build directly on host 2020-01-10 19:04:58 +01:00
e09a035373 Merge branch 'master' into federation 2020-01-02 19:22:23 +01:00
581f36d6ef Implementing very basic federation including test setup 2019-12-30 13:31:54 +01:00
108 changed files with 13215 additions and 2386 deletions

8
.dockerignore vendored
View file

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

16
.gitignore vendored
View file

@ -1,10 +1,18 @@
# local ansible configuration
ansible/inventory
ansible/inventory_dev
ansible/passwords/
# docker build files
docker/lemmy_mine.hjson
docker/dev/env_deploy.sh
build/
.idea/
ui/src/translations
docker/dev/volumes
docker/federation/volumes
docker/federation-test/volumes
docker/dev/volumes
# local build files
build/
ui/src/translations
# ide config
.idea/

2
README.md vendored
View file

@ -73,7 +73,7 @@ Each lemmy server can set its own moderation policy; appointing site-wide admins
- Full vote scores `(+/-)` like old reddit.
- Themes, including light, dark, and solarized.
- Emojis with autocomplete support. Start typing `:`
- User tagging using `@`, Community tagging using `#`.
- User tagging using `@`, Community tagging using `!`.
- Integrated image uploading in both posts and comments.
- A post can consist of a title and any combination of self text, a URL, or nothing else.
- Notifications, on comment replies and when you're tagged.

View file

@ -1,15 +1,18 @@
#!/bin/sh
#!/bin/bash
set -e
BRANCH=$1
git checkout $BRANCH
export COMPOSE_DOCKER_CLI_BUILD=1
export DOCKER_BUILDKIT=1
# Rebuilding dev docker
docker-compose build
docker tag dev_lemmy:latest dessalines/lemmy:test
docker push dessalines/lemmy:test
sudo docker build . -f "docker/dev/Dockerfile" -t "dessalines/lemmy:$BRANCH"
sudo docker push "dessalines/lemmy:$BRANCH"
# Run the playbook
pushd ../../../lemmy-ansible
ansible-playbook -i test playbooks/site.yml --vault-password-file vault_pass
pushd ../lemmy-ansible
ansible-playbook -i test playbooks/site.yml
popd

23
docker/federation-test/run-tests.sh vendored Executable file
View file

@ -0,0 +1,23 @@
#!/bin/bash
set -e
pushd ../../server/
cargo build
popd
sudo docker build ../../ --file ../federation/Dockerfile --tag lemmy-federation:latest
sudo docker-compose --file ../federation/docker-compose.yml --project-directory . up -d
pushd ../../ui
yarn
echo "Waiting for Lemmy to start..."
while [[ "$(curl -s -o /dev/null -w '%{http_code}' 'localhost:8540/api/v1/site')" != "200" ]]; do sleep 1; done
while [[ "$(curl -s -o /dev/null -w '%{http_code}' 'localhost:8550/api/v1/site')" != "200" ]]; do sleep 1; done
while [[ "$(curl -s -o /dev/null -w '%{http_code}' 'localhost:8560/api/v1/site')" != "200" ]]; do sleep 1; done
yarn api-test || true
popd
sudo docker-compose --file ../federation/docker-compose.yml --project-directory . down
sudo rm -r volumes/

17
docker/federation/Dockerfile vendored Normal file
View file

@ -0,0 +1,17 @@
FROM ekidd/rust-musl-builder:1.42.0-openssl11
USER root
RUN mkdir /app/dist/documentation/ -p \
&& addgroup --gid 1001 lemmy \
&& adduser --disabled-password --shell /bin/sh -u 1001 --ingroup lemmy lemmy
# Copy resources
COPY server/config/defaults.hjson /app/config/defaults.hjson
COPY ui/dist /app/dist
COPY server/target/debug/lemmy_server /app/lemmy
RUN chown lemmy:lemmy /app/ -R
USER lemmy
EXPOSE 8536
WORKDIR /app
CMD ["/app/lemmy"]

132
docker/federation/docker-compose.yml vendored Normal file
View file

@ -0,0 +1,132 @@
version: '3.3'
services:
nginx:
image: nginx:1.17-alpine
ports:
- "8540:8540"
- "8550:8550"
- "8560:8560"
volumes:
# Hack to make this work from both docker/federation/ and docker/federation-test/
- ../federation/nginx.conf:/etc/nginx/nginx.conf
depends_on:
- lemmy_alpha
- pictrs_alpha
- lemmy_beta
- pictrs_beta
- lemmy_gamma
- pictrs_gamma
- iframely
restart: "always"
lemmy_alpha:
image: lemmy-federation:latest
environment:
- LEMMY_HOSTNAME=lemmy_alpha:8540
- LEMMY_DATABASE_URL=postgres://lemmy:password@postgres_alpha:5432/lemmy
- LEMMY_JWT_SECRET=changeme
- LEMMY_FRONT_END_DIR=/app/dist
- LEMMY_FEDERATION__ENABLED=true
- LEMMY_FEDERATION__TLS_ENABLED=false
- LEMMY_FEDERATION__ALLOWED_INSTANCES=lemmy_beta,lemmy_gamma
- LEMMY_PORT=8540
- LEMMY_SETUP__ADMIN_USERNAME=lemmy_alpha
- LEMMY_SETUP__ADMIN_PASSWORD=lemmy
- LEMMY_SETUP__SITE_NAME=lemmy_alpha
- RUST_BACKTRACE=1
- RUST_LOG=debug
restart: always
depends_on:
- postgres_alpha
postgres_alpha:
image: postgres:12-alpine
environment:
- POSTGRES_USER=lemmy
- POSTGRES_PASSWORD=password
- POSTGRES_DB=lemmy
volumes:
- ./volumes/postgres_alpha:/var/lib/postgresql/data
restart: always
pictrs_alpha:
image: asonix/pictrs:v0.1.13-r0
user: 991:991
volumes:
- ./volumes/pictrs_alpha:/mnt
restart: always
lemmy_beta:
image: lemmy-federation:latest
environment:
- LEMMY_HOSTNAME=lemmy_beta:8550
- LEMMY_DATABASE_URL=postgres://lemmy:password@postgres_beta:5432/lemmy
- LEMMY_JWT_SECRET=changeme
- LEMMY_FRONT_END_DIR=/app/dist
- LEMMY_FEDERATION__ENABLED=true
- LEMMY_FEDERATION__TLS_ENABLED=false
- LEMMY_FEDERATION__ALLOWED_INSTANCES=lemmy_alpha,lemmy_gamma
- LEMMY_PORT=8550
- LEMMY_SETUP__ADMIN_USERNAME=lemmy_beta
- LEMMY_SETUP__ADMIN_PASSWORD=lemmy
- LEMMY_SETUP__SITE_NAME=lemmy_beta
- RUST_BACKTRACE=1
- RUST_LOG=debug
restart: always
depends_on:
- postgres_beta
postgres_beta:
image: postgres:12-alpine
environment:
- POSTGRES_USER=lemmy
- POSTGRES_PASSWORD=password
- POSTGRES_DB=lemmy
volumes:
- ./volumes/postgres_beta:/var/lib/postgresql/data
restart: always
pictrs_beta:
image: asonix/pictrs:v0.1.13-r0
user: 991:991
volumes:
- ./volumes/pictrs_beta:/mnt
restart: always
lemmy_gamma:
image: lemmy-federation:latest
environment:
- LEMMY_HOSTNAME=lemmy_gamma:8560
- LEMMY_DATABASE_URL=postgres://lemmy:password@postgres_gamma:5432/lemmy
- LEMMY_JWT_SECRET=changeme
- LEMMY_FRONT_END_DIR=/app/dist
- LEMMY_FEDERATION__ENABLED=true
- LEMMY_FEDERATION__TLS_ENABLED=false
- LEMMY_FEDERATION__ALLOWED_INSTANCES=lemmy_alpha,lemmy_beta
- LEMMY_PORT=8560
- LEMMY_SETUP__ADMIN_USERNAME=lemmy_gamma
- LEMMY_SETUP__ADMIN_PASSWORD=lemmy
- LEMMY_SETUP__SITE_NAME=lemmy_gamma
- RUST_BACKTRACE=1
- RUST_LOG=debug
restart: always
depends_on:
- postgres_gamma
postgres_gamma:
image: postgres:12-alpine
environment:
- POSTGRES_USER=lemmy
- POSTGRES_PASSWORD=password
- POSTGRES_DB=lemmy
volumes:
- ./volumes/postgres_gamma:/var/lib/postgresql/data
restart: always
pictrs_gamma:
image: asonix/pictrs:v0.1.13-r0
user: 991:991
volumes:
- ./volumes/pictrs_gamma:/mnt
restart: always
iframely:
image: dogbin/iframely:latest
volumes:
- ../iframely.config.local.js:/iframely/config.local.js:ro
restart: always

125
docker/federation/nginx.conf vendored Normal file
View file

@ -0,0 +1,125 @@
events {
worker_connections 1024;
}
http {
server {
listen 8540;
server_name 127.0.0.1;
access_log off;
# Upload limit for pictshare
client_max_body_size 50M;
location / {
proxy_pass http://lemmy_alpha:8540;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# WebSocket support
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
# pict-rs images
location /pictrs {
location /pictrs/image {
proxy_pass http://pictrs_alpha:8080/image;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
# Block the import
return 403;
}
location /iframely/ {
proxy_pass http://iframely:80/;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
server {
listen 8550;
server_name 127.0.0.1;
access_log off;
# Upload limit for pictshare
client_max_body_size 50M;
location / {
proxy_pass http://lemmy_beta:8550;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# WebSocket support
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
# pict-rs images
location /pictrs {
location /pictrs/image {
proxy_pass http://pictrs_beta:8080/image;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
# Block the import
return 403;
}
location /iframely/ {
proxy_pass http://iframely:80/;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
server {
listen 8560;
server_name 127.0.0.1;
access_log off;
# Upload limit for pictshare
client_max_body_size 50M;
location / {
proxy_pass http://lemmy_gamma:8560;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# WebSocket support
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
# pict-rs images
location /pictrs {
location /pictrs/image {
proxy_pass http://pictrs_gamma:8080/image;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
# Block the import
return 403;
}
location /iframely/ {
proxy_pass http://iframely:80/;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
}

28
docker/federation/run-federation-test.bash vendored Executable file
View file

@ -0,0 +1,28 @@
#!/bin/bash
set -e
# already start rust build in the background
pushd ../../server/ || exit
cargo build &
popd || exit
if [ "$1" = "-yarn" ]; then
pushd ../../ui/ || exit
yarn
yarn build
popd || exit
fi
# wait for rust build to finish
pushd ../../server/ || exit
cargo build
popd || exit
sudo docker build ../../ --file Dockerfile -t lemmy-federation:latest
for Item in alpha beta gamma ; do
sudo mkdir -p volumes/pictrs_$Item
sudo chown -R 991:991 volumes/pictrs_$Item
done
sudo docker-compose up

View file

@ -3,7 +3,7 @@
Start typing...
- `@a_user_name` to get a list of usernames.
- `#a_community` to get a list of communities.
- `!a_community` to get a list of communities.
- `:emoji` to get a list of emojis.
## Sorting

View file

@ -15,7 +15,7 @@ git checkout federation
git pull federation federation
```
## Running
## Running locally
You need to have the following packages installed, the Docker service needs to be running.
@ -31,7 +31,30 @@ cd dev/federation-test
```
After the build is finished and the docker-compose setup is running, open [127.0.0.1:8540](http://127.0.0.1:8540) and
[127.0.0.1:8541](http://127.0.0.1:8541) in your browser to use the test instances. You can login as admin with
username `lemmy` and password `lemmy`, or create new accounts.
[127.0.0.1:8550](http://127.0.0.1:8550) in your browser to use the test instances. You can login as admin with
username `lemmy_alpha` and `lemmy_beta` respectively, with password `lemmy`.
Please get in touch if you want to contribute to this, so we can coordinate things and avoid duplicate work.
## Running on a server
Note that federation is currently in alpha. Only use it for testing, not on any production server, and be aware
that you might have to wipe the instance data at one point or another.
Follow the normal installation instructions, either with [Ansible](administration_install_ansible.md) or
[manually](administration_install_docker.md). Then replace the line `image: dessalines/lemmy:v0.x.x` in
`/lemmy/docker-compose.yml` with `image: dessalines/lemmy:federation`. Also add the following in
`/lemmy/lemmy.hjson`:
```
federation: {
enabled: true
allowed_instances: example.com
}
```
Afterwards, and whenver you want to update to the latest version, run these commands on the server:
```
cd /lemmy/
sudo docker-compose pull
sudo docker-compose up -d
```

View file

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

2618
server/Cargo.lock generated vendored

File diff suppressed because it is too large Load diff

25
server/Cargo.toml vendored
View file

@ -8,15 +8,17 @@ edition = "2018"
lto = true
[dependencies]
diesel = { version = "1.4.2", features = ["postgres","chrono", "r2d2", "64-column-tables"] }
diesel = { version = "1.4.4", features = ["postgres","chrono","r2d2","64-column-tables","serde_json"] }
diesel_migrations = "1.4.0"
dotenv = "0.15.0"
bcrypt = "0.7.0"
activitypub = "0.2.0"
chrono = "0.4.7"
activitystreams = "0.6.2"
activitystreams-new = { git = "https://git.asonix.dog/asonix/activitystreams-sketch" }
activitystreams-ext = { git = "https://git.asonix.dog/asonix/activitystreams-ext" }
bcrypt = "0.8.0"
chrono = { version = "0.4.7", features = ["serde"] }
serde_json = { version = "1.0.52", features = ["preserve_order"]}
failure = "0.1.8"
serde_json = "1.0.52"
serde = "1.0.105"
serde = { version = "1.0.105", features = ["derive"] }
actix = "0.9.0"
actix-web = "2.0.0"
actix-files = "0.2.1"
@ -35,9 +37,16 @@ lettre_email = "0.9.4"
sha2 = "0.8.1"
rss = "1.9.0"
htmlescape = "0.3.1"
url = { version = "2.1.1", features = ["serde"] }
config = {version = "0.10.1", default-features = false, features = ["hjson"] }
percent-encoding = "2.1.0"
attohttpc = { version = "0.14.0", default-features = false, features = ["tls-rustls"] }
comrak = "0.7"
tokio = "0.2.20"
futures = "0.3.4"
openssl = "0.10"
http = "0.2.1"
http-signature-normalization = "0.5.1"
base64 = "0.12.1"
tokio = "0.2.21"
futures = "0.3.5"
itertools = "0.9.0"
uuid = { version = "0.8", features = ["serde", "v4"] }

4
server/config/config.hjson vendored Normal file
View file

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

View file

@ -26,7 +26,7 @@
pool_size: 5
}
# the domain name of your instance (eg "dev.lemmy.ml")
hostname: "my_domain"
hostname: null
# address where lemmy should listen for incoming requests
bind: "0.0.0.0"
# port where lemmy should listen for incoming requests
@ -35,9 +35,6 @@
jwt_secret: "changeme"
# The dir for the front end
front_end_dir: "../ui/dist"
# whether to enable activitypub federation. this feature is in alpha, do not enable in production, as might
# cause problems like remote instances fetching and permanently storing bad data.
federation_enabled: false
# rate limits for various user actions, by user ip
rate_limit: {
# maximum number of messages created in interval
@ -53,6 +50,15 @@
# interval length for registration limit
register_per_second: 3600
}
# settings related to activitypub federation
federation: {
# whether to enable activitypub federation. this feature is in alpha, do not enable in production.
enabled: false
# whether tls is required for activitypub. only disable this for debugging, never for producion.
tls_enabled: true
# comma seperated list of instances with which federation is allowed
allowed_instances: ""
}
# # email sending configuration
# email: {
# # hostname of the smtp server

View file

@ -0,0 +1,16 @@
drop table activity;
alter table user_
drop column actor_id,
drop column private_key,
drop column public_key,
drop column bio,
drop column local,
drop column last_refreshed_at;
alter table community
drop column actor_id,
drop column private_key,
drop column public_key,
drop column local,
drop column last_refreshed_at;

View file

@ -0,0 +1,36 @@
-- The Activitypub activity table
-- All user actions must create a row here.
create table activity (
id serial primary key,
user_id int references user_ on update cascade on delete cascade not null, -- Ensures that the user is set up here.
data jsonb not null,
local boolean not null default true,
published timestamp not null default now(),
updated timestamp
);
-- Making sure that id is unique
create unique index idx_activity_unique_apid on activity ((data ->> 'id'::text));
-- Add federation columns to the two actor tables
alter table user_
-- TODO uniqueness constraints should be added on these 3 columns later
add column actor_id character varying(255) not null default 'changeme', -- This needs to be checked and updated in code, building from the site url if local
add column bio text, -- not on community, already has description
add column local boolean not null default true,
add column private_key text, -- These need to be generated from code
add column public_key text,
add column last_refreshed_at timestamp not null default now() -- Used to re-fetch federated actor periodically
;
-- Community
alter table community
add column actor_id character varying(255) not null default 'changeme', -- This needs to be checked and updated in code, building from the site url if local
add column local boolean not null default true,
add column private_key text, -- These need to be generated from code
add column public_key text,
add column last_refreshed_at timestamp not null default now() -- Used to re-fetch federated actor periodically
;
-- Don't worry about rebuilding the views right now.

View file

@ -0,0 +1,7 @@
alter table post
drop column ap_id,
drop column local;
alter table comment
drop column ap_id,
drop column local;

View file

@ -0,0 +1,14 @@
-- Add federation columns to post, comment
alter table post
-- TODO uniqueness constraints should be added on these 3 columns later
add column ap_id character varying(255) not null default 'changeme', -- This needs to be checked and updated in code, building from the site url if local
add column local boolean not null default true
;
alter table comment
-- TODO uniqueness constraints should be added on these 3 columns later
add column ap_id character varying(255) not null default 'changeme', -- This needs to be checked and updated in code, building from the site url if local
add column local boolean not null default true
;

View file

@ -0,0 +1,36 @@
-- User table
drop view user_view cascade;
alter table user_
add column fedi_name varchar(40) not null default 'changeme';
alter table user_
add constraint user__name_fedi_name_key unique (name, fedi_name);
-- Community
alter table community
add constraint community_name_key unique (name);
create view user_view as
select
u.id,
u.name,
u.avatar,
u.email,
u.matrix_user_id,
u.fedi_name,
u.admin,
u.banned,
u.show_avatars,
u.send_notifications_to_email,
u.published,
(select count(*) from post p where p.creator_id = u.id) as number_of_posts,
(select coalesce(sum(score), 0) from post p, post_like pl where u.id = p.creator_id and p.id = pl.post_id) as post_score,
(select count(*) from comment c where c.creator_id = u.id) as number_of_comments,
(select coalesce(sum(score), 0) from comment c, comment_like cl where u.id = c.creator_id and c.id = cl.comment_id) as comment_score
from user_ u;
create materialized view user_mview as select * from user_view;
create unique index idx_user_mview_id on user_mview (id);

View file

@ -0,0 +1,38 @@
-- User table
-- Need to regenerate user_view, user_mview
drop view user_view cascade;
-- Remove the fedi_name constraint, drop that useless column
alter table user_
drop constraint user__name_fedi_name_key;
alter table user_
drop column fedi_name;
-- Community
alter table community
drop constraint community_name_key;
create view user_view as
select
u.id,
u.name,
u.avatar,
u.email,
u.matrix_user_id,
u.admin,
u.banned,
u.show_avatars,
u.send_notifications_to_email,
u.published,
(select count(*) from post p where p.creator_id = u.id) as number_of_posts,
(select coalesce(sum(score), 0) from post p, post_like pl where u.id = p.creator_id and p.id = pl.post_id) as post_score,
(select count(*) from comment c where c.creator_id = u.id) as number_of_comments,
(select coalesce(sum(score), 0) from comment c, comment_like cl where u.id = c.creator_id and c.id = cl.comment_id) as comment_score
from user_ u;
create materialized view user_mview as select * from user_view;
create unique index idx_user_mview_id on user_mview (id);

View file

@ -0,0 +1,440 @@
-- user_view
drop view user_view cascade;
create view user_view as
select
u.id,
u.name,
u.avatar,
u.email,
u.matrix_user_id,
u.admin,
u.banned,
u.show_avatars,
u.send_notifications_to_email,
u.published,
(select count(*) from post p where p.creator_id = u.id) as number_of_posts,
(select coalesce(sum(score), 0) from post p, post_like pl where u.id = p.creator_id and p.id = pl.post_id) as post_score,
(select count(*) from comment c where c.creator_id = u.id) as number_of_comments,
(select coalesce(sum(score), 0) from comment c, comment_like cl where u.id = c.creator_id and c.id = cl.comment_id) as comment_score
from user_ u;
create materialized view user_mview as select * from user_view;
create unique index idx_user_mview_id on user_mview (id);
-- community_view
drop view community_aggregates_view cascade;
create view community_aggregates_view as
select c.*,
(select name from user_ u where c.creator_id = u.id) as creator_name,
(select avatar from user_ u where c.creator_id = u.id) as creator_avatar,
(select name from category ct where c.category_id = ct.id) as category_name,
(select count(*) from community_follower cf where cf.community_id = c.id) as number_of_subscribers,
(select count(*) from post p where p.community_id = c.id) as number_of_posts,
(select count(*) from comment co, post p where c.id = p.community_id and p.id = co.post_id) as number_of_comments,
hot_rank((select count(*) from community_follower cf where cf.community_id = c.id), c.published) as hot_rank
from community c;
create materialized view community_aggregates_mview as select * from community_aggregates_view;
create unique index idx_community_aggregates_mview_id on community_aggregates_mview (id);
create view community_view as
with all_community as
(
select
ca.*
from community_aggregates_view ca
)
select
ac.*,
u.id as user_id,
(select cf.id::boolean from community_follower cf where u.id = cf.user_id and ac.id = cf.community_id) as subscribed
from user_ u
cross join all_community ac
union all
select
ac.*,
null as user_id,
null as subscribed
from all_community ac
;
create view community_mview as
with all_community as
(
select
ca.*
from community_aggregates_mview ca
)
select
ac.*,
u.id as user_id,
(select cf.id::boolean from community_follower cf where u.id = cf.user_id and ac.id = cf.community_id) as subscribed
from user_ u
cross join all_community ac
union all
select
ac.*,
null as user_id,
null as subscribed
from all_community ac
;
-- community views
drop view community_moderator_view;
drop view community_follower_view;
drop view community_user_ban_view;
create view community_moderator_view as
select *,
(select name from user_ u where cm.user_id = u.id) as user_name,
(select avatar from user_ u where cm.user_id = u.id),
(select name from community c where cm.community_id = c.id) as community_name
from community_moderator cm;
create view community_follower_view as
select *,
(select name from user_ u where cf.user_id = u.id) as user_name,
(select avatar from user_ u where cf.user_id = u.id),
(select name from community c where cf.community_id = c.id) as community_name
from community_follower cf;
create view community_user_ban_view as
select *,
(select name from user_ u where cm.user_id = u.id) as user_name,
(select avatar from user_ u where cm.user_id = u.id),
(select name from community c where cm.community_id = c.id) as community_name
from community_user_ban cm;
-- post_view
drop view post_view;
drop view post_mview;
drop materialized view post_aggregates_mview;
drop view post_aggregates_view;
-- regen post view
create view post_aggregates_view as
select
p.*,
(select u.banned from user_ u where p.creator_id = u.id) as banned,
(select cb.id::bool from community_user_ban cb where p.creator_id = cb.user_id and p.community_id = cb.community_id) as banned_from_community,
(select name from user_ where p.creator_id = user_.id) as creator_name,
(select avatar from user_ where p.creator_id = user_.id) as creator_avatar,
(select name from community where p.community_id = community.id) as community_name,
(select removed from community c where p.community_id = c.id) as community_removed,
(select deleted from community c where p.community_id = c.id) as community_deleted,
(select nsfw from community c where p.community_id = c.id) as community_nsfw,
(select count(*) from comment where comment.post_id = p.id) as number_of_comments,
coalesce(sum(pl.score), 0) as score,
count (case when pl.score = 1 then 1 else null end) as upvotes,
count (case when pl.score = -1 then 1 else null end) as downvotes,
hot_rank(coalesce(sum(pl.score) , 0),
(
case when (p.published < ('now'::timestamp - '1 month'::interval)) then p.published -- Prevents necro-bumps
else greatest(c.recent_comment_time, p.published)
end
)
) as hot_rank,
(
case when (p.published < ('now'::timestamp - '1 month'::interval)) then p.published -- Prevents necro-bumps
else greatest(c.recent_comment_time, p.published)
end
) as newest_activity_time
from post p
left join post_like pl on p.id = pl.post_id
left join (
select post_id,
max(published) as recent_comment_time
from comment
group by 1
) c on p.id = c.post_id
group by p.id, c.recent_comment_time;
create materialized view post_aggregates_mview as select * from post_aggregates_view;
create unique index idx_post_aggregates_mview_id on post_aggregates_mview (id);
create view post_view as
with all_post as (
select
pa.*
from post_aggregates_view pa
)
select
ap.*,
u.id as user_id,
coalesce(pl.score, 0) as my_vote,
(select cf.id::bool from community_follower cf where u.id = cf.user_id and cf.community_id = ap.community_id) as subscribed,
(select pr.id::bool from post_read pr where u.id = pr.user_id and pr.post_id = ap.id) as read,
(select ps.id::bool from post_saved ps where u.id = ps.user_id and ps.post_id = ap.id) as saved
from user_ u
cross join all_post ap
left join post_like pl on u.id = pl.user_id and ap.id = pl.post_id
union all
select
ap.*,
null as user_id,
null as my_vote,
null as subscribed,
null as read,
null as saved
from all_post ap
;
create view post_mview as
with all_post as (
select
pa.*
from post_aggregates_mview pa
)
select
ap.*,
u.id as user_id,
coalesce(pl.score, 0) as my_vote,
(select cf.id::bool from community_follower cf where u.id = cf.user_id and cf.community_id = ap.community_id) as subscribed,
(select pr.id::bool from post_read pr where u.id = pr.user_id and pr.post_id = ap.id) as read,
(select ps.id::bool from post_saved ps where u.id = ps.user_id and ps.post_id = ap.id) as saved
from user_ u
cross join all_post ap
left join post_like pl on u.id = pl.user_id and ap.id = pl.post_id
union all
select
ap.*,
null as user_id,
null as my_vote,
null as subscribed,
null as read,
null as saved
from all_post ap
;
-- reply_view, comment_view, user_mention
drop view reply_view;
drop view user_mention_view;
drop view user_mention_mview;
drop view comment_view;
drop view comment_mview;
drop materialized view comment_aggregates_mview;
drop view comment_aggregates_view;
-- reply and comment view
create view comment_aggregates_view as
select
c.*,
(select community_id from post p where p.id = c.post_id),
(select co.name from post p, community co where p.id = c.post_id and p.community_id = co.id) as community_name,
(select u.banned from user_ u where c.creator_id = u.id) as banned,
(select cb.id::bool from community_user_ban cb, post p where c.creator_id = cb.user_id and p.id = c.post_id and p.community_id = cb.community_id) as banned_from_community,
(select name from user_ where c.creator_id = user_.id) as creator_name,
(select avatar from user_ where c.creator_id = user_.id) as creator_avatar,
coalesce(sum(cl.score), 0) as score,
count (case when cl.score = 1 then 1 else null end) as upvotes,
count (case when cl.score = -1 then 1 else null end) as downvotes,
hot_rank(coalesce(sum(cl.score) , 0), c.published) as hot_rank
from comment c
left join comment_like cl on c.id = cl.comment_id
group by c.id;
create materialized view comment_aggregates_mview as select * from comment_aggregates_view;
create unique index idx_comment_aggregates_mview_id on comment_aggregates_mview (id);
create view comment_view as
with all_comment as
(
select
ca.*
from comment_aggregates_view ca
)
select
ac.*,
u.id as user_id,
coalesce(cl.score, 0) as my_vote,
(select cf.id::boolean from community_follower cf where u.id = cf.user_id and ac.community_id = cf.community_id) as subscribed,
(select cs.id::bool from comment_saved cs where u.id = cs.user_id and cs.comment_id = ac.id) as saved
from user_ u
cross join all_comment ac
left join comment_like cl on u.id = cl.user_id and ac.id = cl.comment_id
union all
select
ac.*,
null as user_id,
null as my_vote,
null as subscribed,
null as saved
from all_comment ac
;
create view comment_mview as
with all_comment as
(
select
ca.*
from comment_aggregates_mview ca
)
select
ac.*,
u.id as user_id,
coalesce(cl.score, 0) as my_vote,
(select cf.id::boolean from community_follower cf where u.id = cf.user_id and ac.community_id = cf.community_id) as subscribed,
(select cs.id::bool from comment_saved cs where u.id = cs.user_id and cs.comment_id = ac.id) as saved
from user_ u
cross join all_comment ac
left join comment_like cl on u.id = cl.user_id and ac.id = cl.comment_id
union all
select
ac.*,
null as user_id,
null as my_vote,
null as subscribed,
null as saved
from all_comment ac
;
-- Do the reply_view referencing the comment_mview
create view reply_view as
with closereply as (
select
c2.id,
c2.creator_id as sender_id,
c.creator_id as recipient_id
from comment c
inner join comment c2 on c.id = c2.parent_id
where c2.creator_id != c.creator_id
-- Do union where post is null
union
select
c.id,
c.creator_id as sender_id,
p.creator_id as recipient_id
from comment c, post p
where c.post_id = p.id and c.parent_id is null and c.creator_id != p.creator_id
)
select cv.*,
closereply.recipient_id
from comment_mview cv, closereply
where closereply.id = cv.id
;
-- user mention
create view user_mention_view as
select
c.id,
um.id as user_mention_id,
c.creator_id,
c.post_id,
c.parent_id,
c.content,
c.removed,
um.read,
c.published,
c.updated,
c.deleted,
c.community_id,
c.community_name,
c.banned,
c.banned_from_community,
c.creator_name,
c.creator_avatar,
c.score,
c.upvotes,
c.downvotes,
c.hot_rank,
c.user_id,
c.my_vote,
c.saved,
um.recipient_id
from user_mention um, comment_view c
where um.comment_id = c.id;
create view user_mention_mview as
with all_comment as
(
select
ca.*
from comment_aggregates_mview ca
)
select
ac.id,
um.id as user_mention_id,
ac.creator_id,
ac.post_id,
ac.parent_id,
ac.content,
ac.removed,
um.read,
ac.published,
ac.updated,
ac.deleted,
ac.community_id,
ac.community_name,
ac.banned,
ac.banned_from_community,
ac.creator_name,
ac.creator_avatar,
ac.score,
ac.upvotes,
ac.downvotes,
ac.hot_rank,
u.id as user_id,
coalesce(cl.score, 0) as my_vote,
(select cs.id::bool from comment_saved cs where u.id = cs.user_id and cs.comment_id = ac.id) as saved,
um.recipient_id
from user_ u
cross join all_comment ac
left join comment_like cl on u.id = cl.user_id and ac.id = cl.comment_id
left join user_mention um on um.comment_id = ac.id
union all
select
ac.id,
um.id as user_mention_id,
ac.creator_id,
ac.post_id,
ac.parent_id,
ac.content,
ac.removed,
um.read,
ac.published,
ac.updated,
ac.deleted,
ac.community_id,
ac.community_name,
ac.banned,
ac.banned_from_community,
ac.creator_name,
ac.creator_avatar,
ac.score,
ac.upvotes,
ac.downvotes,
ac.hot_rank,
null as user_id,
null as my_vote,
null as saved,
um.recipient_id
from all_comment ac
left join user_mention um on um.comment_id = ac.id
;

View file

@ -0,0 +1,497 @@
-- user_view
drop view user_view cascade;
create view user_view as
select
u.id,
u.actor_id,
u.name,
u.avatar,
u.email,
u.matrix_user_id,
u.bio,
u.local,
u.admin,
u.banned,
u.show_avatars,
u.send_notifications_to_email,
u.published,
(select count(*) from post p where p.creator_id = u.id) as number_of_posts,
(select coalesce(sum(score), 0) from post p, post_like pl where u.id = p.creator_id and p.id = pl.post_id) as post_score,
(select count(*) from comment c where c.creator_id = u.id) as number_of_comments,
(select coalesce(sum(score), 0) from comment c, comment_like cl where u.id = c.creator_id and c.id = cl.comment_id) as comment_score
from user_ u;
create materialized view user_mview as select * from user_view;
create unique index idx_user_mview_id on user_mview (id);
-- community_view
drop view community_aggregates_view cascade;
create view community_aggregates_view as
-- Now that there's public and private keys, you have to be explicit here
select c.id,
c.name,
c.title,
c.description,
c.category_id,
c.creator_id,
c.removed,
c.published,
c.updated,
c.deleted,
c.nsfw,
c.actor_id,
c.local,
c.last_refreshed_at,
(select actor_id from user_ u where c.creator_id = u.id) as creator_actor_id,
(select local from user_ u where c.creator_id = u.id) as creator_local,
(select name from user_ u where c.creator_id = u.id) as creator_name,
(select avatar from user_ u where c.creator_id = u.id) as creator_avatar,
(select name from category ct where c.category_id = ct.id) as category_name,
(select count(*) from community_follower cf where cf.community_id = c.id) as number_of_subscribers,
(select count(*) from post p where p.community_id = c.id) as number_of_posts,
(select count(*) from comment co, post p where c.id = p.community_id and p.id = co.post_id) as number_of_comments,
hot_rank((select count(*) from community_follower cf where cf.community_id = c.id), c.published) as hot_rank
from community c;
create materialized view community_aggregates_mview as select * from community_aggregates_view;
create unique index idx_community_aggregates_mview_id on community_aggregates_mview (id);
create view community_view as
with all_community as
(
select
ca.*
from community_aggregates_view ca
)
select
ac.*,
u.id as user_id,
(select cf.id::boolean from community_follower cf where u.id = cf.user_id and ac.id = cf.community_id) as subscribed
from user_ u
cross join all_community ac
union all
select
ac.*,
null as user_id,
null as subscribed
from all_community ac
;
create view community_mview as
with all_community as
(
select
ca.*
from community_aggregates_mview ca
)
select
ac.*,
u.id as user_id,
(select cf.id::boolean from community_follower cf where u.id = cf.user_id and ac.id = cf.community_id) as subscribed
from user_ u
cross join all_community ac
union all
select
ac.*,
null as user_id,
null as subscribed
from all_community ac
;
-- community views
drop view community_moderator_view;
drop view community_follower_view;
drop view community_user_ban_view;
create view community_moderator_view as
select *,
(select actor_id from user_ u where cm.user_id = u.id) as user_actor_id,
(select local from user_ u where cm.user_id = u.id) as user_local,
(select name from user_ u where cm.user_id = u.id) as user_name,
(select avatar from user_ u where cm.user_id = u.id),
(select actor_id from community c where cm.community_id = c.id) as community_actor_id,
(select local from community c where cm.community_id = c.id) as community_local,
(select name from community c where cm.community_id = c.id) as community_name
from community_moderator cm;
create view community_follower_view as
select *,
(select actor_id from user_ u where cf.user_id = u.id) as user_actor_id,
(select local from user_ u where cf.user_id = u.id) as user_local,
(select name from user_ u where cf.user_id = u.id) as user_name,
(select avatar from user_ u where cf.user_id = u.id),
(select actor_id from community c where cf.community_id = c.id) as community_actor_id,
(select local from community c where cf.community_id = c.id) as community_local,
(select name from community c where cf.community_id = c.id) as community_name
from community_follower cf;
create view community_user_ban_view as
select *,
(select actor_id from user_ u where cm.user_id = u.id) as user_actor_id,
(select local from user_ u where cm.user_id = u.id) as user_local,
(select name from user_ u where cm.user_id = u.id) as user_name,
(select avatar from user_ u where cm.user_id = u.id),
(select actor_id from community c where cm.community_id = c.id) as community_actor_id,
(select local from community c where cm.community_id = c.id) as community_local,
(select name from community c where cm.community_id = c.id) as community_name
from community_user_ban cm;
-- post_view
drop view post_view;
drop view post_mview;
drop materialized view post_aggregates_mview;
drop view post_aggregates_view;
-- regen post view
create view post_aggregates_view as
select
p.*,
(select u.banned from user_ u where p.creator_id = u.id) as banned,
(select cb.id::bool from community_user_ban cb where p.creator_id = cb.user_id and p.community_id = cb.community_id) as banned_from_community,
(select actor_id from user_ where p.creator_id = user_.id) as creator_actor_id,
(select local from user_ where p.creator_id = user_.id) as creator_local,
(select name from user_ where p.creator_id = user_.id) as creator_name,
(select avatar from user_ where p.creator_id = user_.id) as creator_avatar,
(select actor_id from community where p.community_id = community.id) as community_actor_id,
(select local from community where p.community_id = community.id) as community_local,
(select name from community where p.community_id = community.id) as community_name,
(select removed from community c where p.community_id = c.id) as community_removed,
(select deleted from community c where p.community_id = c.id) as community_deleted,
(select nsfw from community c where p.community_id = c.id) as community_nsfw,
(select count(*) from comment where comment.post_id = p.id) as number_of_comments,
coalesce(sum(pl.score), 0) as score,
count (case when pl.score = 1 then 1 else null end) as upvotes,
count (case when pl.score = -1 then 1 else null end) as downvotes,
hot_rank(coalesce(sum(pl.score) , 0),
(
case when (p.published < ('now'::timestamp - '1 month'::interval)) then p.published -- Prevents necro-bumps
else greatest(c.recent_comment_time, p.published)
end
)
) as hot_rank,
(
case when (p.published < ('now'::timestamp - '1 month'::interval)) then p.published -- Prevents necro-bumps
else greatest(c.recent_comment_time, p.published)
end
) as newest_activity_time
from post p
left join post_like pl on p.id = pl.post_id
left join (
select post_id,
max(published) as recent_comment_time
from comment
group by 1
) c on p.id = c.post_id
group by p.id, c.recent_comment_time;
create materialized view post_aggregates_mview as select * from post_aggregates_view;
create unique index idx_post_aggregates_mview_id on post_aggregates_mview (id);
create view post_view as
with all_post as (
select
pa.*
from post_aggregates_view pa
)
select
ap.*,
u.id as user_id,
coalesce(pl.score, 0) as my_vote,
(select cf.id::bool from community_follower cf where u.id = cf.user_id and cf.community_id = ap.community_id) as subscribed,
(select pr.id::bool from post_read pr where u.id = pr.user_id and pr.post_id = ap.id) as read,
(select ps.id::bool from post_saved ps where u.id = ps.user_id and ps.post_id = ap.id) as saved
from user_ u
cross join all_post ap
left join post_like pl on u.id = pl.user_id and ap.id = pl.post_id
union all
select
ap.*,
null as user_id,
null as my_vote,
null as subscribed,
null as read,
null as saved
from all_post ap
;
create view post_mview as
with all_post as (
select
pa.*
from post_aggregates_mview pa
)
select
ap.*,
u.id as user_id,
coalesce(pl.score, 0) as my_vote,
(select cf.id::bool from community_follower cf where u.id = cf.user_id and cf.community_id = ap.community_id) as subscribed,
(select pr.id::bool from post_read pr where u.id = pr.user_id and pr.post_id = ap.id) as read,
(select ps.id::bool from post_saved ps where u.id = ps.user_id and ps.post_id = ap.id) as saved
from user_ u
cross join all_post ap
left join post_like pl on u.id = pl.user_id and ap.id = pl.post_id
union all
select
ap.*,
null as user_id,
null as my_vote,
null as subscribed,
null as read,
null as saved
from all_post ap
;
-- reply_view, comment_view, user_mention
drop view reply_view;
drop view user_mention_view;
drop view user_mention_mview;
drop view comment_view;
drop view comment_mview;
drop materialized view comment_aggregates_mview;
drop view comment_aggregates_view;
-- reply and comment view
create view comment_aggregates_view as
select
c.*,
(select community_id from post p where p.id = c.post_id),
(select co.actor_id from post p, community co where p.id = c.post_id and p.community_id = co.id) as community_actor_id,
(select co.local from post p, community co where p.id = c.post_id and p.community_id = co.id) as community_local,
(select co.name from post p, community co where p.id = c.post_id and p.community_id = co.id) as community_name,
(select u.banned from user_ u where c.creator_id = u.id) as banned,
(select cb.id::bool from community_user_ban cb, post p where c.creator_id = cb.user_id and p.id = c.post_id and p.community_id = cb.community_id) as banned_from_community,
(select actor_id from user_ where c.creator_id = user_.id) as creator_actor_id,
(select local from user_ where c.creator_id = user_.id) as creator_local,
(select name from user_ where c.creator_id = user_.id) as creator_name,
(select avatar from user_ where c.creator_id = user_.id) as creator_avatar,
coalesce(sum(cl.score), 0) as score,
count (case when cl.score = 1 then 1 else null end) as upvotes,
count (case when cl.score = -1 then 1 else null end) as downvotes,
hot_rank(coalesce(sum(cl.score) , 0), c.published) as hot_rank
from comment c
left join comment_like cl on c.id = cl.comment_id
group by c.id;
create materialized view comment_aggregates_mview as select * from comment_aggregates_view;
create unique index idx_comment_aggregates_mview_id on comment_aggregates_mview (id);
create view comment_view as
with all_comment as
(
select
ca.*
from comment_aggregates_view ca
)
select
ac.*,
u.id as user_id,
coalesce(cl.score, 0) as my_vote,
(select cf.id::boolean from community_follower cf where u.id = cf.user_id and ac.community_id = cf.community_id) as subscribed,
(select cs.id::bool from comment_saved cs where u.id = cs.user_id and cs.comment_id = ac.id) as saved
from user_ u
cross join all_comment ac
left join comment_like cl on u.id = cl.user_id and ac.id = cl.comment_id
union all
select
ac.*,
null as user_id,
null as my_vote,
null as subscribed,
null as saved
from all_comment ac
;
create view comment_mview as
with all_comment as
(
select
ca.*
from comment_aggregates_mview ca
)
select
ac.*,
u.id as user_id,
coalesce(cl.score, 0) as my_vote,
(select cf.id::boolean from community_follower cf where u.id = cf.user_id and ac.community_id = cf.community_id) as subscribed,
(select cs.id::bool from comment_saved cs where u.id = cs.user_id and cs.comment_id = ac.id) as saved
from user_ u
cross join all_comment ac
left join comment_like cl on u.id = cl.user_id and ac.id = cl.comment_id
union all
select
ac.*,
null as user_id,
null as my_vote,
null as subscribed,
null as saved
from all_comment ac
;
-- Do the reply_view referencing the comment_mview
create view reply_view as
with closereply as (
select
c2.id,
c2.creator_id as sender_id,
c.creator_id as recipient_id
from comment c
inner join comment c2 on c.id = c2.parent_id
where c2.creator_id != c.creator_id
-- Do union where post is null
union
select
c.id,
c.creator_id as sender_id,
p.creator_id as recipient_id
from comment c, post p
where c.post_id = p.id and c.parent_id is null and c.creator_id != p.creator_id
)
select cv.*,
closereply.recipient_id
from comment_mview cv, closereply
where closereply.id = cv.id
;
-- user mention
create view user_mention_view as
select
c.id,
um.id as user_mention_id,
c.creator_id,
c.creator_actor_id,
c.creator_local,
c.post_id,
c.parent_id,
c.content,
c.removed,
um.read,
c.published,
c.updated,
c.deleted,
c.community_id,
c.community_actor_id,
c.community_local,
c.community_name,
c.banned,
c.banned_from_community,
c.creator_name,
c.creator_avatar,
c.score,
c.upvotes,
c.downvotes,
c.hot_rank,
c.user_id,
c.my_vote,
c.saved,
um.recipient_id,
(select actor_id from user_ u where u.id = um.recipient_id) as recipient_actor_id,
(select local from user_ u where u.id = um.recipient_id) as recipient_local
from user_mention um, comment_view c
where um.comment_id = c.id;
create view user_mention_mview as
with all_comment as
(
select
ca.*
from comment_aggregates_mview ca
)
select
ac.id,
um.id as user_mention_id,
ac.creator_id,
ac.creator_actor_id,
ac.creator_local,
ac.post_id,
ac.parent_id,
ac.content,
ac.removed,
um.read,
ac.published,
ac.updated,
ac.deleted,
ac.community_id,
ac.community_actor_id,
ac.community_local,
ac.community_name,
ac.banned,
ac.banned_from_community,
ac.creator_name,
ac.creator_avatar,
ac.score,
ac.upvotes,
ac.downvotes,
ac.hot_rank,
u.id as user_id,
coalesce(cl.score, 0) as my_vote,
(select cs.id::bool from comment_saved cs where u.id = cs.user_id and cs.comment_id = ac.id) as saved,
um.recipient_id,
(select actor_id from user_ u where u.id = um.recipient_id) as recipient_actor_id,
(select local from user_ u where u.id = um.recipient_id) as recipient_local
from user_ u
cross join all_comment ac
left join comment_like cl on u.id = cl.user_id and ac.id = cl.comment_id
left join user_mention um on um.comment_id = ac.id
union all
select
ac.id,
um.id as user_mention_id,
ac.creator_id,
ac.creator_actor_id,
ac.creator_local,
ac.post_id,
ac.parent_id,
ac.content,
ac.removed,
um.read,
ac.published,
ac.updated,
ac.deleted,
ac.community_id,
ac.community_actor_id,
ac.community_local,
ac.community_name,
ac.banned,
ac.banned_from_community,
ac.creator_name,
ac.creator_avatar,
ac.score,
ac.upvotes,
ac.downvotes,
ac.hot_rank,
null as user_id,
null as my_vote,
null as saved,
um.recipient_id,
(select actor_id from user_ u where u.id = um.recipient_id) as recipient_actor_id,
(select local from user_ u where u.id = um.recipient_id) as recipient_local
from all_comment ac
left join user_mention um on um.comment_id = ac.id
;

View file

@ -0,0 +1,4 @@
-- The username index
drop index idx_user_name_lower_actor_id;
create unique index idx_user_name_lower on user_ (lower(name));

View file

@ -0,0 +1,2 @@
drop index idx_user_name_lower;
create unique index idx_user_name_lower_actor_id on user_ (lower(name), lower(actor_id));

View file

@ -0,0 +1,21 @@
drop materialized view private_message_mview;
drop view private_message_view;
alter table private_message
drop column ap_id,
drop column local;
create view private_message_view as
select
pm.*,
u.name as creator_name,
u.avatar as creator_avatar,
u2.name as recipient_name,
u2.avatar as recipient_avatar
from private_message pm
inner join user_ u on u.id = pm.creator_id
inner join user_ u2 on u2.id = pm.recipient_id;
create materialized view private_message_mview as select * from private_message_view;
create unique index idx_private_message_mview_id on private_message_mview (id);

View file

@ -0,0 +1,25 @@
alter table private_message
add column ap_id character varying(255) not null default 'changeme', -- This needs to be checked and updated in code, building from the site url if local
add column local boolean not null default true
;
drop materialized view private_message_mview;
drop view private_message_view;
create view private_message_view as
select
pm.*,
u.name as creator_name,
u.avatar as creator_avatar,
u.actor_id as creator_actor_id,
u.local as creator_local,
u2.name as recipient_name,
u2.avatar as recipient_avatar,
u2.actor_id as recipient_actor_id,
u2.local as recipient_local
from private_message pm
inner join user_ u on u.id = pm.creator_id
inner join user_ u2 on u2.id = pm.recipient_id;
create materialized view private_message_mview as select * from private_message_view;
create unique index idx_private_message_mview_id on private_message_mview (id);

View file

@ -1,4 +1,42 @@
use super::*;
use crate::{
api::{APIError, Oper, Perform},
apub::{ApubLikeableType, ApubObjectType},
db::{
comment::*,
comment_view::*,
community_view::*,
moderator::*,
post::*,
site_view::*,
user::*,
user_mention::*,
user_view::*,
Crud,
Likeable,
ListingType,
Saveable,
SortType,
},
naive_now,
remove_slurs,
scrape_text_for_mentions,
send_email,
settings::Settings,
websocket::{
server::{JoinCommunityRoom, SendComment},
UserOperation,
WebsocketInfo,
},
MentionData,
};
use diesel::{
r2d2::{ConnectionManager, Pool},
PgConnection,
};
use failure::Error;
use log::error;
use serde::{Deserialize, Serialize};
use std::str::FromStr;
#[derive(Serialize, Deserialize)]
pub struct CreateComment {
@ -76,8 +114,6 @@ impl Perform for Oper<CreateComment> {
let user_id = claims.id;
let hostname = &format!("https://{}", Settings::get().hostname);
let conn = pool.get()?;
// Check for a community ban
@ -87,7 +123,8 @@ impl Perform for Oper<CreateComment> {
}
// Check for a site ban
if UserView::read(&conn, user_id)?.banned {
let user = User_::read(&conn, user_id)?;
if user.banned {
return Err(APIError::err("site_ban").into());
}
@ -101,7 +138,10 @@ impl Perform for Oper<CreateComment> {
removed: None,
deleted: None,
read: None,
published: None,
updated: None,
ap_id: "changeme".into(),
local: true,
};
let inserted_comment = match Comment::create(&conn, &comment_form) {
@ -109,107 +149,16 @@ impl Perform for Oper<CreateComment> {
Err(_e) => return Err(APIError::err("couldnt_create_comment").into()),
};
let mut recipient_ids = Vec::new();
let updated_comment = match Comment::update_ap_id(&conn, inserted_comment.id) {
Ok(comment) => comment,
Err(_e) => return Err(APIError::err("couldnt_create_comment").into()),
};
updated_comment.send_create(&user, &conn)?;
// Scan the comment for user mentions, add those rows
let extracted_usernames = extract_usernames(&comment_form.content);
for username_mention in &extracted_usernames {
if let Ok(mention_user) = User_::read_from_name(&conn, (*username_mention).to_string()) {
// You can't mention yourself
// At some point, make it so you can't tag the parent creator either
// This can cause two notifications, one for reply and the other for mention
if mention_user.id != user_id {
recipient_ids.push(mention_user.id);
let user_mention_form = UserMentionForm {
recipient_id: mention_user.id,
comment_id: inserted_comment.id,
read: None,
};
// Allow this to fail softly, since comment edits might re-update or replace it
// Let the uniqueness handle this fail
match UserMention::create(&conn, &user_mention_form) {
Ok(_mention) => (),
Err(_e) => error!("{}", &_e),
};
// Send an email to those users that have notifications on
if mention_user.send_notifications_to_email {
if let Some(mention_email) = mention_user.email {
let subject = &format!(
"{} - Mentioned by {}",
Settings::get().hostname,
claims.username
);
let html = &format!(
"<h1>User Mention</h1><br><div>{} - {}</div><br><a href={}/inbox>inbox</a>",
claims.username, comment_form.content, hostname
);
match send_email(subject, &mention_email, &mention_user.name, html) {
Ok(_o) => _o,
Err(e) => error!("{}", e),
};
}
}
}
}
}
// Send notifs to the parent commenter / poster
match data.parent_id {
Some(parent_id) => {
let parent_comment = Comment::read(&conn, parent_id)?;
if parent_comment.creator_id != user_id {
let parent_user = User_::read(&conn, parent_comment.creator_id)?;
recipient_ids.push(parent_user.id);
if parent_user.send_notifications_to_email {
if let Some(comment_reply_email) = parent_user.email {
let subject = &format!(
"{} - Reply from {}",
Settings::get().hostname,
claims.username
);
let html = &format!(
"<h1>Comment Reply</h1><br><div>{} - {}</div><br><a href={}/inbox>inbox</a>",
claims.username, comment_form.content, hostname
);
match send_email(subject, &comment_reply_email, &parent_user.name, html) {
Ok(_o) => _o,
Err(e) => error!("{}", e),
};
}
}
}
}
// Its a post
None => {
if post.creator_id != user_id {
let parent_user = User_::read(&conn, post.creator_id)?;
recipient_ids.push(parent_user.id);
if parent_user.send_notifications_to_email {
if let Some(post_reply_email) = parent_user.email {
let subject = &format!(
"{} - Reply from {}",
Settings::get().hostname,
claims.username
);
let html = &format!(
"<h1>Post Reply</h1><br><div>{} - {}</div><br><a href={}/inbox>inbox</a>",
claims.username, comment_form.content, hostname
);
match send_email(subject, &post_reply_email, &parent_user.name, html) {
Ok(_o) => _o,
Err(e) => error!("{}", e),
};
}
}
}
}
};
let mentions = scrape_text_for_mentions(&comment_form.content);
let recipient_ids = send_local_notifs(&conn, &mentions, &updated_comment, &user, &post);
// You like your own comment by default
let like_form = CommentLikeForm {
@ -224,6 +173,8 @@ impl Perform for Oper<CreateComment> {
Err(_e) => return Err(APIError::err("couldnt_like_comment").into()),
};
updated_comment.send_like(&user, &conn)?;
let comment_view = CommentView::read(&conn, inserted_comment.id, Some(user_id))?;
let mut res = CommentResponse {
@ -266,6 +217,8 @@ impl Perform for Oper<EditComment> {
let conn = pool.get()?;
let user = User_::read(&conn, user_id)?;
let orig_comment = CommentView::read(&conn, data.edit_id, None)?;
// You are allowed to mark the comment as read even if you're banned.
@ -290,13 +243,15 @@ impl Perform for Oper<EditComment> {
}
// Check for a site ban
if UserView::read(&conn, user_id)?.banned {
if user.banned {
return Err(APIError::err("site_ban").into());
}
}
let content_slurs_removed = remove_slurs(&data.content.to_owned());
let read_comment = Comment::read(&conn, data.edit_id)?;
let comment_form = CommentForm {
content: content_slurs_removed,
parent_id: data.parent_id,
@ -305,65 +260,41 @@ impl Perform for Oper<EditComment> {
removed: data.removed.to_owned(),
deleted: data.deleted.to_owned(),
read: data.read.to_owned(),
published: None,
updated: if data.read.is_some() {
orig_comment.updated
} else {
Some(naive_now())
},
ap_id: read_comment.ap_id,
local: read_comment.local,
};
let _updated_comment = match Comment::update(&conn, data.edit_id, &comment_form) {
let updated_comment = match Comment::update(&conn, data.edit_id, &comment_form) {
Ok(comment) => comment,
Err(_e) => return Err(APIError::err("couldnt_update_comment").into()),
};
let mut recipient_ids = Vec::new();
// Scan the comment for user mentions, add those rows
let extracted_usernames = extract_usernames(&comment_form.content);
for username_mention in &extracted_usernames {
let mention_user = User_::read_from_name(&conn, (*username_mention).to_string());
if mention_user.is_ok() {
let mention_user_id = mention_user?.id;
// You can't mention yourself
// At some point, make it so you can't tag the parent creator either
// This can cause two notifications, one for reply and the other for mention
if mention_user_id != user_id {
recipient_ids.push(mention_user_id);
let user_mention_form = UserMentionForm {
recipient_id: mention_user_id,
comment_id: data.edit_id,
read: None,
};
// Allow this to fail softly, since comment edits might re-update or replace it
// Let the uniqueness handle this fail
match UserMention::create(&conn, &user_mention_form) {
Ok(_mention) => (),
Err(_e) => error!("{}", &_e),
}
}
if let Some(deleted) = data.deleted.to_owned() {
if deleted {
updated_comment.send_delete(&user, &conn)?;
} else {
updated_comment.send_undo_delete(&user, &conn)?;
}
} else if let Some(removed) = data.removed.to_owned() {
if removed {
updated_comment.send_remove(&user, &conn)?;
} else {
updated_comment.send_undo_remove(&user, &conn)?;
}
} else {
updated_comment.send_update(&user, &conn)?;
}
// Add to recipient ids
match data.parent_id {
Some(parent_id) => {
let parent_comment = Comment::read(&conn, parent_id)?;
if parent_comment.creator_id != user_id {
let parent_user = User_::read(&conn, parent_comment.creator_id)?;
recipient_ids.push(parent_user.id);
}
}
None => {
let post = Post::read(&conn, data.post_id)?;
recipient_ids.push(post.creator_id);
}
}
let post = Post::read(&conn, data.post_id)?;
let mentions = scrape_text_for_mentions(&comment_form.content);
let recipient_ids = send_local_notifs(&conn, &mentions, &updated_comment, &user, &post);
// Mod tables
if let Some(removed) = data.removed.to_owned() {
@ -480,7 +411,8 @@ impl Perform for Oper<CreateCommentLike> {
}
// Check for a site ban
if UserView::read(&conn, user_id)?.banned {
let user = User_::read(&conn, user_id)?;
if user.banned {
return Err(APIError::err("site_ban").into());
}
@ -517,6 +449,14 @@ impl Perform for Oper<CreateCommentLike> {
Ok(like) => like,
Err(_e) => return Err(APIError::err("couldnt_like_comment").into()),
};
if like_form.score == 1 {
comment.send_like(&user, &conn)?;
} else if like_form.score == -1 {
comment.send_dislike(&user, &conn)?;
}
} else {
comment.send_undo_like(&user, &conn)?;
}
// Have to refetch the comment to get the current state
@ -601,3 +541,106 @@ impl Perform for Oper<GetComments> {
Ok(GetCommentsResponse { comments })
}
}
pub fn send_local_notifs(
conn: &PgConnection,
mentions: &[MentionData],
comment: &Comment,
user: &User_,
post: &Post,
) -> Vec<i32> {
let mut recipient_ids = Vec::new();
let hostname = &format!("https://{}", Settings::get().hostname);
// Send the local mentions
for mention in mentions
.iter()
.filter(|m| m.is_local() && m.name.ne(&user.name))
.collect::<Vec<&MentionData>>()
{
if let Ok(mention_user) = User_::read_from_name(&conn, &mention.name) {
// TODO
// At some point, make it so you can't tag the parent creator either
// This can cause two notifications, one for reply and the other for mention
recipient_ids.push(mention_user.id);
let user_mention_form = UserMentionForm {
recipient_id: mention_user.id,
comment_id: comment.id,
read: None,
};
// Allow this to fail softly, since comment edits might re-update or replace it
// Let the uniqueness handle this fail
match UserMention::create(&conn, &user_mention_form) {
Ok(_mention) => (),
Err(_e) => error!("{}", &_e),
};
// Send an email to those users that have notifications on
if mention_user.send_notifications_to_email {
if let Some(mention_email) = mention_user.email {
let subject = &format!("{} - Mentioned by {}", Settings::get().hostname, user.name,);
let html = &format!(
"<h1>User Mention</h1><br><div>{} - {}</div><br><a href={}/inbox>inbox</a>",
user.name, comment.content, hostname
);
match send_email(subject, &mention_email, &mention_user.name, html) {
Ok(_o) => _o,
Err(e) => error!("{}", e),
};
}
}
}
}
// Send notifs to the parent commenter / poster
match comment.parent_id {
Some(parent_id) => {
if let Ok(parent_comment) = Comment::read(&conn, parent_id) {
if parent_comment.creator_id != user.id {
if let Ok(parent_user) = User_::read(&conn, parent_comment.creator_id) {
recipient_ids.push(parent_user.id);
if parent_user.send_notifications_to_email {
if let Some(comment_reply_email) = parent_user.email {
let subject = &format!("{} - Reply from {}", Settings::get().hostname, user.name,);
let html = &format!(
"<h1>Comment Reply</h1><br><div>{} - {}</div><br><a href={}/inbox>inbox</a>",
user.name, comment.content, hostname
);
match send_email(subject, &comment_reply_email, &parent_user.name, html) {
Ok(_o) => _o,
Err(e) => error!("{}", e),
};
}
}
}
}
}
}
// Its a post
None => {
if post.creator_id != user.id {
if let Ok(parent_user) = User_::read(&conn, post.creator_id) {
recipient_ids.push(parent_user.id);
if parent_user.send_notifications_to_email {
if let Some(post_reply_email) = parent_user.email {
let subject = &format!("{} - Reply from {}", Settings::get().hostname, user.name,);
let html = &format!(
"<h1>Post Reply</h1><br><div>{} - {}</div><br><a href={}/inbox>inbox</a>",
user.name, comment.content, hostname
);
match send_email(subject, &post_reply_email, &parent_user.name, html) {
Ok(_o) => _o,
Err(e) => error!("{}", e),
};
}
}
}
}
}
};
recipient_ids
}

View file

@ -1,18 +1,44 @@
use super::*;
use crate::is_valid_community_name;
use crate::{
api::{APIError, Oper, Perform},
apub::{
extensions::signatures::generate_actor_keypair,
make_apub_endpoint,
ActorType,
EndpointType,
},
db::{Bannable, Crud, Followable, Joinable, SortType},
is_valid_community_name,
naive_from_unix,
naive_now,
slur_check,
slurs_vec_to_str,
websocket::{
server::{JoinCommunityRoom, SendCommunityRoomMessage},
UserOperation,
WebsocketInfo,
},
};
use diesel::{
r2d2::{ConnectionManager, Pool},
PgConnection,
};
use failure::Error;
use serde::{Deserialize, Serialize};
use std::str::FromStr;
#[derive(Serialize, Deserialize)]
pub struct GetCommunity {
id: Option<i32>,
name: Option<String>,
pub name: Option<String>,
auth: Option<String>,
}
#[derive(Serialize, Deserialize)]
pub struct GetCommunityResponse {
pub community: CommunityView,
moderators: Vec<CommunityModeratorView>,
admins: Vec<UserView>,
pub moderators: Vec<CommunityModeratorView>,
pub admins: Vec<UserView>,
pub online: usize,
}
@ -31,17 +57,17 @@ pub struct CommunityResponse {
pub community: CommunityView,
}
#[derive(Serialize, Deserialize)]
#[derive(Serialize, Deserialize, Debug)]
pub struct ListCommunities {
sort: String,
page: Option<i64>,
limit: Option<i64>,
auth: Option<String>,
pub sort: String,
pub page: Option<i64>,
pub limit: Option<i64>,
pub auth: Option<String>,
}
#[derive(Serialize, Deserialize)]
#[derive(Serialize, Deserialize, Debug)]
pub struct ListCommunitiesResponse {
communities: Vec<CommunityView>,
pub communities: Vec<CommunityView>,
}
#[derive(Serialize, Deserialize, Clone)]
@ -135,25 +161,25 @@ impl Perform for Oper<GetCommunity> {
let conn = pool.get()?;
let community_id = match data.id {
Some(id) => id,
let community = match data.id {
Some(id) => Community::read(&conn, id)?,
None => {
match Community::read_from_name(
&conn,
data.name.to_owned().unwrap_or_else(|| "main".to_string()),
&data.name.to_owned().unwrap_or_else(|| "main".to_string()),
) {
Ok(community) => community.id,
Ok(community) => community,
Err(_e) => return Err(APIError::err("couldnt_find_community").into()),
}
}
};
let community_view = match CommunityView::read(&conn, community_id, user_id) {
let community_view = match CommunityView::read(&conn, community.id, user_id) {
Ok(community) => community,
Err(_e) => return Err(APIError::err("couldnt_find_community").into()),
};
let moderators = match CommunityModeratorView::for_community(&conn, community_id) {
let moderators = match CommunityModeratorView::for_community(&conn, community.id) {
Ok(moderators) => moderators,
Err(_e) => return Err(APIError::err("couldnt_find_community").into()),
};
@ -166,8 +192,10 @@ impl Perform for Oper<GetCommunity> {
let online = if let Some(ws) = websocket_info {
if let Some(id) = ws.id {
ws.chatserver
.do_send(JoinCommunityRoom { community_id, id });
ws.chatserver.do_send(JoinCommunityRoom {
community_id: community.id,
id,
});
}
// TODO
@ -235,6 +263,8 @@ impl Perform for Oper<CreateCommunity> {
}
// When you create a community, make sure the user becomes a moderator and a follower
let keypair = generate_actor_keypair()?;
let community_form = CommunityForm {
name: data.name.to_owned(),
title: data.title.to_owned(),
@ -245,6 +275,12 @@ impl Perform for Oper<CreateCommunity> {
deleted: None,
nsfw: data.nsfw,
updated: None,
actor_id: make_apub_endpoint(EndpointType::Community, &data.name).to_string(),
local: true,
private_key: Some(keypair.private_key),
public_key: Some(keypair.public_key),
last_refreshed_at: None,
published: None,
};
let inserted_community = match Community::create(&conn, &community_form) {
@ -320,7 +356,8 @@ impl Perform for Oper<EditCommunity> {
let conn = pool.get()?;
// Check for a site ban
if UserView::read(&conn, user_id)?.banned {
let user = User_::read(&conn, user_id)?;
if user.banned {
return Err(APIError::err("site_ban").into());
}
@ -337,6 +374,8 @@ impl Perform for Oper<EditCommunity> {
return Err(APIError::err("no_community_edit_allowed").into());
}
let read_community = Community::read(&conn, data.edit_id)?;
let community_form = CommunityForm {
name: data.name.to_owned(),
title: data.title.to_owned(),
@ -347,9 +386,15 @@ impl Perform for Oper<EditCommunity> {
deleted: data.deleted.to_owned(),
nsfw: data.nsfw,
updated: Some(naive_now()),
actor_id: read_community.actor_id,
local: read_community.local,
private_key: read_community.private_key,
public_key: read_community.public_key,
last_refreshed_at: None,
published: None,
};
let _updated_community = match Community::update(&conn, data.edit_id, &community_form) {
let updated_community = match Community::update(&conn, data.edit_id, &community_form) {
Ok(community) => community,
Err(_e) => return Err(APIError::err("couldnt_update_community").into()),
};
@ -370,6 +415,20 @@ impl Perform for Oper<EditCommunity> {
ModRemoveCommunity::create(&conn, &form)?;
}
if let Some(deleted) = data.deleted.to_owned() {
if deleted {
updated_community.send_delete(&user, &conn)?;
} else {
updated_community.send_undo_delete(&user, &conn)?;
}
} else if let Some(removed) = data.removed.to_owned() {
if removed {
updated_community.send_remove(&user, &conn)?;
} else {
updated_community.send_undo_remove(&user, &conn)?;
}
}
let community_view = CommunityView::read(&conn, data.edit_id, Some(user_id))?;
let res = CommunityResponse {
@ -456,23 +515,41 @@ impl Perform for Oper<FollowCommunity> {
let user_id = claims.id;
let conn = pool.get()?;
let community = Community::read(&conn, data.community_id)?;
let community_follower_form = CommunityFollowerForm {
community_id: data.community_id,
user_id,
};
let conn = pool.get()?;
if data.follow {
match CommunityFollower::follow(&conn, &community_follower_form) {
Ok(user) => user,
Err(_e) => return Err(APIError::err("community_follower_already_exists").into()),
};
if community.local {
if data.follow {
match CommunityFollower::follow(&conn, &community_follower_form) {
Ok(user) => user,
Err(_e) => return Err(APIError::err("community_follower_already_exists").into()),
};
} else {
match CommunityFollower::unfollow(&conn, &community_follower_form) {
Ok(user) => user,
Err(_e) => return Err(APIError::err("community_follower_already_exists").into()),
};
}
} else {
match CommunityFollower::ignore(&conn, &community_follower_form) {
Ok(user) => user,
Err(_e) => return Err(APIError::err("community_follower_already_exists").into()),
};
let user = User_::read(&conn, user_id)?;
if data.follow {
// Dont actually add to the community followers here, because you need
// to wait for the accept
user.send_follow(&community.actor_id, &conn)?;
} else {
user.send_unfollow(&community.actor_id, &conn)?;
match CommunityFollower::unfollow(&conn, &community_follower_form) {
Ok(user) => user,
Err(_e) => return Err(APIError::err("community_follower_already_exists").into()),
};
}
// TODO: this needs to return a "pending" state, until Accept is received from the remote server
}
let community_view = CommunityView::read(&conn, data.community_id, Some(user_id))?;
@ -684,11 +761,17 @@ impl Perform for Oper<TransferCommunity> {
title: read_community.title,
description: read_community.description,
category_id: read_community.category_id,
creator_id: data.user_id,
creator_id: data.user_id, // This makes the new user the community creator
removed: None,
deleted: None,
nsfw: read_community.nsfw,
updated: Some(naive_now()),
actor_id: read_community.actor_id,
local: read_community.local,
private_key: read_community.private_key,
public_key: read_community.public_key,
last_refreshed_at: None,
published: None,
};
let _updated_community = match Community::update(&conn, data.community_id, &community_form) {

View file

@ -1,42 +1,12 @@
use crate::db::category::*;
use crate::db::comment::*;
use crate::db::comment_view::*;
use crate::db::community::*;
use crate::db::community_view::*;
use crate::db::moderator::*;
use crate::db::moderator_views::*;
use crate::db::password_reset_request::*;
use crate::db::post::*;
use crate::db::post_view::*;
use crate::db::private_message::*;
use crate::db::private_message_view::*;
use crate::db::site::*;
use crate::db::site_view::*;
use crate::db::user::*;
use crate::db::user_mention::*;
use crate::db::user_mention_view::*;
use crate::db::user_view::*;
use crate::db::*;
use crate::{
extract_usernames, fetch_iframely_and_pictrs_data, generate_random_string, naive_from_unix,
naive_now, remove_slurs, send_email, slur_check, slurs_vec_to_str,
db::{community::*, community_view::*, moderator::*, site::*, user::*, user_view::*},
websocket::WebsocketInfo,
};
use crate::settings::Settings;
use crate::websocket::UserOperation;
use crate::websocket::{
server::{
JoinCommunityRoom, JoinPostRoom, JoinUserRoom, SendAllMessage, SendComment,
SendCommunityRoomMessage, SendPost, SendUserRoomMessage,
},
WebsocketInfo,
use diesel::{
r2d2::{ConnectionManager, Pool},
PgConnection,
};
use diesel::r2d2::{ConnectionManager, Pool};
use diesel::PgConnection;
use failure::Error;
use log::{error, info};
use serde::{Deserialize, Serialize};
use std::str::FromStr;
pub mod comment;
pub mod community;
@ -62,8 +32,8 @@ pub struct Oper<T> {
data: T,
}
impl<T> Oper<T> {
pub fn new(data: T) -> Oper<T> {
impl<Data> Oper<Data> {
pub fn new(data: Data) -> Oper<Data> {
Oper { data }
}
}

View file

@ -1,6 +1,41 @@
use super::*;
use crate::{
api::{APIError, Oper, Perform},
apub::{ApubLikeableType, ApubObjectType},
db::{
comment_view::*,
community_view::*,
moderator::*,
post::*,
post_view::*,
site::*,
site_view::*,
user::*,
user_view::*,
Crud,
Likeable,
ListingType,
Saveable,
SortType,
},
fetch_iframely_and_pictrs_data,
naive_now,
slur_check,
slurs_vec_to_str,
websocket::{
server::{JoinCommunityRoom, JoinPostRoom, SendPost},
UserOperation,
WebsocketInfo,
},
};
use diesel::{
r2d2::{ConnectionManager, Pool},
PgConnection,
};
use failure::Error;
use serde::{Deserialize, Serialize};
use std::str::FromStr;
#[derive(Serialize, Deserialize)]
#[derive(Serialize, Deserialize, Debug)]
pub struct CreatePost {
name: String,
url: Option<String>,
@ -31,7 +66,7 @@ pub struct GetPostResponse {
pub online: usize,
}
#[derive(Serialize, Deserialize)]
#[derive(Serialize, Deserialize, Debug)]
pub struct GetPosts {
type_: String,
sort: String,
@ -41,9 +76,9 @@ pub struct GetPosts {
auth: Option<String>,
}
#[derive(Serialize, Deserialize)]
#[derive(Serialize, Deserialize, Debug)]
pub struct GetPostsResponse {
posts: Vec<PostView>,
pub posts: Vec<PostView>,
}
#[derive(Serialize, Deserialize)]
@ -112,7 +147,8 @@ impl Perform for Oper<CreatePost> {
}
// Check for a site ban
if UserView::read(&conn, user_id)?.banned {
let user = User_::read(&conn, user_id)?;
if user.banned {
return Err(APIError::err("site_ban").into());
}
@ -136,6 +172,9 @@ impl Perform for Oper<CreatePost> {
embed_description: iframely_description,
embed_html: iframely_html,
thumbnail_url: pictrs_thumbnail,
ap_id: "changeme".into(),
local: true,
published: None,
};
let inserted_post = match Post::create(&conn, &post_form) {
@ -151,6 +190,13 @@ impl Perform for Oper<CreatePost> {
}
};
let updated_post = match Post::update_ap_id(&conn, inserted_post.id) {
Ok(post) => post,
Err(_e) => return Err(APIError::err("couldnt_create_post").into()),
};
updated_post.send_create(&user, &conn)?;
// They like their own post by default
let like_form = PostLikeForm {
post_id: inserted_post.id,
@ -158,12 +204,13 @@ impl Perform for Oper<CreatePost> {
score: 1,
};
// Only add the like if the score isnt 0
let _inserted_like = match PostLike::like(&conn, &like_form) {
Ok(like) => like,
Err(_e) => return Err(APIError::err("couldnt_like_post").into()),
};
updated_post.send_like(&user, &conn)?;
// Refetch the view
let post_view = match PostView::read(&conn, inserted_post.id, Some(user_id)) {
Ok(post) => post,
@ -357,7 +404,8 @@ impl Perform for Oper<CreatePostLike> {
}
// Check for a site ban
if UserView::read(&conn, user_id)?.banned {
let user = User_::read(&conn, user_id)?;
if user.banned {
return Err(APIError::err("site_ban").into());
}
@ -377,6 +425,14 @@ impl Perform for Oper<CreatePostLike> {
Ok(like) => like,
Err(_e) => return Err(APIError::err("couldnt_like_post").into()),
};
if like_form.score == 1 {
post.send_like(&user, &conn)?;
} else if like_form.score == -1 {
post.send_dislike(&user, &conn)?;
}
} else {
post.send_undo_like(&user, &conn)?;
}
let post_view = match PostView::read(&conn, data.post_id, Some(user_id)) {
@ -446,7 +502,8 @@ impl Perform for Oper<EditPost> {
}
// Check for a site ban
if UserView::read(&conn, user_id)?.banned {
let user = User_::read(&conn, user_id)?;
if user.banned {
return Err(APIError::err("site_ban").into());
}
@ -454,6 +511,8 @@ impl Perform for Oper<EditPost> {
let (iframely_title, iframely_description, iframely_html, pictrs_thumbnail) =
fetch_iframely_and_pictrs_data(data.url.to_owned());
let read_post = Post::read(&conn, data.edit_id)?;
let post_form = PostForm {
name: data.name.to_owned(),
url: data.url.to_owned(),
@ -470,9 +529,12 @@ impl Perform for Oper<EditPost> {
embed_description: iframely_description,
embed_html: iframely_html,
thumbnail_url: pictrs_thumbnail,
ap_id: read_post.ap_id,
local: read_post.local,
published: None,
};
let _updated_post = match Post::update(&conn, data.edit_id, &post_form) {
let updated_post = match Post::update(&conn, data.edit_id, &post_form) {
Ok(post) => post,
Err(e) => {
let err_type = if e.to_string() == "value too long for type character varying(200)" {
@ -514,6 +576,22 @@ impl Perform for Oper<EditPost> {
ModStickyPost::create(&conn, &form)?;
}
if let Some(deleted) = data.deleted.to_owned() {
if deleted {
updated_post.send_delete(&user, &conn)?;
} else {
updated_post.send_undo_delete(&user, &conn)?;
}
} else if let Some(removed) = data.removed.to_owned() {
if removed {
updated_post.send_remove(&user, &conn)?;
} else {
updated_post.send_undo_remove(&user, &conn)?;
}
} else {
updated_post.send_update(&user, &conn)?;
}
let post_view = PostView::read(&conn, data.edit_id, Some(user_id))?;
let res = PostResponse { post: post_view };

View file

@ -1,5 +1,36 @@
use super::user::Register;
use super::*;
use crate::{
api::{APIError, Oper, Perform},
apub::fetcher::search_by_apub_id,
db::{
category::*,
comment_view::*,
community_view::*,
moderator::*,
moderator_views::*,
post_view::*,
site::*,
site_view::*,
user::*,
user_view::*,
Crud,
SearchType,
SortType,
},
naive_now,
settings::Settings,
slur_check,
slurs_vec_to_str,
websocket::{server::SendAllMessage, UserOperation, WebsocketInfo},
};
use diesel::{
r2d2::{ConnectionManager, Pool},
PgConnection,
};
use failure::Error;
use log::{debug, info};
use serde::{Deserialize, Serialize};
use std::str::FromStr;
#[derive(Serialize, Deserialize)]
pub struct ListCategories {}
@ -9,7 +40,7 @@ pub struct ListCategoriesResponse {
categories: Vec<Category>,
}
#[derive(Serialize, Deserialize)]
#[derive(Serialize, Deserialize, Debug)]
pub struct Search {
q: String,
type_: String,
@ -20,13 +51,13 @@ pub struct Search {
auth: Option<String>,
}
#[derive(Serialize, Deserialize)]
#[derive(Serialize, Deserialize, Debug)]
pub struct SearchResponse {
type_: String,
comments: Vec<CommentView>,
posts: Vec<PostView>,
communities: Vec<CommunityView>,
users: Vec<UserView>,
pub type_: String,
pub comments: Vec<CommentView>,
pub posts: Vec<PostView>,
pub communities: Vec<CommunityView>,
pub users: Vec<UserView>,
}
#[derive(Serialize, Deserialize)]
@ -342,8 +373,7 @@ impl Perform for Oper<GetSite> {
let conn = pool.get()?;
// TODO refactor this a little
let site = Site::read(&conn, 1);
let site_view = if site.is_ok() {
let site_view = if let Ok(_site) = Site::read(&conn, 1) {
Some(SiteView::read(&conn)?)
} else if let Some(setup) = Settings::get().setup.as_ref() {
let register = Register {
@ -360,9 +390,9 @@ impl Perform for Oper<GetSite> {
let create_site = CreateSite {
name: setup.site_name.to_owned(),
description: None,
enable_downvotes: false,
open_registration: false,
enable_nsfw: false,
enable_downvotes: true,
open_registration: true,
enable_nsfw: true,
auth: login_response.jwt,
};
Oper::new(create_site).perform(pool, websocket_info.clone())?;
@ -373,11 +403,16 @@ impl Perform for Oper<GetSite> {
};
let mut admins = UserView::admins(&conn)?;
if site_view.is_some() {
let site_creator_id = site_view.to_owned().unwrap().creator_id;
let creator_index = admins.iter().position(|r| r.id == site_creator_id).unwrap();
let creator_user = admins.remove(creator_index);
admins.insert(0, creator_user);
// Make sure the site creator is the top admin
if let Some(site_view) = site_view.to_owned() {
let site_creator_id = site_view.creator_id;
// TODO investigate why this is sometimes coming back null
// Maybe user_.admin isn't being set to true?
if let Some(creator_index) = admins.iter().position(|r| r.id == site_creator_id) {
let creator_user = admins.remove(creator_index);
admins.insert(0, creator_user);
}
}
let banned = UserView::banned(&conn)?;
@ -412,6 +447,15 @@ impl Perform for Oper<Search> {
) -> Result<SearchResponse, Error> {
let data: &Search = &self.data;
dbg!(&data);
let conn = pool.get()?;
match search_by_apub_id(&data.q, &conn) {
Ok(r) => return Ok(r),
Err(e) => debug!("Failed to resolve search query as activitypub ID: {}", e),
}
let user_id: Option<i32> = match &data.auth {
Some(auth) => match Claims::decode(&auth) {
Ok(claims) => {
@ -433,8 +477,6 @@ impl Perform for Oper<Search> {
// TODO no clean / non-nsfw searching rn
let conn = pool.get()?;
match type_ {
SearchType::Posts => {
posts = PostQueryBuilder::create(&conn)

View file

@ -1,6 +1,58 @@
use super::*;
use crate::is_valid_username;
use crate::{
api::{APIError, Oper, Perform},
apub::{
extensions::signatures::generate_actor_keypair,
make_apub_endpoint,
ApubObjectType,
EndpointType,
},
db::{
comment::*,
comment_view::*,
community::*,
community_view::*,
moderator::*,
password_reset_request::*,
post::*,
post_view::*,
private_message::*,
private_message_view::*,
site::*,
site_view::*,
user::*,
user_mention::*,
user_mention_view::*,
user_view::*,
Crud,
Followable,
Joinable,
ListingType,
SortType,
},
generate_random_string,
is_valid_username,
naive_from_unix,
naive_now,
remove_slurs,
send_email,
settings::Settings,
slur_check,
slurs_vec_to_str,
websocket::{
server::{JoinUserRoom, SendAllMessage, SendUserRoomMessage},
UserOperation,
WebsocketInfo,
},
};
use bcrypt::verify;
use diesel::{
r2d2::{ConnectionManager, Pool},
PgConnection,
};
use failure::Error;
use log::error;
use serde::{Deserialize, Serialize};
use std::str::FromStr;
#[derive(Serialize, Deserialize, Debug)]
pub struct Login {
@ -187,7 +239,7 @@ pub struct PrivateMessagesResponse {
#[derive(Serialize, Deserialize, Clone)]
pub struct PrivateMessageResponse {
message: PrivateMessageView,
pub message: PrivateMessageView,
}
#[derive(Serialize, Deserialize, Debug)]
@ -262,6 +314,7 @@ impl Perform for Oper<Register> {
return Err(APIError::err("admin_already_created").into());
}
let user_keypair = generate_actor_keypair()?;
if !is_valid_username(&data.username) {
return Err(APIError::err("invalid_username").into());
}
@ -269,7 +322,6 @@ impl Perform for Oper<Register> {
// Register the new user
let user_form = UserForm {
name: data.username.to_owned(),
fedi_name: Settings::get().hostname,
email: data.email.to_owned(),
matrix_user_id: None,
avatar: None,
@ -285,6 +337,12 @@ impl Perform for Oper<Register> {
lang: "browser".into(),
show_avatars: true,
send_notifications_to_email: false,
actor_id: make_apub_endpoint(EndpointType::User, &data.username).to_string(),
bio: None,
local: true,
private_key: Some(user_keypair.private_key),
public_key: Some(user_keypair.public_key),
last_refreshed_at: None,
};
// Create the user
@ -303,12 +361,15 @@ impl Perform for Oper<Register> {
}
};
let main_community_keypair = generate_actor_keypair()?;
// Create the main community if it doesn't exist
let main_community: Community = match Community::read(&conn, 2) {
Ok(c) => c,
Err(_e) => {
let default_community_name = "main";
let community_form = CommunityForm {
name: "main".to_string(),
name: default_community_name.to_string(),
title: "The Default Community".to_string(),
description: Some("The Default Community".to_string()),
category_id: 1,
@ -317,6 +378,12 @@ impl Perform for Oper<Register> {
removed: None,
deleted: None,
updated: None,
actor_id: make_apub_endpoint(EndpointType::Community, default_community_name).to_string(),
local: true,
private_key: Some(main_community_keypair.private_key),
public_key: Some(main_community_keypair.public_key),
last_refreshed_at: None,
published: None,
};
Community::create(&conn, &community_form).unwrap()
}
@ -411,7 +478,6 @@ impl Perform for Oper<SaveUserSettings> {
let user_form = UserForm {
name: read_user.name,
fedi_name: read_user.fedi_name,
email,
matrix_user_id: data.matrix_user_id.to_owned(),
avatar: data.avatar.to_owned(),
@ -427,6 +493,12 @@ impl Perform for Oper<SaveUserSettings> {
lang: data.lang.to_owned(),
show_avatars: data.show_avatars,
send_notifications_to_email: data.send_notifications_to_email,
actor_id: read_user.actor_id,
bio: read_user.bio,
local: read_user.local,
private_key: read_user.private_key,
public_key: read_user.public_key,
last_refreshed_at: None,
};
let updated_user = match User_::update(&conn, user_id, &user_form) {
@ -488,7 +560,7 @@ impl Perform for Oper<GetUserDetails> {
None => {
match User_::read_from_name(
&conn,
data
&data
.username
.to_owned()
.unwrap_or_else(|| "admin".to_string()),
@ -580,30 +652,7 @@ impl Perform for Oper<AddAdmin> {
return Err(APIError::err("not_an_admin").into());
}
let read_user = User_::read(&conn, data.user_id)?;
// TODO make addadmin easier
let user_form = UserForm {
name: read_user.name,
fedi_name: read_user.fedi_name,
email: read_user.email,
matrix_user_id: read_user.matrix_user_id,
avatar: read_user.avatar,
password_encrypted: read_user.password_encrypted,
preferred_username: read_user.preferred_username,
updated: Some(naive_now()),
admin: data.added,
banned: read_user.banned,
show_nsfw: read_user.show_nsfw,
theme: read_user.theme,
default_sort_type: read_user.default_sort_type,
default_listing_type: read_user.default_listing_type,
lang: read_user.lang,
show_avatars: read_user.show_avatars,
send_notifications_to_email: read_user.send_notifications_to_email,
};
match User_::update(&conn, data.user_id, &user_form) {
match User_::add_admin(&conn, user_id, data.added) {
Ok(user) => user,
Err(_e) => return Err(APIError::err("couldnt_update_user").into()),
};
@ -661,30 +710,7 @@ impl Perform for Oper<BanUser> {
return Err(APIError::err("not_an_admin").into());
}
let read_user = User_::read(&conn, data.user_id)?;
// TODO make bans and addadmins easier
let user_form = UserForm {
name: read_user.name,
fedi_name: read_user.fedi_name,
email: read_user.email,
matrix_user_id: read_user.matrix_user_id,
avatar: read_user.avatar,
password_encrypted: read_user.password_encrypted,
preferred_username: read_user.preferred_username,
updated: Some(naive_now()),
admin: read_user.admin,
banned: data.ban,
show_nsfw: read_user.show_nsfw,
theme: read_user.theme,
default_sort_type: read_user.default_sort_type,
default_listing_type: read_user.default_listing_type,
lang: read_user.lang,
show_avatars: read_user.show_avatars,
send_notifications_to_email: read_user.send_notifications_to_email,
};
match User_::update(&conn, data.user_id, &user_form) {
match User_::ban_user(&conn, user_id, data.ban) {
Ok(user) => user,
Err(_e) => return Err(APIError::err("couldnt_update_user").into()),
};
@ -855,18 +881,7 @@ impl Perform for Oper<MarkAllAsRead> {
.list()?;
for reply in &replies {
let comment_form = CommentForm {
content: reply.to_owned().content,
parent_id: reply.to_owned().parent_id,
post_id: reply.to_owned().post_id,
creator_id: reply.to_owned().creator_id,
removed: None,
deleted: None,
read: Some(true),
updated: reply.to_owned().updated,
};
let _updated_comment = match Comment::update(&conn, reply.id, &comment_form) {
match Comment::mark_as_read(&conn, reply.id) {
Ok(comment) => comment,
Err(_e) => return Err(APIError::err("couldnt_update_comment").into()),
};
@ -902,12 +917,15 @@ impl Perform for Oper<MarkAllAsRead> {
for message in &messages {
let private_message_form = PrivateMessageForm {
content: None,
content: message.to_owned().content,
creator_id: message.to_owned().creator_id,
recipient_id: message.to_owned().recipient_id,
deleted: None,
read: Some(true),
updated: None,
ap_id: message.to_owned().ap_id,
local: message.local,
published: None,
};
let _updated_message = match PrivateMessage::update(&conn, message.id, &private_message_form)
@ -955,18 +973,7 @@ impl Perform for Oper<DeleteAccount> {
.list()?;
for comment in &comments {
let comment_form = CommentForm {
content: "*Permananently Deleted*".to_string(),
parent_id: comment.to_owned().parent_id,
post_id: comment.to_owned().post_id,
creator_id: comment.to_owned().creator_id,
removed: None,
deleted: Some(true),
read: None,
updated: Some(naive_now()),
};
let _updated_comment = match Comment::update(&conn, comment.id, &comment_form) {
let _updated_comment = match Comment::permadelete(&conn, comment.id) {
Ok(comment) => comment,
Err(_e) => return Err(APIError::err("couldnt_update_comment").into()),
};
@ -980,25 +987,7 @@ impl Perform for Oper<DeleteAccount> {
.list()?;
for post in &posts {
let post_form = PostForm {
name: "*Permananently Deleted*".to_string(),
url: Some("https://deleted.com".to_string()),
body: Some("*Permananently Deleted*".to_string()),
creator_id: post.to_owned().creator_id,
community_id: post.to_owned().community_id,
removed: None,
deleted: Some(true),
nsfw: post.to_owned().nsfw,
locked: None,
stickied: None,
updated: Some(naive_now()),
embed_title: None,
embed_description: None,
embed_html: None,
thumbnail_url: None,
};
let _updated_post = match Post::update(&conn, post.id, &post_form) {
let _updated_post = match Post::permadelete(&conn, post.id) {
Ok(post) => post,
Err(_e) => return Err(APIError::err("couldnt_update_post").into()),
};
@ -1104,19 +1093,23 @@ impl Perform for Oper<CreatePrivateMessage> {
let conn = pool.get()?;
// Check for a site ban
if UserView::read(&conn, user_id)?.banned {
let user = User_::read(&conn, user_id)?;
if user.banned {
return Err(APIError::err("site_ban").into());
}
let content_slurs_removed = remove_slurs(&data.content.to_owned());
let private_message_form = PrivateMessageForm {
content: Some(content_slurs_removed.to_owned()),
content: content_slurs_removed.to_owned(),
creator_id: user_id,
recipient_id: data.recipient_id,
deleted: None,
read: None,
updated: None,
ap_id: "changeme".into(),
local: true,
published: None,
};
let inserted_private_message = match PrivateMessage::create(&conn, &private_message_form) {
@ -1126,6 +1119,14 @@ impl Perform for Oper<CreatePrivateMessage> {
}
};
let updated_private_message =
match PrivateMessage::update_ap_id(&conn, inserted_private_message.id) {
Ok(private_message) => private_message,
Err(_e) => return Err(APIError::err("couldnt_create_private_message").into()),
};
updated_private_message.send_create(&user, &conn)?;
// Send notifications to the recipient
let recipient_user = User_::read(&conn, data.recipient_id)?;
if recipient_user.send_notifications_to_email {
@ -1169,7 +1170,7 @@ impl Perform for Oper<EditPrivateMessage> {
fn perform(
&self,
pool: Pool<ConnectionManager<PgConnection>>,
_websocket_info: Option<WebsocketInfo>,
websocket_info: Option<WebsocketInfo>,
) -> Result<PrivateMessageResponse, Error> {
let data: &EditPrivateMessage = &self.data;
@ -1185,7 +1186,8 @@ impl Perform for Oper<EditPrivateMessage> {
let orig_private_message = PrivateMessage::read(&conn, data.edit_id)?;
// Check for a site ban
if UserView::read(&conn, user_id)?.banned {
let user = User_::read(&conn, user_id)?;
if user.banned {
return Err(APIError::err("site_ban").into());
}
@ -1197,8 +1199,8 @@ impl Perform for Oper<EditPrivateMessage> {
}
let content_slurs_removed = match &data.content {
Some(content) => Some(remove_slurs(content)),
None => None,
Some(content) => remove_slurs(content),
None => orig_private_message.content,
};
let private_message_form = PrivateMessageForm {
@ -1212,17 +1214,41 @@ impl Perform for Oper<EditPrivateMessage> {
} else {
Some(naive_now())
},
ap_id: orig_private_message.ap_id,
local: orig_private_message.local,
published: None,
};
let _updated_private_message =
let updated_private_message =
match PrivateMessage::update(&conn, data.edit_id, &private_message_form) {
Ok(private_message) => private_message,
Err(_e) => return Err(APIError::err("couldnt_update_private_message").into()),
};
if let Some(deleted) = data.deleted.to_owned() {
if deleted {
updated_private_message.send_delete(&user, &conn)?;
} else {
updated_private_message.send_undo_delete(&user, &conn)?;
}
} else {
updated_private_message.send_update(&user, &conn)?;
}
let message = PrivateMessageView::read(&conn, data.edit_id)?;
Ok(PrivateMessageResponse { message })
let res = PrivateMessageResponse { message };
if let Some(ws) = websocket_info {
ws.chatserver.do_send(SendUserRoomMessage {
op: UserOperation::EditPrivateMessage,
response: res.clone(),
recipient_id: orig_private_message.recipient_id,
my_id: ws.id,
});
}
Ok(res)
}
}

View file

@ -0,0 +1,74 @@
use crate::{
apub::{extensions::signatures::sign, is_apub_id_valid, ActorType},
db::{activity::insert_activity, community::Community, user::User_},
};
use activitystreams::{context, object::properties::ObjectProperties, public, Activity, Base};
use diesel::PgConnection;
use failure::{Error, _core::fmt::Debug};
use log::debug;
use serde::Serialize;
use url::Url;
pub fn populate_object_props(
props: &mut ObjectProperties,
addressed_ccs: Vec<String>,
object_id: &str,
) -> Result<(), Error> {
props
.set_context_xsd_any_uri(context())?
// TODO: the activity needs a seperate id from the object
.set_id(object_id)?
// TODO: should to/cc go on the Create, or on the Post? or on both?
// TODO: handle privacy on the receiving side (at least ignore anything thats not public)
.set_to_xsd_any_uri(public())?
.set_many_cc_xsd_any_uris(addressed_ccs)?;
Ok(())
}
pub fn send_activity_to_community<A>(
creator: &User_,
conn: &PgConnection,
community: &Community,
to: Vec<String>,
activity: A,
) -> Result<(), Error>
where
A: Activity + Base + Serialize + Debug,
{
insert_activity(&conn, creator.id, &activity, true)?;
// if this is a local community, we need to do an announce from the community instead
if community.local {
Community::do_announce(activity, &community, creator, conn)?;
} else {
send_activity(&activity, creator, to)?;
}
Ok(())
}
/// Send an activity to a list of recipients, using the correct headers etc.
pub fn send_activity<A>(activity: &A, actor: &dyn ActorType, to: Vec<String>) -> Result<(), Error>
where
A: Serialize + Debug,
{
let json = serde_json::to_string(&activity)?;
debug!("Sending activitypub activity {} to {:?}", json, to);
for t in to {
let to_url = Url::parse(&t)?;
if !is_apub_id_valid(&to_url) {
debug!("Not sending activity to {} (invalid or blacklisted)", t);
continue;
}
let mut request = attohttpc::post(t).header("Host", to_url.domain().unwrap());
let signature = sign(&mut request, actor)?;
let res = request
.header("Signature", signature)
.header("Content-Type", "application/json")
.text(json.to_owned())
.send()?
.text()?;
debug!("Result for activity send: {:?}", res);
}
Ok(())
}

510
server/src/apub/comment.rs Normal file
View file

@ -0,0 +1,510 @@
use crate::{
apub::{
activities::{populate_object_props, send_activity_to_community},
create_apub_response,
create_apub_tombstone_response,
create_tombstone,
fetch_webfinger_url,
fetcher::{
get_or_fetch_and_insert_remote_comment,
get_or_fetch_and_insert_remote_post,
get_or_fetch_and_upsert_remote_user,
},
ActorType,
ApubLikeableType,
ApubObjectType,
FromApub,
ToApub,
},
convert_datetime,
db::{
comment::{Comment, CommentForm},
community::Community,
post::Post,
user::User_,
Crud,
},
routes::DbPoolParam,
scrape_text_for_mentions,
MentionData,
};
use activitystreams::{
activity::{Create, Delete, Dislike, Like, Remove, Undo, Update},
context,
link::Mention,
object::{kind::NoteType, properties::ObjectProperties, Note},
};
use activitystreams_new::object::Tombstone;
use actix_web::{body::Body, web::Path, HttpResponse, Result};
use diesel::PgConnection;
use failure::Error;
use itertools::Itertools;
use log::debug;
use serde::Deserialize;
#[derive(Deserialize)]
pub struct CommentQuery {
comment_id: String,
}
/// Return the post json over HTTP.
pub async fn get_apub_comment(
info: Path<CommentQuery>,
db: DbPoolParam,
) -> Result<HttpResponse<Body>, Error> {
let id = info.comment_id.parse::<i32>()?;
let comment = Comment::read(&&db.get()?, id)?;
if !comment.deleted {
Ok(create_apub_response(&comment.to_apub(&db.get().unwrap())?))
} else {
Ok(create_apub_tombstone_response(&comment.to_tombstone()?))
}
}
impl ToApub for Comment {
type Response = Note;
fn to_apub(&self, conn: &PgConnection) -> Result<Note, Error> {
let mut comment = Note::default();
let oprops: &mut ObjectProperties = comment.as_mut();
let creator = User_::read(&conn, self.creator_id)?;
let post = Post::read(&conn, self.post_id)?;
let community = Community::read(&conn, post.community_id)?;
// Add a vector containing some important info to the "in_reply_to" field
// [post_ap_id, Option(parent_comment_ap_id)]
let mut in_reply_to_vec = vec![post.ap_id];
if let Some(parent_id) = self.parent_id {
let parent_comment = Comment::read(&conn, parent_id)?;
in_reply_to_vec.push(parent_comment.ap_id);
}
oprops
// Not needed when the Post is embedded in a collection (like for community outbox)
.set_context_xsd_any_uri(context())?
.set_id(self.ap_id.to_owned())?
.set_published(convert_datetime(self.published))?
.set_to_xsd_any_uri(community.actor_id)?
.set_many_in_reply_to_xsd_any_uris(in_reply_to_vec)?
.set_content_xsd_string(self.content.to_owned())?
.set_attributed_to_xsd_any_uri(creator.actor_id)?;
if let Some(u) = self.updated {
oprops.set_updated(convert_datetime(u))?;
}
Ok(comment)
}
fn to_tombstone(&self) -> Result<Tombstone, Error> {
create_tombstone(
self.deleted,
&self.ap_id,
self.updated,
NoteType.to_string(),
)
}
}
impl FromApub for CommentForm {
type ApubType = Note;
/// Parse an ActivityPub note received from another instance into a Lemmy comment
fn from_apub(note: &Note, conn: &PgConnection) -> Result<CommentForm, Error> {
let oprops = &note.object_props;
let creator_actor_id = &oprops.get_attributed_to_xsd_any_uri().unwrap().to_string();
let creator = get_or_fetch_and_upsert_remote_user(&creator_actor_id, &conn)?;
let mut in_reply_tos = oprops.get_many_in_reply_to_xsd_any_uris().unwrap();
let post_ap_id = in_reply_tos.next().unwrap().to_string();
// This post, or the parent comment might not yet exist on this server yet, fetch them.
let post = get_or_fetch_and_insert_remote_post(&post_ap_id, &conn)?;
// The 2nd item, if it exists, is the parent comment apub_id
// For deeply nested comments, FromApub automatically gets called recursively
let parent_id: Option<i32> = match in_reply_tos.next() {
Some(parent_comment_uri) => {
let parent_comment_ap_id = &parent_comment_uri.to_string();
let parent_comment = get_or_fetch_and_insert_remote_comment(&parent_comment_ap_id, &conn)?;
Some(parent_comment.id)
}
None => None,
};
Ok(CommentForm {
creator_id: creator.id,
post_id: post.id,
parent_id,
content: oprops
.get_content_xsd_string()
.map(|c| c.to_string())
.unwrap(),
removed: None,
read: None,
published: oprops
.get_published()
.map(|u| u.as_ref().to_owned().naive_local()),
updated: oprops
.get_updated()
.map(|u| u.as_ref().to_owned().naive_local()),
deleted: None,
ap_id: oprops.get_id().unwrap().to_string(),
local: false,
})
}
}
impl ApubObjectType for Comment {
/// Send out information about a newly created comment, to the followers of the community.
fn send_create(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error> {
let note = self.to_apub(conn)?;
let post = Post::read(&conn, self.post_id)?;
let community = Community::read(conn, post.community_id)?;
let id = format!("{}/create/{}", self.ap_id, uuid::Uuid::new_v4());
let maa: MentionsAndAddresses =
collect_non_local_mentions_and_addresses(&conn, &self.content, &community)?;
let mut create = Create::new();
populate_object_props(&mut create.object_props, maa.addressed_ccs, &id)?;
// Set the mention tags
create.object_props.set_many_tag_base_boxes(maa.tags)?;
create
.create_props
.set_actor_xsd_any_uri(creator.actor_id.to_owned())?
.set_object_base_box(note)?;
send_activity_to_community(&creator, &conn, &community, maa.inboxes, create)?;
Ok(())
}
/// Send out information about an edited post, to the followers of the community.
fn send_update(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error> {
let note = self.to_apub(&conn)?;
let post = Post::read(&conn, self.post_id)?;
let community = Community::read(&conn, post.community_id)?;
let id = format!("{}/update/{}", self.ap_id, uuid::Uuid::new_v4());
let maa: MentionsAndAddresses =
collect_non_local_mentions_and_addresses(&conn, &self.content, &community)?;
let mut update = Update::new();
populate_object_props(&mut update.object_props, maa.addressed_ccs, &id)?;
// Set the mention tags
update.object_props.set_many_tag_base_boxes(maa.tags)?;
update
.update_props
.set_actor_xsd_any_uri(creator.actor_id.to_owned())?
.set_object_base_box(note)?;
send_activity_to_community(&creator, &conn, &community, maa.inboxes, update)?;
Ok(())
}
fn send_delete(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error> {
let note = self.to_apub(&conn)?;
let post = Post::read(&conn, self.post_id)?;
let community = Community::read(&conn, post.community_id)?;
let id = format!("{}/delete/{}", self.ap_id, uuid::Uuid::new_v4());
let mut delete = Delete::default();
populate_object_props(
&mut delete.object_props,
vec![community.get_followers_url()],
&id,
)?;
delete
.delete_props
.set_actor_xsd_any_uri(creator.actor_id.to_owned())?
.set_object_base_box(note)?;
send_activity_to_community(
&creator,
&conn,
&community,
vec![community.get_shared_inbox_url()],
delete,
)?;
Ok(())
}
fn send_undo_delete(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error> {
let note = self.to_apub(&conn)?;
let post = Post::read(&conn, self.post_id)?;
let community = Community::read(&conn, post.community_id)?;
// Generate a fake delete activity, with the correct object
let id = format!("{}/delete/{}", self.ap_id, uuid::Uuid::new_v4());
let mut delete = Delete::default();
populate_object_props(
&mut delete.object_props,
vec![community.get_followers_url()],
&id,
)?;
delete
.delete_props
.set_actor_xsd_any_uri(creator.actor_id.to_owned())?
.set_object_base_box(note)?;
// TODO
// Undo that fake activity
let undo_id = format!("{}/undo/delete/{}", self.ap_id, uuid::Uuid::new_v4());
let mut undo = Undo::default();
populate_object_props(
&mut undo.object_props,
vec![community.get_followers_url()],
&undo_id,
)?;
undo
.undo_props
.set_actor_xsd_any_uri(creator.actor_id.to_owned())?
.set_object_base_box(delete)?;
send_activity_to_community(
&creator,
&conn,
&community,
vec![community.get_shared_inbox_url()],
undo,
)?;
Ok(())
}
fn send_remove(&self, mod_: &User_, conn: &PgConnection) -> Result<(), Error> {
let note = self.to_apub(&conn)?;
let post = Post::read(&conn, self.post_id)?;
let community = Community::read(&conn, post.community_id)?;
let id = format!("{}/remove/{}", self.ap_id, uuid::Uuid::new_v4());
let mut remove = Remove::default();
populate_object_props(
&mut remove.object_props,
vec![community.get_followers_url()],
&id,
)?;
remove
.remove_props
.set_actor_xsd_any_uri(mod_.actor_id.to_owned())?
.set_object_base_box(note)?;
send_activity_to_community(
&mod_,
&conn,
&community,
vec![community.get_shared_inbox_url()],
remove,
)?;
Ok(())
}
fn send_undo_remove(&self, mod_: &User_, conn: &PgConnection) -> Result<(), Error> {
let note = self.to_apub(&conn)?;
let post = Post::read(&conn, self.post_id)?;
let community = Community::read(&conn, post.community_id)?;
// Generate a fake delete activity, with the correct object
let id = format!("{}/remove/{}", self.ap_id, uuid::Uuid::new_v4());
let mut remove = Remove::default();
populate_object_props(
&mut remove.object_props,
vec![community.get_followers_url()],
&id,
)?;
remove
.remove_props
.set_actor_xsd_any_uri(mod_.actor_id.to_owned())?
.set_object_base_box(note)?;
// Undo that fake activity
let undo_id = format!("{}/undo/remove/{}", self.ap_id, uuid::Uuid::new_v4());
let mut undo = Undo::default();
populate_object_props(
&mut undo.object_props,
vec![community.get_followers_url()],
&undo_id,
)?;
undo
.undo_props
.set_actor_xsd_any_uri(mod_.actor_id.to_owned())?
.set_object_base_box(remove)?;
send_activity_to_community(
&mod_,
&conn,
&community,
vec![community.get_shared_inbox_url()],
undo,
)?;
Ok(())
}
}
impl ApubLikeableType for Comment {
fn send_like(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error> {
let note = self.to_apub(&conn)?;
let post = Post::read(&conn, self.post_id)?;
let community = Community::read(&conn, post.community_id)?;
let id = format!("{}/like/{}", self.ap_id, uuid::Uuid::new_v4());
let mut like = Like::new();
populate_object_props(
&mut like.object_props,
vec![community.get_followers_url()],
&id,
)?;
like
.like_props
.set_actor_xsd_any_uri(creator.actor_id.to_owned())?
.set_object_base_box(note)?;
send_activity_to_community(
&creator,
&conn,
&community,
vec![community.get_shared_inbox_url()],
like,
)?;
Ok(())
}
fn send_dislike(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error> {
let note = self.to_apub(&conn)?;
let post = Post::read(&conn, self.post_id)?;
let community = Community::read(&conn, post.community_id)?;
let id = format!("{}/dislike/{}", self.ap_id, uuid::Uuid::new_v4());
let mut dislike = Dislike::new();
populate_object_props(
&mut dislike.object_props,
vec![community.get_followers_url()],
&id,
)?;
dislike
.dislike_props
.set_actor_xsd_any_uri(creator.actor_id.to_owned())?
.set_object_base_box(note)?;
send_activity_to_community(
&creator,
&conn,
&community,
vec![community.get_shared_inbox_url()],
dislike,
)?;
Ok(())
}
fn send_undo_like(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error> {
let note = self.to_apub(&conn)?;
let post = Post::read(&conn, self.post_id)?;
let community = Community::read(&conn, post.community_id)?;
let id = format!("{}/dislike/{}", self.ap_id, uuid::Uuid::new_v4());
let mut like = Like::new();
populate_object_props(
&mut like.object_props,
vec![community.get_followers_url()],
&id,
)?;
like
.like_props
.set_actor_xsd_any_uri(creator.actor_id.to_owned())?
.set_object_base_box(note)?;
// TODO
// Undo that fake activity
let undo_id = format!("{}/undo/like/{}", self.ap_id, uuid::Uuid::new_v4());
let mut undo = Undo::default();
populate_object_props(
&mut undo.object_props,
vec![community.get_followers_url()],
&undo_id,
)?;
undo
.undo_props
.set_actor_xsd_any_uri(creator.actor_id.to_owned())?
.set_object_base_box(like)?;
send_activity_to_community(
&creator,
&conn,
&community,
vec![community.get_shared_inbox_url()],
undo,
)?;
Ok(())
}
}
struct MentionsAndAddresses {
addressed_ccs: Vec<String>,
inboxes: Vec<String>,
tags: Vec<Mention>,
}
/// This takes a comment, and builds a list of to_addresses, inboxes,
/// and mention tags, so they know where to be sent to.
/// Addresses are the users / addresses that go in the cc field.
fn collect_non_local_mentions_and_addresses(
conn: &PgConnection,
content: &str,
community: &Community,
) -> Result<MentionsAndAddresses, Error> {
let mut addressed_ccs = vec![community.get_followers_url()];
// Add the mention tag
let mut tags = Vec::new();
// Get the inboxes for any mentions
let mentions = scrape_text_for_mentions(&content)
.into_iter()
// Filter only the non-local ones
.filter(|m| !m.is_local())
.collect::<Vec<MentionData>>();
let mut mention_inboxes = Vec::new();
for mention in &mentions {
// TODO should it be fetching it every time?
if let Ok(actor_id) = fetch_webfinger_url(mention) {
debug!("mention actor_id: {}", actor_id);
addressed_ccs.push(actor_id.to_owned());
let mention_user = get_or_fetch_and_upsert_remote_user(&actor_id, &conn)?;
let shared_inbox = mention_user.get_shared_inbox_url();
mention_inboxes.push(shared_inbox);
let mut mention_tag = Mention::new();
mention_tag
.link_props
.set_href(actor_id)?
.set_name_xsd_string(mention.full_name())?;
tags.push(mention_tag);
}
}
let mut inboxes = vec![community.get_shared_inbox_url()];
inboxes.extend(mention_inboxes);
inboxes = inboxes.into_iter().unique().collect();
Ok(MentionsAndAddresses {
addressed_ccs,
inboxes,
tags,
})
}

View file

@ -1,109 +1,414 @@
use crate::apub::make_apub_endpoint;
use crate::db::community::Community;
use crate::db::community_view::CommunityFollowerView;
use crate::db::establish_unpooled_connection;
use crate::to_datetime_utc;
use activitypub::{actor::Group, collection::UnorderedCollection, context};
use actix_web::body::Body;
use actix_web::web::Path;
use actix_web::HttpResponse;
use serde::Deserialize;
impl Community {
pub fn as_group(&self) -> Group {
let base_url = make_apub_endpoint("c", &self.name);
let mut group = Group::default();
group.object_props.set_context_object(context()).ok();
group.object_props.set_id_string(base_url.to_string()).ok();
group
.object_props
.set_name_string(self.name.to_owned())
.ok();
group
.object_props
.set_published_utctime(to_datetime_utc(self.published))
.ok();
if let Some(updated) = self.updated {
group
.object_props
.set_updated_utctime(to_datetime_utc(updated))
.ok();
}
if let Some(description) = &self.description {
group
.object_props
.set_summary_string(description.to_string())
.ok();
}
group
.ap_actor_props
.set_inbox_string(format!("{}/inbox", &base_url))
.ok();
group
.ap_actor_props
.set_outbox_string(format!("{}/outbox", &base_url))
.ok();
group
.ap_actor_props
.set_followers_string(format!("{}/followers", &base_url))
.ok();
group
}
pub fn followers_as_collection(&self) -> UnorderedCollection {
let base_url = make_apub_endpoint("c", &self.name);
let mut collection = UnorderedCollection::default();
collection.object_props.set_context_object(context()).ok();
collection.object_props.set_id_string(base_url).ok();
let connection = establish_unpooled_connection();
//As we are an object, we validated that the community id was valid
let community_followers = CommunityFollowerView::for_community(&connection, self.id).unwrap();
let ap_followers = community_followers
.iter()
.map(|follower| make_apub_endpoint("u", &follower.user_name))
.collect();
collection
.collection_props
.set_items_string_vec(ap_followers)
.unwrap();
collection
}
}
use crate::{
apub::{
activities::{populate_object_props, send_activity},
create_apub_response,
create_apub_tombstone_response,
create_tombstone,
extensions::{group_extensions::GroupExtension, signatures::PublicKey},
fetcher::get_or_fetch_and_upsert_remote_user,
get_shared_inbox,
ActorType,
FromApub,
GroupExt,
ToApub,
},
convert_datetime,
db::{
activity::insert_activity,
community::{Community, CommunityForm},
community_view::{CommunityFollowerView, CommunityModeratorView},
user::User_,
},
naive_now,
routes::DbPoolParam,
};
use activitystreams::{
activity::{Accept, Announce, Delete, Remove, Undo},
actor::{kind::GroupType, properties::ApActorProperties, Group},
collection::UnorderedCollection,
context,
endpoint::EndpointProperties,
object::properties::ObjectProperties,
Activity,
Base,
BaseBox,
};
use activitystreams_ext::Ext3;
use activitystreams_new::{activity::Follow, object::Tombstone};
use actix_web::{body::Body, web::Path, HttpResponse, Result};
use diesel::PgConnection;
use failure::{Error, _core::fmt::Debug};
use itertools::Itertools;
use serde::{Deserialize, Serialize};
#[derive(Deserialize)]
pub struct CommunityQuery {
community_name: String,
}
pub async fn get_apub_community(info: Path<CommunityQuery>) -> HttpResponse<Body> {
let connection = establish_unpooled_connection();
impl ToApub for Community {
type Response = GroupExt;
if let Ok(community) = Community::read_from_name(&connection, info.community_name.to_owned()) {
HttpResponse::Ok()
.content_type("application/activity+json")
.body(serde_json::to_string(&community.as_group()).unwrap())
} else {
HttpResponse::NotFound().finish()
// Turn a Lemmy Community into an ActivityPub group that can be sent out over the network.
fn to_apub(&self, conn: &PgConnection) -> Result<GroupExt, Error> {
let mut group = Group::default();
let oprops: &mut ObjectProperties = group.as_mut();
// The attributed to, is an ordered vector with the creator actor_ids first,
// then the rest of the moderators
// TODO Technically the instance admins can mod the community, but lets
// ignore that for now
let moderators = CommunityModeratorView::for_community(&conn, self.id)?
.into_iter()
.map(|m| m.user_actor_id)
.collect();
oprops
.set_context_xsd_any_uri(context())?
.set_id(self.actor_id.to_owned())?
.set_name_xsd_string(self.name.to_owned())?
.set_published(convert_datetime(self.published))?
.set_many_attributed_to_xsd_any_uris(moderators)?;
if let Some(u) = self.updated.to_owned() {
oprops.set_updated(convert_datetime(u))?;
}
if let Some(d) = self.description.to_owned() {
// TODO: this should be html, also add source field with raw markdown
// -> same for post.content and others
oprops.set_content_xsd_string(d)?;
}
let mut endpoint_props = EndpointProperties::default();
endpoint_props.set_shared_inbox(self.get_shared_inbox_url())?;
let mut actor_props = ApActorProperties::default();
actor_props
.set_preferred_username(self.title.to_owned())?
.set_inbox(self.get_inbox_url())?
.set_outbox(self.get_outbox_url())?
.set_endpoints(endpoint_props)?
.set_followers(self.get_followers_url())?;
let group_extension = GroupExtension::new(conn, self.category_id, self.nsfw)?;
Ok(Ext3::new(
group,
group_extension,
actor_props,
self.get_public_key_ext(),
))
}
fn to_tombstone(&self) -> Result<Tombstone, Error> {
create_tombstone(
self.deleted,
&self.actor_id,
self.updated,
GroupType.to_string(),
)
}
}
pub async fn get_apub_community_followers(info: Path<CommunityQuery>) -> HttpResponse<Body> {
let connection = establish_unpooled_connection();
impl ActorType for Community {
fn actor_id(&self) -> String {
self.actor_id.to_owned()
}
if let Ok(community) = Community::read_from_name(&connection, info.community_name.to_owned()) {
HttpResponse::Ok()
.content_type("application/activity+json")
.body(serde_json::to_string(&community.followers_as_collection()).unwrap())
} else {
HttpResponse::NotFound().finish()
fn public_key(&self) -> String {
self.public_key.to_owned().unwrap()
}
fn private_key(&self) -> String {
self.private_key.to_owned().unwrap()
}
/// As a local community, accept the follow request from a remote user.
fn send_accept_follow(&self, follow: &Follow, conn: &PgConnection) -> Result<(), Error> {
let actor_uri = follow.actor.as_single_xsd_any_uri().unwrap().to_string();
let id = format!("{}/accept/{}", self.actor_id, uuid::Uuid::new_v4());
let mut accept = Accept::new();
accept
.object_props
.set_context_xsd_any_uri(context())?
.set_id(id)?;
accept
.accept_props
.set_actor_xsd_any_uri(self.actor_id.to_owned())?
.set_object_base_box(BaseBox::from_concrete(follow.clone())?)?;
let to = format!("{}/inbox", actor_uri);
insert_activity(&conn, self.creator_id, &accept, true)?;
send_activity(&accept, self, vec![to])?;
Ok(())
}
fn send_delete(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error> {
let group = self.to_apub(conn)?;
let id = format!("{}/delete/{}", self.actor_id, uuid::Uuid::new_v4());
let mut delete = Delete::default();
populate_object_props(
&mut delete.object_props,
vec![self.get_followers_url()],
&id,
)?;
delete
.delete_props
.set_actor_xsd_any_uri(creator.actor_id.to_owned())?
.set_object_base_box(BaseBox::from_concrete(group)?)?;
insert_activity(&conn, self.creator_id, &delete, true)?;
// Note: For an accept, since it was automatic, no one pushed a button,
// the community was the actor.
// But for delete, the creator is the actor, and does the signing
send_activity(&delete, creator, self.get_follower_inboxes(&conn)?)?;
Ok(())
}
fn send_undo_delete(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error> {
let group = self.to_apub(conn)?;
let id = format!("{}/delete/{}", self.actor_id, uuid::Uuid::new_v4());
let mut delete = Delete::default();
populate_object_props(
&mut delete.object_props,
vec![self.get_followers_url()],
&id,
)?;
delete
.delete_props
.set_actor_xsd_any_uri(creator.actor_id.to_owned())?
.set_object_base_box(BaseBox::from_concrete(group)?)?;
// TODO
// Undo that fake activity
let undo_id = format!("{}/undo/delete/{}", self.actor_id, uuid::Uuid::new_v4());
let mut undo = Undo::default();
populate_object_props(
&mut undo.object_props,
vec![self.get_followers_url()],
&undo_id,
)?;
undo
.undo_props
.set_actor_xsd_any_uri(creator.actor_id.to_owned())?
.set_object_base_box(delete)?;
insert_activity(&conn, self.creator_id, &undo, true)?;
// Note: For an accept, since it was automatic, no one pushed a button,
// the community was the actor.
// But for delete, the creator is the actor, and does the signing
send_activity(&undo, creator, self.get_follower_inboxes(&conn)?)?;
Ok(())
}
fn send_remove(&self, mod_: &User_, conn: &PgConnection) -> Result<(), Error> {
let group = self.to_apub(conn)?;
let id = format!("{}/remove/{}", self.actor_id, uuid::Uuid::new_v4());
let mut remove = Remove::default();
populate_object_props(
&mut remove.object_props,
vec![self.get_followers_url()],
&id,
)?;
remove
.remove_props
.set_actor_xsd_any_uri(mod_.actor_id.to_owned())?
.set_object_base_box(BaseBox::from_concrete(group)?)?;
insert_activity(&conn, mod_.id, &remove, true)?;
// Note: For an accept, since it was automatic, no one pushed a button,
// the community was the actor.
// But for delete, the creator is the actor, and does the signing
send_activity(&remove, mod_, self.get_follower_inboxes(&conn)?)?;
Ok(())
}
fn send_undo_remove(&self, mod_: &User_, conn: &PgConnection) -> Result<(), Error> {
let group = self.to_apub(conn)?;
let id = format!("{}/remove/{}", self.actor_id, uuid::Uuid::new_v4());
let mut remove = Remove::default();
populate_object_props(
&mut remove.object_props,
vec![self.get_followers_url()],
&id,
)?;
remove
.remove_props
.set_actor_xsd_any_uri(mod_.actor_id.to_owned())?
.set_object_base_box(BaseBox::from_concrete(group)?)?;
// Undo that fake activity
let undo_id = format!("{}/undo/remove/{}", self.actor_id, uuid::Uuid::new_v4());
let mut undo = Undo::default();
populate_object_props(
&mut undo.object_props,
vec![self.get_followers_url()],
&undo_id,
)?;
undo
.undo_props
.set_actor_xsd_any_uri(mod_.actor_id.to_owned())?
.set_object_base_box(remove)?;
insert_activity(&conn, mod_.id, &undo, true)?;
// Note: For an accept, since it was automatic, no one pushed a button,
// the community was the actor.
// But for remove , the creator is the actor, and does the signing
send_activity(&undo, mod_, self.get_follower_inboxes(&conn)?)?;
Ok(())
}
/// For a given community, returns the inboxes of all followers.
fn get_follower_inboxes(&self, conn: &PgConnection) -> Result<Vec<String>, Error> {
Ok(
CommunityFollowerView::for_community(conn, self.id)?
.into_iter()
.map(|c| get_shared_inbox(&c.user_actor_id))
.filter(|s| !s.is_empty())
.unique()
.collect(),
)
}
fn send_follow(&self, _follow_actor_id: &str, _conn: &PgConnection) -> Result<(), Error> {
unimplemented!()
}
fn send_unfollow(&self, _follow_actor_id: &str, _conn: &PgConnection) -> Result<(), Error> {
unimplemented!()
}
}
impl FromApub for CommunityForm {
type ApubType = GroupExt;
/// Parse an ActivityPub group received from another instance into a Lemmy community.
fn from_apub(group: &GroupExt, conn: &PgConnection) -> Result<Self, Error> {
let group_extensions: &GroupExtension = &group.ext_one;
let oprops = &group.inner.object_props;
let aprops = &group.ext_two;
let public_key: &PublicKey = &group.ext_three.public_key;
let mut creator_and_moderator_uris = oprops.get_many_attributed_to_xsd_any_uris().unwrap();
let creator = creator_and_moderator_uris
.next()
.map(|c| get_or_fetch_and_upsert_remote_user(&c.to_string(), &conn).unwrap())
.unwrap();
Ok(CommunityForm {
name: oprops.get_name_xsd_string().unwrap().to_string(),
title: aprops.get_preferred_username().unwrap().to_string(),
// TODO: should be parsed as html and tags like <script> removed (or use markdown source)
// -> same for post.content etc
description: oprops.get_content_xsd_string().map(|s| s.to_string()),
category_id: group_extensions.category.identifier.parse::<i32>()?,
creator_id: creator.id,
removed: None,
published: oprops
.get_published()
.map(|u| u.as_ref().to_owned().naive_local()),
updated: oprops
.get_updated()
.map(|u| u.as_ref().to_owned().naive_local()),
deleted: None,
nsfw: group_extensions.sensitive,
actor_id: oprops.get_id().unwrap().to_string(),
local: false,
private_key: None,
public_key: Some(public_key.to_owned().public_key_pem),
last_refreshed_at: Some(naive_now()),
})
}
}
/// Return the community json over HTTP.
pub async fn get_apub_community_http(
info: Path<CommunityQuery>,
db: DbPoolParam,
) -> Result<HttpResponse<Body>, Error> {
let community = Community::read_from_name(&&db.get()?, &info.community_name)?;
if !community.deleted {
Ok(create_apub_response(
&community.to_apub(&db.get().unwrap())?,
))
} else {
Ok(create_apub_tombstone_response(&community.to_tombstone()?))
}
}
/// Returns an empty followers collection, only populating the size (for privacy).
pub async fn get_apub_community_followers(
info: Path<CommunityQuery>,
db: DbPoolParam,
) -> Result<HttpResponse<Body>, Error> {
let community = Community::read_from_name(&&db.get()?, &info.community_name)?;
let conn = db.get()?;
//As we are an object, we validated that the community id was valid
let community_followers = CommunityFollowerView::for_community(&conn, community.id).unwrap();
let mut collection = UnorderedCollection::default();
let oprops: &mut ObjectProperties = collection.as_mut();
oprops
.set_context_xsd_any_uri(context())?
.set_id(community.actor_id)?;
collection
.collection_props
.set_total_items(community_followers.len() as u64)?;
Ok(create_apub_response(&collection))
}
impl Community {
pub fn do_announce<A>(
activity: A,
community: &Community,
sender: &dyn ActorType,
conn: &PgConnection,
) -> Result<HttpResponse, Error>
where
A: Activity + Base + Serialize + Debug,
{
let mut announce = Announce::default();
populate_object_props(
&mut announce.object_props,
vec![community.get_followers_url()],
&format!("{}/announce/{}", community.actor_id, uuid::Uuid::new_v4()),
)?;
announce
.announce_props
.set_actor_xsd_any_uri(community.actor_id.to_owned())?
.set_object_base_box(BaseBox::from_concrete(activity)?)?;
insert_activity(&conn, community.creator_id, &announce, true)?;
// dont send to the instance where the activity originally came from, because that would result
// in a database error (same data inserted twice)
let mut to = community.get_follower_inboxes(&conn)?;
// this seems to be the "easiest" stable alternative for remove_item()
to.retain(|x| *x != sender.get_shared_inbox_url());
send_activity(&announce, community, to)?;
Ok(HttpResponse::Ok().finish())
}
}

View file

@ -0,0 +1,123 @@
use crate::{
apub::{
extensions::signatures::verify,
fetcher::{get_or_fetch_and_upsert_remote_community, get_or_fetch_and_upsert_remote_user},
ActorType,
},
db::{
activity::insert_activity,
community::{Community, CommunityFollower, CommunityFollowerForm},
user::User_,
Followable,
},
routes::{ChatServerParam, DbPoolParam},
};
use activitystreams::activity::Undo;
use activitystreams_new::activity::Follow;
use actix_web::{web, HttpRequest, HttpResponse, Result};
use diesel::PgConnection;
use failure::{Error, _core::fmt::Debug};
use log::debug;
use serde::Deserialize;
#[serde(untagged)]
#[derive(Deserialize, Debug)]
pub enum CommunityAcceptedObjects {
Follow(Follow),
Undo(Undo),
}
impl CommunityAcceptedObjects {
fn follow(&self) -> Result<Follow, Error> {
match self {
CommunityAcceptedObjects::Follow(f) => Ok(f.to_owned()),
CommunityAcceptedObjects::Undo(u) => Ok(
u.undo_props
.get_object_base_box()
.to_owned()
.unwrap()
.to_owned()
.into_concrete::<Follow>()?,
),
}
}
}
/// Handler for all incoming activities to community inboxes.
pub async fn community_inbox(
request: HttpRequest,
input: web::Json<CommunityAcceptedObjects>,
path: web::Path<String>,
db: DbPoolParam,
_chat_server: ChatServerParam,
) -> Result<HttpResponse, Error> {
let input = input.into_inner();
let conn = db.get()?;
let community = Community::read_from_name(&conn, &path.into_inner())?;
if !community.local {
return Err(format_err!(
"Received activity is addressed to remote community {}",
&community.actor_id
));
}
debug!(
"Community {} received activity {:?}",
&community.name, &input
);
let follow = input.follow()?;
let user_uri = follow.actor.as_single_xsd_any_uri().unwrap().to_string();
let community_uri = follow.object.as_single_xsd_any_uri().unwrap().to_string();
let conn = db.get()?;
let user = get_or_fetch_and_upsert_remote_user(&user_uri, &conn)?;
let community = get_or_fetch_and_upsert_remote_community(&community_uri, &conn)?;
verify(&request, &user)?;
match input {
CommunityAcceptedObjects::Follow(f) => handle_follow(&f, &user, &community, &conn),
CommunityAcceptedObjects::Undo(u) => handle_undo_follow(&u, &user, &community, &conn),
}
}
/// Handle a follow request from a remote user, adding it to the local database and returning an
/// Accept activity.
fn handle_follow(
follow: &Follow,
user: &User_,
community: &Community,
conn: &PgConnection,
) -> Result<HttpResponse, Error> {
insert_activity(&conn, user.id, &follow, false)?;
let community_follower_form = CommunityFollowerForm {
community_id: community.id,
user_id: user.id,
};
// This will fail if they're already a follower, but ignore the error.
CommunityFollower::follow(&conn, &community_follower_form).ok();
community.send_accept_follow(&follow, &conn)?;
Ok(HttpResponse::Ok().finish())
}
fn handle_undo_follow(
undo: &Undo,
user: &User_,
community: &Community,
conn: &PgConnection,
) -> Result<HttpResponse, Error> {
insert_activity(&conn, user.id, &undo, false)?;
let community_follower_form = CommunityFollowerForm {
community_id: community.id,
user_id: user.id,
};
CommunityFollower::unfollow(&conn, &community_follower_form).ok();
Ok(HttpResponse::Ok().finish())
}

View file

@ -0,0 +1,40 @@
use crate::db::{category::Category, Crud};
use activitystreams::{ext::Extension, Actor};
use diesel::PgConnection;
use failure::Error;
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct GroupExtension {
pub category: GroupCategory,
pub sensitive: bool,
}
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct GroupCategory {
// Using a string because that's how Peertube does it.
pub identifier: String,
pub name: String,
}
impl GroupExtension {
pub fn new(
conn: &PgConnection,
category_id: i32,
sensitive: bool,
) -> Result<GroupExtension, Error> {
let category = Category::read(conn, category_id)?;
let group_category = GroupCategory {
identifier: category_id.to_string(),
name: category.name,
};
Ok(GroupExtension {
category: group_category,
sensitive,
})
}
}
impl<T> Extension<T> for GroupExtension where T: Actor {}

View file

@ -0,0 +1,3 @@
pub mod group_extensions;
pub mod page_extension;
pub mod signatures;

View file

@ -0,0 +1,11 @@
use activitystreams::{ext::Extension, Base};
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct PageExtension {
pub comments_enabled: bool,
pub sensitive: bool,
}
impl<T> Extension<T> for PageExtension where T: Base {}

View file

@ -0,0 +1,137 @@
use crate::apub::ActorType;
use activitystreams::ext::Extension;
use actix_web::HttpRequest;
use attohttpc::RequestBuilder;
use failure::Error;
use http_signature_normalization::Config;
use log::debug;
use openssl::{
hash::MessageDigest,
pkey::PKey,
rsa::Rsa,
sign::{Signer, Verifier},
};
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
lazy_static! {
static ref HTTP_SIG_CONFIG: Config = Config::new();
}
pub struct Keypair {
pub private_key: String,
pub public_key: String,
}
/// Generate the asymmetric keypair for ActivityPub HTTP signatures.
pub fn generate_actor_keypair() -> Result<Keypair, Error> {
let rsa = Rsa::generate(2048)?;
let pkey = PKey::from_rsa(rsa)?;
let public_key = pkey.public_key_to_pem()?;
let private_key = pkey.private_key_to_pem_pkcs8()?;
Ok(Keypair {
private_key: String::from_utf8(private_key)?,
public_key: String::from_utf8(public_key)?,
})
}
// TODO is it possible to create this signature, with just the url and actor?
/// Signs request headers with the given keypair.
pub fn sign(request: &mut RequestBuilder, actor: &dyn ActorType) -> Result<String, Error> {
let signing_key_id = format!("{}#main-key", actor.actor_id());
let headers = request
.inspect()
.headers()
.iter()
.map(|h| -> Result<(String, String), Error> {
Ok((h.0.as_str().to_owned(), h.1.to_str()?.to_owned()))
})
.collect::<Result<BTreeMap<String, String>, Error>>()?;
let mut path_and_query = request.inspect().url().path().to_owned();
if let Some(query) = request.inspect().url().query() {
path_and_query.push_str(query);
}
let signature_header_value = HTTP_SIG_CONFIG
.begin_sign(
request.inspect().method().as_str(),
&path_and_query,
headers,
)?
.sign(signing_key_id, |signing_string| {
let private_key = PKey::private_key_from_pem(actor.private_key().as_bytes())?;
let mut signer = Signer::new(MessageDigest::sha256(), &private_key).unwrap();
signer.update(signing_string.as_bytes()).unwrap();
Ok(base64::encode(signer.sign_to_vec()?)) as Result<_, Error>
})?
.signature_header();
Ok(signature_header_value)
}
pub fn verify(request: &HttpRequest, actor: &dyn ActorType) -> Result<(), Error> {
let headers = request
.headers()
.iter()
.map(|h| -> Result<(String, String), Error> {
Ok((h.0.as_str().to_owned(), h.1.to_str()?.to_owned()))
})
.collect::<Result<BTreeMap<String, String>, Error>>()?;
let verified = HTTP_SIG_CONFIG
.begin_verify(
request.method().as_str(),
request.uri().path_and_query().unwrap().as_str(),
headers,
)?
.verify(|signature, signing_string| -> Result<bool, Error> {
debug!(
"Verifying with key {}, message {}",
&actor.public_key(),
&signing_string
);
let public_key = PKey::public_key_from_pem(actor.public_key().as_bytes())?;
let mut verifier = Verifier::new(MessageDigest::sha256(), &public_key).unwrap();
verifier.update(&signing_string.as_bytes()).unwrap();
Ok(verifier.verify(&base64::decode(signature)?)?)
})?;
if verified {
debug!("verified signature for {}", &request.uri());
Ok(())
} else {
Err(format_err!(
"Invalid signature on request: {}",
&request.uri()
))
}
}
// The following is taken from here:
// https://docs.rs/activitystreams/0.5.0-alpha.17/activitystreams/ext/index.html
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct PublicKey {
pub id: String,
pub owner: String,
pub public_key_pem: String,
}
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct PublicKeyExtension {
pub public_key: PublicKey,
}
impl PublicKey {
pub fn to_ext(&self) -> PublicKeyExtension {
PublicKeyExtension {
public_key: self.to_owned(),
}
}
}
impl<T> Extension<T> for PublicKeyExtension where T: activitystreams::Actor {}

326
server/src/apub/fetcher.rs Normal file
View file

@ -0,0 +1,326 @@
use activitystreams::object::Note;
use actix_web::Result;
use diesel::{result::Error::NotFound, PgConnection};
use failure::{Error, _core::fmt::Debug};
use log::debug;
use serde::Deserialize;
use std::time::Duration;
use url::Url;
use crate::{
api::site::SearchResponse,
db::{
comment::{Comment, CommentForm},
comment_view::CommentView,
community::{Community, CommunityForm, CommunityModerator, CommunityModeratorForm},
community_view::CommunityView,
post::{Post, PostForm},
post_view::PostView,
user::{UserForm, User_},
Crud,
Joinable,
SearchType,
},
naive_now,
routes::nodeinfo::{NodeInfo, NodeInfoWellKnown},
};
use crate::{
apub::{
get_apub_protocol_string,
is_apub_id_valid,
FromApub,
GroupExt,
PageExt,
PersonExt,
APUB_JSON_CONTENT_TYPE,
},
db::user_view::UserView,
};
use chrono::NaiveDateTime;
static ACTOR_REFETCH_INTERVAL_SECONDS: i64 = 24 * 60 * 60;
// Fetch nodeinfo metadata from a remote instance.
fn _fetch_node_info(domain: &str) -> Result<NodeInfo, Error> {
let well_known_uri = Url::parse(&format!(
"{}://{}/.well-known/nodeinfo",
get_apub_protocol_string(),
domain
))?;
let well_known = fetch_remote_object::<NodeInfoWellKnown>(&well_known_uri)?;
Ok(fetch_remote_object::<NodeInfo>(&well_known.links.href)?)
}
/// Fetch any type of ActivityPub object, handling things like HTTP headers, deserialisation,
/// timeouts etc.
pub fn fetch_remote_object<Response>(url: &Url) -> Result<Response, Error>
where
Response: for<'de> Deserialize<'de>,
{
if !is_apub_id_valid(&url) {
return Err(format_err!("Activitypub uri invalid or blocked: {}", url));
}
// TODO: this function should return a future
let timeout = Duration::from_secs(60);
let text: String = attohttpc::get(url.as_str())
.header("Accept", APUB_JSON_CONTENT_TYPE)
.connect_timeout(timeout)
.timeout(timeout)
// .body(())
.send()?
.text()?;
let res: Response = serde_json::from_str(&text)?;
Ok(res)
}
/// The types of ActivityPub objects that can be fetched directly by searching for their ID.
#[serde(untagged)]
#[derive(serde::Deserialize, Debug)]
pub enum SearchAcceptedObjects {
Person(Box<PersonExt>),
Group(Box<GroupExt>),
Page(Box<PageExt>),
Comment(Box<Note>),
}
/// Attempt to parse the query as URL, and fetch an ActivityPub object from it.
///
/// Some working examples for use with the docker/federation/ setup:
/// http://lemmy_alpha:8540/c/main, or !main@lemmy_alpha:8540
/// http://lemmy_alpha:8540/u/lemmy_alpha, or @lemmy_alpha@lemmy_alpha:8540
/// http://lemmy_alpha:8540/post/3
/// http://lemmy_alpha:8540/comment/2
pub fn search_by_apub_id(query: &str, conn: &PgConnection) -> Result<SearchResponse, Error> {
// Parse the shorthand query url
let query_url = if query.contains('@') {
debug!("{}", query);
let split = query.split('@').collect::<Vec<&str>>();
// User type will look like ['', username, instance]
// Community will look like [!community, instance]
let (name, instance) = if split.len() == 3 {
(format!("/u/{}", split[1]), split[2])
} else if split.len() == 2 {
if split[0].contains('!') {
let split2 = split[0].split('!').collect::<Vec<&str>>();
(format!("/c/{}", split2[1]), split[1])
} else {
return Err(format_err!("Invalid search query: {}", query));
}
} else {
return Err(format_err!("Invalid search query: {}", query));
};
let url = format!("{}://{}{}", get_apub_protocol_string(), instance, name);
Url::parse(&url)?
} else {
Url::parse(&query)?
};
let mut response = SearchResponse {
type_: SearchType::All.to_string(),
comments: vec![],
posts: vec![],
communities: vec![],
users: vec![],
};
match fetch_remote_object::<SearchAcceptedObjects>(&query_url)? {
SearchAcceptedObjects::Person(p) => {
let user_uri = p.inner.object_props.get_id().unwrap().to_string();
let user = get_or_fetch_and_upsert_remote_user(&user_uri, &conn)?;
response.users = vec![UserView::read(conn, user.id)?];
}
SearchAcceptedObjects::Group(g) => {
let community_uri = g.inner.object_props.get_id().unwrap().to_string();
let community = get_or_fetch_and_upsert_remote_community(&community_uri, &conn)?;
// TODO Maybe at some point in the future, fetch all the history of a community
// fetch_community_outbox(&c, conn)?;
response.communities = vec![CommunityView::read(conn, community.id, None)?];
}
SearchAcceptedObjects::Page(p) => {
let p = upsert_post(&PostForm::from_apub(&p, conn)?, conn)?;
response.posts = vec![PostView::read(conn, p.id, None)?];
}
SearchAcceptedObjects::Comment(c) => {
let post_url = c
.object_props
.get_many_in_reply_to_xsd_any_uris()
.unwrap()
.next()
.unwrap()
.to_string();
// TODO: also fetch parent comments if any
let post = fetch_remote_object(&Url::parse(&post_url)?)?;
upsert_post(&PostForm::from_apub(&post, conn)?, conn)?;
let c = upsert_comment(&CommentForm::from_apub(&c, conn)?, conn)?;
response.comments = vec![CommentView::read(conn, c.id, None)?];
}
}
Ok(response)
}
/// Check if a remote user exists, create if not found, if its too old update it.Fetch a user, insert/update it in the database and return the user.
pub fn get_or_fetch_and_upsert_remote_user(
apub_id: &str,
conn: &PgConnection,
) -> Result<User_, Error> {
match User_::read_from_actor_id(&conn, &apub_id) {
Ok(u) => {
// If its older than a day, re-fetch it
if !u.local && should_refetch_actor(u.last_refreshed_at) {
debug!("Fetching and updating from remote user: {}", apub_id);
let person = fetch_remote_object::<PersonExt>(&Url::parse(apub_id)?)?;
let mut uf = UserForm::from_apub(&person, &conn)?;
uf.last_refreshed_at = Some(naive_now());
Ok(User_::update(&conn, u.id, &uf)?)
} else {
Ok(u)
}
}
Err(NotFound {}) => {
debug!("Fetching and creating remote user: {}", apub_id);
let person = fetch_remote_object::<PersonExt>(&Url::parse(apub_id)?)?;
let uf = UserForm::from_apub(&person, &conn)?;
Ok(User_::create(conn, &uf)?)
}
Err(e) => Err(Error::from(e)),
}
}
/// Determines when a remote actor should be refetched from its instance. In release builds, this is
/// ACTOR_REFETCH_INTERVAL_SECONDS after the last refetch, in debug builds always.
///
/// TODO it won't pick up new avatars, summaries etc until a day after.
/// Actors need an "update" activity pushed to other servers to fix this.
fn should_refetch_actor(last_refreshed: NaiveDateTime) -> bool {
if cfg!(debug_assertions) {
true
} else {
let update_interval = chrono::Duration::seconds(ACTOR_REFETCH_INTERVAL_SECONDS);
last_refreshed.lt(&(naive_now() - update_interval))
}
}
/// Check if a remote community exists, create if not found, if its too old update it.Fetch a community, insert/update it in the database and return the community.
pub fn get_or_fetch_and_upsert_remote_community(
apub_id: &str,
conn: &PgConnection,
) -> Result<Community, Error> {
match Community::read_from_actor_id(&conn, &apub_id) {
Ok(c) => {
if !c.local && should_refetch_actor(c.last_refreshed_at) {
debug!("Fetching and updating from remote community: {}", apub_id);
let group = fetch_remote_object::<GroupExt>(&Url::parse(apub_id)?)?;
let mut cf = CommunityForm::from_apub(&group, conn)?;
cf.last_refreshed_at = Some(naive_now());
Ok(Community::update(&conn, c.id, &cf)?)
} else {
Ok(c)
}
}
Err(NotFound {}) => {
debug!("Fetching and creating remote community: {}", apub_id);
let group = fetch_remote_object::<GroupExt>(&Url::parse(apub_id)?)?;
let cf = CommunityForm::from_apub(&group, conn)?;
let community = Community::create(conn, &cf)?;
// Also add the community moderators too
let creator_and_moderator_uris = group
.inner
.object_props
.get_many_attributed_to_xsd_any_uris()
.unwrap();
let creator_and_moderators = creator_and_moderator_uris
.map(|c| get_or_fetch_and_upsert_remote_user(&c.to_string(), &conn).unwrap())
.collect::<Vec<User_>>();
for mod_ in creator_and_moderators {
let community_moderator_form = CommunityModeratorForm {
community_id: community.id,
user_id: mod_.id,
};
CommunityModerator::join(&conn, &community_moderator_form)?;
}
Ok(community)
}
Err(e) => Err(Error::from(e)),
}
}
fn upsert_post(post_form: &PostForm, conn: &PgConnection) -> Result<Post, Error> {
let existing = Post::read_from_apub_id(conn, &post_form.ap_id);
match existing {
Err(NotFound {}) => Ok(Post::create(conn, &post_form)?),
Ok(p) => Ok(Post::update(conn, p.id, &post_form)?),
Err(e) => Err(Error::from(e)),
}
}
pub fn get_or_fetch_and_insert_remote_post(
post_ap_id: &str,
conn: &PgConnection,
) -> Result<Post, Error> {
match Post::read_from_apub_id(conn, post_ap_id) {
Ok(p) => Ok(p),
Err(NotFound {}) => {
debug!("Fetching and creating remote post: {}", post_ap_id);
let post = fetch_remote_object::<PageExt>(&Url::parse(post_ap_id)?)?;
let post_form = PostForm::from_apub(&post, conn)?;
Ok(Post::create(conn, &post_form)?)
}
Err(e) => Err(Error::from(e)),
}
}
fn upsert_comment(comment_form: &CommentForm, conn: &PgConnection) -> Result<Comment, Error> {
let existing = Comment::read_from_apub_id(conn, &comment_form.ap_id);
match existing {
Err(NotFound {}) => Ok(Comment::create(conn, &comment_form)?),
Ok(p) => Ok(Comment::update(conn, p.id, &comment_form)?),
Err(e) => Err(Error::from(e)),
}
}
pub fn get_or_fetch_and_insert_remote_comment(
comment_ap_id: &str,
conn: &PgConnection,
) -> Result<Comment, Error> {
match Comment::read_from_apub_id(conn, comment_ap_id) {
Ok(p) => Ok(p),
Err(NotFound {}) => {
debug!(
"Fetching and creating remote comment and its parents: {}",
comment_ap_id
);
let comment = fetch_remote_object::<Note>(&Url::parse(comment_ap_id)?)?;
let comment_form = CommentForm::from_apub(&comment, conn)?;
Ok(Comment::create(conn, &comment_form)?)
}
Err(e) => Err(Error::from(e)),
}
}
// TODO It should not be fetching data from a community outbox.
// All posts, comments, comment likes, etc should be posts to our community_inbox
// The only data we should be periodically fetching (if it hasn't been fetched in the last day
// maybe), is community and user actors
// and user actors
// Fetch all posts in the outbox of the given user, and insert them into the database.
// fn fetch_community_outbox(community: &Community, conn: &PgConnection) -> Result<Vec<Post>, Error> {
// let outbox_url = Url::parse(&community.get_outbox_url())?;
// let outbox = fetch_remote_object::<OrderedCollection>(&outbox_url)?;
// let items = outbox.collection_props.get_many_items_base_boxes();
// Ok(
// items
// .unwrap()
// .map(|obox: &BaseBox| -> Result<PostForm, Error> {
// let page = obox.clone().to_concrete::<Page>()?;
// PostForm::from_page(&page, conn)
// })
// .map(|pf| upsert_post(&pf?, conn))
// .collect::<Result<Vec<Post>, Error>>()?,
// )
// }

View file

@ -1,107 +1,266 @@
pub mod activities;
pub mod comment;
pub mod community;
pub mod community_inbox;
pub mod extensions;
pub mod fetcher;
pub mod post;
pub mod private_message;
pub mod shared_inbox;
pub mod user;
use crate::Settings;
pub mod user_inbox;
use std::fmt::Display;
use crate::{
apub::extensions::{
group_extensions::GroupExtension,
page_extension::PageExtension,
signatures::{PublicKey, PublicKeyExtension},
},
convert_datetime,
db::user::User_,
routes::webfinger::WebFingerResponse,
MentionData,
Settings,
};
use activitystreams::{
actor::{properties::ApActorProperties, Group, Person},
object::Page,
};
use activitystreams_ext::{Ext1, Ext2, Ext3};
use activitystreams_new::{activity::Follow, object::Tombstone, prelude::*};
use actix_web::{body::Body, HttpResponse, Result};
use chrono::NaiveDateTime;
use diesel::PgConnection;
use failure::Error;
use log::debug;
use serde::Serialize;
use url::Url;
#[cfg(test)]
mod tests {
use crate::db::community::Community;
use crate::db::post::Post;
use crate::db::user::User_;
use crate::db::{ListingType, SortType};
use crate::{naive_now, Settings};
type GroupExt = Ext3<Group, GroupExtension, ApActorProperties, PublicKeyExtension>;
type PersonExt = Ext2<Person, ApActorProperties, PublicKeyExtension>;
type PageExt = Ext1<Page, PageExtension>;
#[test]
fn test_person() {
let user = User_ {
id: 52,
name: "thom".into(),
fedi_name: "rrf".into(),
preferred_username: None,
password_encrypted: "here".into(),
email: None,
matrix_user_id: None,
avatar: None,
published: naive_now(),
admin: false,
banned: false,
updated: None,
show_nsfw: false,
theme: "darkly".into(),
default_sort_type: SortType::Hot as i16,
default_listing_type: ListingType::Subscribed as i16,
lang: "browser".into(),
show_avatars: true,
send_notifications_to_email: false,
};
pub static APUB_JSON_CONTENT_TYPE: &str = "application/activity+json";
let person = user.as_person();
assert_eq!(
format!("https://{}/federation/u/thom", Settings::get().hostname),
person.object_props.id_string().unwrap()
);
}
#[test]
fn test_community() {
let community = Community {
id: 42,
name: "Test".into(),
title: "Test Title".into(),
description: Some("Test community".into()),
category_id: 32,
creator_id: 52,
removed: false,
published: naive_now(),
updated: Some(naive_now()),
deleted: false,
nsfw: false,
};
let group = community.as_group();
assert_eq!(
format!("https://{}/federation/c/Test", Settings::get().hostname),
group.object_props.id_string().unwrap()
);
}
#[test]
fn test_post() {
let post = Post {
id: 62,
name: "A test post".into(),
url: None,
body: None,
creator_id: 52,
community_id: 42,
published: naive_now(),
removed: false,
locked: false,
stickied: false,
nsfw: false,
deleted: false,
updated: None,
embed_title: None,
embed_description: None,
embed_html: None,
thumbnail_url: None,
};
let page = post.as_page();
assert_eq!(
format!("https://{}/federation/post/62", Settings::get().hostname),
page.object_props.id_string().unwrap()
);
}
pub enum EndpointType {
Community,
User,
Post,
Comment,
PrivateMessage,
}
pub fn make_apub_endpoint<S: Display, T: Display>(point: S, value: T) -> String {
format!(
"https://{}/federation/{}/{}",
/// Convert the data to json and turn it into an HTTP Response with the correct ActivityPub
/// headers.
fn create_apub_response<T>(data: &T) -> HttpResponse<Body>
where
T: Serialize,
{
HttpResponse::Ok()
.content_type(APUB_JSON_CONTENT_TYPE)
.json(data)
}
fn create_apub_tombstone_response<T>(data: &T) -> HttpResponse<Body>
where
T: Serialize,
{
HttpResponse::Gone()
.content_type(APUB_JSON_CONTENT_TYPE)
.json(data)
}
/// Generates the ActivityPub ID for a given object type and ID.
pub fn make_apub_endpoint(endpoint_type: EndpointType, name: &str) -> Url {
let point = match endpoint_type {
EndpointType::Community => "c",
EndpointType::User => "u",
EndpointType::Post => "post",
EndpointType::Comment => "comment",
EndpointType::PrivateMessage => "private_message",
};
Url::parse(&format!(
"{}://{}/{}/{}",
get_apub_protocol_string(),
Settings::get().hostname,
point,
value
name
))
.unwrap()
}
pub fn get_apub_protocol_string() -> &'static str {
if Settings::get().federation.tls_enabled {
"https"
} else {
"http"
}
}
// Checks if the ID has a valid format, correct scheme, and is in the allowed instance list.
fn is_apub_id_valid(apub_id: &Url) -> bool {
if apub_id.scheme() != get_apub_protocol_string() {
return false;
}
let allowed_instances: Vec<String> = Settings::get()
.federation
.allowed_instances
.split(',')
.map(|d| d.to_string())
.collect();
match apub_id.domain() {
Some(d) => allowed_instances.contains(&d.to_owned()),
None => false,
}
}
pub trait ToApub {
type Response;
fn to_apub(&self, conn: &PgConnection) -> Result<Self::Response, Error>;
fn to_tombstone(&self) -> Result<Tombstone, Error>;
}
/// Updated is actually the deletion time
fn create_tombstone(
deleted: bool,
object_id: &str,
updated: Option<NaiveDateTime>,
former_type: String,
) -> Result<Tombstone, Error> {
if deleted {
if let Some(updated) = updated {
let mut tombstone = Tombstone::new();
tombstone.set_id(object_id.parse()?);
tombstone.set_former_type(former_type);
tombstone.set_deleted(convert_datetime(updated).into());
Ok(tombstone)
} else {
Err(format_err!(
"Cant convert to tombstone because updated time was None."
))
}
} else {
Err(format_err!(
"Cant convert object to tombstone if it wasnt deleted"
))
}
}
pub trait FromApub {
type ApubType;
fn from_apub(apub: &Self::ApubType, conn: &PgConnection) -> Result<Self, Error>
where
Self: Sized;
}
pub trait ApubObjectType {
fn send_create(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error>;
fn send_update(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error>;
fn send_delete(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error>;
fn send_undo_delete(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error>;
fn send_remove(&self, mod_: &User_, conn: &PgConnection) -> Result<(), Error>;
fn send_undo_remove(&self, mod_: &User_, conn: &PgConnection) -> Result<(), Error>;
}
pub trait ApubLikeableType {
fn send_like(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error>;
fn send_dislike(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error>;
fn send_undo_like(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error>;
}
pub fn get_shared_inbox(actor_id: &str) -> String {
let url = Url::parse(actor_id).unwrap();
format!(
"{}://{}{}/inbox",
&url.scheme(),
&url.host_str().unwrap(),
if let Some(port) = url.port() {
format!(":{}", port)
} else {
"".to_string()
},
)
}
pub trait ActorType {
fn actor_id(&self) -> String;
fn public_key(&self) -> String;
fn private_key(&self) -> String;
// These two have default impls, since currently a community can't follow anything,
// and a user can't be followed (yet)
#[allow(unused_variables)]
fn send_follow(&self, follow_actor_id: &str, conn: &PgConnection) -> Result<(), Error>;
fn send_unfollow(&self, follow_actor_id: &str, conn: &PgConnection) -> Result<(), Error>;
#[allow(unused_variables)]
fn send_accept_follow(&self, follow: &Follow, conn: &PgConnection) -> Result<(), Error>;
fn send_delete(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error>;
fn send_undo_delete(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error>;
fn send_remove(&self, mod_: &User_, conn: &PgConnection) -> Result<(), Error>;
fn send_undo_remove(&self, mod_: &User_, conn: &PgConnection) -> Result<(), Error>;
/// For a given community, returns the inboxes of all followers.
fn get_follower_inboxes(&self, conn: &PgConnection) -> Result<Vec<String>, Error>;
// TODO move these to the db rows
fn get_inbox_url(&self) -> String {
format!("{}/inbox", &self.actor_id())
}
fn get_shared_inbox_url(&self) -> String {
get_shared_inbox(&self.actor_id())
}
fn get_outbox_url(&self) -> String {
format!("{}/outbox", &self.actor_id())
}
fn get_followers_url(&self) -> String {
format!("{}/followers", &self.actor_id())
}
fn get_following_url(&self) -> String {
format!("{}/following", &self.actor_id())
}
fn get_liked_url(&self) -> String {
format!("{}/liked", &self.actor_id())
}
fn get_public_key_ext(&self) -> PublicKeyExtension {
PublicKey {
id: format!("{}#main-key", self.actor_id()),
owner: self.actor_id(),
public_key_pem: self.public_key(),
}
.to_ext()
}
}
pub fn fetch_webfinger_url(mention: &MentionData) -> Result<String, Error> {
let fetch_url = format!(
"{}://{}/.well-known/webfinger?resource=acct:{}@{}",
get_apub_protocol_string(),
mention.domain,
mention.name,
mention.domain
);
debug!("Fetching webfinger url: {}", &fetch_url);
let text: String = attohttpc::get(&fetch_url).send()?.text()?;
let res: WebFingerResponse = serde_json::from_str(&text)?;
let link = res
.links
.iter()
.find(|l| l.type_.eq(&Some("application/activity+json".to_string())))
.ok_or_else(|| format_err!("No application/activity+json link found."))?;
link
.href
.to_owned()
.ok_or_else(|| format_err!("No href found."))
}

View file

@ -1,38 +1,518 @@
use crate::apub::make_apub_endpoint;
use crate::db::post::Post;
use crate::to_datetime_utc;
use activitypub::{context, object::Page};
use crate::{
apub::{
activities::{populate_object_props, send_activity_to_community},
create_apub_response,
create_apub_tombstone_response,
create_tombstone,
extensions::page_extension::PageExtension,
fetcher::{get_or_fetch_and_upsert_remote_community, get_or_fetch_and_upsert_remote_user},
get_apub_protocol_string,
ActorType,
ApubLikeableType,
ApubObjectType,
FromApub,
PageExt,
ToApub,
},
convert_datetime,
db::{
community::Community,
post::{Post, PostForm},
user::User_,
Crud,
},
routes::DbPoolParam,
Settings,
};
use activitystreams::{
activity::{Create, Delete, Dislike, Like, Remove, Undo, Update},
context,
object::{kind::PageType, properties::ObjectProperties, AnyImage, Image, Page},
BaseBox,
};
use activitystreams_ext::Ext1;
use activitystreams_new::object::Tombstone;
use actix_web::{body::Body, web::Path, HttpResponse, Result};
use diesel::PgConnection;
use failure::Error;
use serde::Deserialize;
impl Post {
pub fn as_page(&self) -> Page {
let base_url = make_apub_endpoint("post", self.id);
let mut page = Page::default();
#[derive(Deserialize)]
pub struct PostQuery {
post_id: String,
}
page.object_props.set_context_object(context()).ok();
page.object_props.set_id_string(base_url).ok();
page.object_props.set_name_string(self.name.to_owned()).ok();
if let Some(body) = &self.body {
page.object_props.set_content_string(body.to_owned()).ok();
}
if let Some(url) = &self.url {
page.object_props.set_url_string(url.to_owned()).ok();
}
//page.object_props.set_attributed_to_string
page
.object_props
.set_published_utctime(to_datetime_utc(self.published))
.ok();
if let Some(updated) = self.updated {
page
.object_props
.set_updated_utctime(to_datetime_utc(updated))
.ok();
}
page
/// Return the post json over HTTP.
pub async fn get_apub_post(
info: Path<PostQuery>,
db: DbPoolParam,
) -> Result<HttpResponse<Body>, Error> {
let id = info.post_id.parse::<i32>()?;
let post = Post::read(&&db.get()?, id)?;
if !post.deleted {
Ok(create_apub_response(&post.to_apub(&db.get().unwrap())?))
} else {
Ok(create_apub_tombstone_response(&post.to_tombstone()?))
}
}
impl ToApub for Post {
type Response = PageExt;
// Turn a Lemmy post into an ActivityPub page that can be sent out over the network.
fn to_apub(&self, conn: &PgConnection) -> Result<PageExt, Error> {
let mut page = Page::default();
let oprops: &mut ObjectProperties = page.as_mut();
let creator = User_::read(conn, self.creator_id)?;
let community = Community::read(conn, self.community_id)?;
oprops
// Not needed when the Post is embedded in a collection (like for community outbox)
// TODO: need to set proper context defining sensitive/commentsEnabled fields
// https://git.asonix.dog/Aardwolf/activitystreams/issues/5
.set_context_xsd_any_uri(context())?
.set_id(self.ap_id.to_owned())?
// Use summary field to be consistent with mastodon content warning.
// https://mastodon.xyz/@Louisa/103987265222901387.json
.set_summary_xsd_string(self.name.to_owned())?
.set_published(convert_datetime(self.published))?
.set_to_xsd_any_uri(community.actor_id)?
.set_attributed_to_xsd_any_uri(creator.actor_id)?;
if let Some(body) = &self.body {
oprops.set_content_xsd_string(body.to_owned())?;
}
// TODO: hacky code because we get self.url == Some("")
// https://github.com/LemmyNet/lemmy/issues/602
let url = self.url.as_ref().filter(|u| !u.is_empty());
if let Some(u) = url {
oprops.set_url_xsd_any_uri(u.to_owned())?;
// Embeds
let mut page_preview = Page::new();
page_preview
.object_props
.set_url_xsd_any_uri(u.to_owned())?;
if let Some(embed_title) = &self.embed_title {
page_preview
.object_props
.set_name_xsd_string(embed_title.to_owned())?;
}
if let Some(embed_description) = &self.embed_description {
page_preview
.object_props
.set_summary_xsd_string(embed_description.to_owned())?;
}
if let Some(embed_html) = &self.embed_html {
page_preview
.object_props
.set_content_xsd_string(embed_html.to_owned())?;
}
oprops.set_preview_base_box(page_preview)?;
}
if let Some(thumbnail_url) = &self.thumbnail_url {
let full_url = format!(
"{}://{}/pictshare/{}",
get_apub_protocol_string(),
Settings::get().hostname,
thumbnail_url
);
let mut image = Image::new();
image.object_props.set_url_xsd_any_uri(full_url)?;
let any_image = AnyImage::from_concrete(image)?;
oprops.set_image_any_image(any_image)?;
}
if let Some(u) = self.updated {
oprops.set_updated(convert_datetime(u))?;
}
let ext = PageExtension {
comments_enabled: !self.locked,
sensitive: self.nsfw,
};
Ok(Ext1::new(page, ext))
}
fn to_tombstone(&self) -> Result<Tombstone, Error> {
create_tombstone(
self.deleted,
&self.ap_id,
self.updated,
PageType.to_string(),
)
}
}
impl FromApub for PostForm {
type ApubType = PageExt;
/// Parse an ActivityPub page received from another instance into a Lemmy post.
fn from_apub(page: &PageExt, conn: &PgConnection) -> Result<PostForm, Error> {
let ext = &page.ext_one;
let oprops = &page.inner.object_props;
let creator_actor_id = &oprops.get_attributed_to_xsd_any_uri().unwrap().to_string();
let creator = get_or_fetch_and_upsert_remote_user(&creator_actor_id, &conn)?;
let community_actor_id = &oprops.get_to_xsd_any_uri().unwrap().to_string();
let community = get_or_fetch_and_upsert_remote_community(&community_actor_id, &conn)?;
let thumbnail_url = match oprops.get_image_any_image() {
Some(any_image) => any_image
.to_owned()
.into_concrete::<Image>()?
.object_props
.get_url_xsd_any_uri()
.map(|u| u.to_string()),
None => None,
};
let url = oprops.get_url_xsd_any_uri().map(|u| u.to_string());
let (embed_title, embed_description, embed_html) = match oprops.get_preview_base_box() {
Some(preview) => {
let preview_page = preview.to_owned().into_concrete::<Page>()?;
let name = preview_page
.object_props
.get_name_xsd_string()
.map(|n| n.to_string());
let summary = preview_page
.object_props
.get_summary_xsd_string()
.map(|s| s.to_string());
let content = preview_page
.object_props
.get_content_xsd_string()
.map(|c| c.to_string());
(name, summary, content)
}
None => (None, None, None),
};
Ok(PostForm {
name: oprops.get_summary_xsd_string().unwrap().to_string(),
url,
body: oprops.get_content_xsd_string().map(|c| c.to_string()),
creator_id: creator.id,
community_id: community.id,
removed: None,
locked: Some(!ext.comments_enabled),
published: oprops
.get_published()
.map(|u| u.as_ref().to_owned().naive_local()),
updated: oprops
.get_updated()
.map(|u| u.as_ref().to_owned().naive_local()),
deleted: None,
nsfw: ext.sensitive,
stickied: None, // -> put it in "featured" collection of the community
embed_title,
embed_description,
embed_html,
thumbnail_url,
ap_id: oprops.get_id().unwrap().to_string(),
local: false,
})
}
}
impl ApubObjectType for Post {
/// Send out information about a newly created post, to the followers of the community.
fn send_create(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error> {
let page = self.to_apub(conn)?;
let community = Community::read(conn, self.community_id)?;
let id = format!("{}/create/{}", self.ap_id, uuid::Uuid::new_v4());
let mut create = Create::new();
populate_object_props(
&mut create.object_props,
vec![community.get_followers_url()],
&id,
)?;
create
.create_props
.set_actor_xsd_any_uri(creator.actor_id.to_owned())?
.set_object_base_box(BaseBox::from_concrete(page)?)?;
send_activity_to_community(
creator,
conn,
&community,
vec![community.get_shared_inbox_url()],
create,
)?;
Ok(())
}
/// Send out information about an edited post, to the followers of the community.
fn send_update(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error> {
let page = self.to_apub(conn)?;
let community = Community::read(conn, self.community_id)?;
let id = format!("{}/update/{}", self.ap_id, uuid::Uuid::new_v4());
let mut update = Update::new();
populate_object_props(
&mut update.object_props,
vec![community.get_followers_url()],
&id,
)?;
update
.update_props
.set_actor_xsd_any_uri(creator.actor_id.to_owned())?
.set_object_base_box(BaseBox::from_concrete(page)?)?;
send_activity_to_community(
creator,
conn,
&community,
vec![community.get_shared_inbox_url()],
update,
)?;
Ok(())
}
fn send_delete(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error> {
let page = self.to_apub(conn)?;
let community = Community::read(conn, self.community_id)?;
let id = format!("{}/delete/{}", self.ap_id, uuid::Uuid::new_v4());
let mut delete = Delete::default();
populate_object_props(
&mut delete.object_props,
vec![community.get_followers_url()],
&id,
)?;
delete
.delete_props
.set_actor_xsd_any_uri(creator.actor_id.to_owned())?
.set_object_base_box(BaseBox::from_concrete(page)?)?;
let community = Community::read(conn, self.community_id)?;
send_activity_to_community(
creator,
conn,
&community,
vec![community.get_shared_inbox_url()],
delete,
)?;
Ok(())
}
fn send_undo_delete(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error> {
let page = self.to_apub(conn)?;
let community = Community::read(conn, self.community_id)?;
let id = format!("{}/delete/{}", self.ap_id, uuid::Uuid::new_v4());
let mut delete = Delete::default();
populate_object_props(
&mut delete.object_props,
vec![community.get_followers_url()],
&id,
)?;
delete
.delete_props
.set_actor_xsd_any_uri(creator.actor_id.to_owned())?
.set_object_base_box(BaseBox::from_concrete(page)?)?;
// TODO
// Undo that fake activity
let undo_id = format!("{}/undo/delete/{}", self.ap_id, uuid::Uuid::new_v4());
let mut undo = Undo::default();
populate_object_props(
&mut undo.object_props,
vec![community.get_followers_url()],
&undo_id,
)?;
undo
.undo_props
.set_actor_xsd_any_uri(creator.actor_id.to_owned())?
.set_object_base_box(delete)?;
let community = Community::read(conn, self.community_id)?;
send_activity_to_community(
creator,
conn,
&community,
vec![community.get_shared_inbox_url()],
undo,
)?;
Ok(())
}
fn send_remove(&self, mod_: &User_, conn: &PgConnection) -> Result<(), Error> {
let page = self.to_apub(conn)?;
let community = Community::read(conn, self.community_id)?;
let id = format!("{}/remove/{}", self.ap_id, uuid::Uuid::new_v4());
let mut remove = Remove::default();
populate_object_props(
&mut remove.object_props,
vec![community.get_followers_url()],
&id,
)?;
remove
.remove_props
.set_actor_xsd_any_uri(mod_.actor_id.to_owned())?
.set_object_base_box(BaseBox::from_concrete(page)?)?;
let community = Community::read(conn, self.community_id)?;
send_activity_to_community(
mod_,
conn,
&community,
vec![community.get_shared_inbox_url()],
remove,
)?;
Ok(())
}
fn send_undo_remove(&self, mod_: &User_, conn: &PgConnection) -> Result<(), Error> {
let page = self.to_apub(conn)?;
let community = Community::read(conn, self.community_id)?;
let id = format!("{}/remove/{}", self.ap_id, uuid::Uuid::new_v4());
let mut remove = Remove::default();
populate_object_props(
&mut remove.object_props,
vec![community.get_followers_url()],
&id,
)?;
remove
.remove_props
.set_actor_xsd_any_uri(mod_.actor_id.to_owned())?
.set_object_base_box(BaseBox::from_concrete(page)?)?;
// Undo that fake activity
let undo_id = format!("{}/undo/remove/{}", self.ap_id, uuid::Uuid::new_v4());
let mut undo = Undo::default();
populate_object_props(
&mut undo.object_props,
vec![community.get_followers_url()],
&undo_id,
)?;
undo
.undo_props
.set_actor_xsd_any_uri(mod_.actor_id.to_owned())?
.set_object_base_box(remove)?;
let community = Community::read(conn, self.community_id)?;
send_activity_to_community(
mod_,
conn,
&community,
vec![community.get_shared_inbox_url()],
undo,
)?;
Ok(())
}
}
impl ApubLikeableType for Post {
fn send_like(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error> {
let page = self.to_apub(conn)?;
let community = Community::read(conn, self.community_id)?;
let id = format!("{}/like/{}", self.ap_id, uuid::Uuid::new_v4());
let mut like = Like::new();
populate_object_props(
&mut like.object_props,
vec![community.get_followers_url()],
&id,
)?;
like
.like_props
.set_actor_xsd_any_uri(creator.actor_id.to_owned())?
.set_object_base_box(BaseBox::from_concrete(page)?)?;
send_activity_to_community(
&creator,
&conn,
&community,
vec![community.get_shared_inbox_url()],
like,
)?;
Ok(())
}
fn send_dislike(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error> {
let page = self.to_apub(conn)?;
let community = Community::read(conn, self.community_id)?;
let id = format!("{}/dislike/{}", self.ap_id, uuid::Uuid::new_v4());
let mut dislike = Dislike::new();
populate_object_props(
&mut dislike.object_props,
vec![community.get_followers_url()],
&id,
)?;
dislike
.dislike_props
.set_actor_xsd_any_uri(creator.actor_id.to_owned())?
.set_object_base_box(BaseBox::from_concrete(page)?)?;
send_activity_to_community(
&creator,
&conn,
&community,
vec![community.get_shared_inbox_url()],
dislike,
)?;
Ok(())
}
fn send_undo_like(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error> {
let page = self.to_apub(conn)?;
let community = Community::read(conn, self.community_id)?;
let id = format!("{}/like/{}", self.ap_id, uuid::Uuid::new_v4());
let mut like = Like::new();
populate_object_props(
&mut like.object_props,
vec![community.get_followers_url()],
&id,
)?;
like
.like_props
.set_actor_xsd_any_uri(creator.actor_id.to_owned())?
.set_object_base_box(BaseBox::from_concrete(page)?)?;
// TODO
// Undo that fake activity
let undo_id = format!("{}/undo/like/{}", self.ap_id, uuid::Uuid::new_v4());
let mut undo = Undo::default();
populate_object_props(
&mut undo.object_props,
vec![community.get_followers_url()],
&undo_id,
)?;
undo
.undo_props
.set_actor_xsd_any_uri(creator.actor_id.to_owned())?
.set_object_base_box(like)?;
send_activity_to_community(
&creator,
&conn,
&community,
vec![community.get_shared_inbox_url()],
undo,
)?;
Ok(())
}
}

View file

@ -0,0 +1,211 @@
use crate::{
apub::{
activities::send_activity,
create_tombstone,
fetcher::get_or_fetch_and_upsert_remote_user,
ApubObjectType,
FromApub,
ToApub,
},
convert_datetime,
db::{
activity::insert_activity,
private_message::{PrivateMessage, PrivateMessageForm},
user::User_,
Crud,
},
};
use activitystreams::{
activity::{Create, Delete, Undo, Update},
context,
object::{kind::NoteType, properties::ObjectProperties, Note},
};
use activitystreams_new::object::Tombstone;
use actix_web::Result;
use diesel::PgConnection;
use failure::Error;
impl ToApub for PrivateMessage {
type Response = Note;
fn to_apub(&self, conn: &PgConnection) -> Result<Note, Error> {
let mut private_message = Note::default();
let oprops: &mut ObjectProperties = private_message.as_mut();
let creator = User_::read(&conn, self.creator_id)?;
let recipient = User_::read(&conn, self.recipient_id)?;
oprops
.set_context_xsd_any_uri(context())?
.set_id(self.ap_id.to_owned())?
.set_published(convert_datetime(self.published))?
.set_content_xsd_string(self.content.to_owned())?
.set_to_xsd_any_uri(recipient.actor_id)?
.set_attributed_to_xsd_any_uri(creator.actor_id)?;
if let Some(u) = self.updated {
oprops.set_updated(convert_datetime(u))?;
}
Ok(private_message)
}
fn to_tombstone(&self) -> Result<Tombstone, Error> {
create_tombstone(
self.deleted,
&self.ap_id,
self.updated,
NoteType.to_string(),
)
}
}
impl FromApub for PrivateMessageForm {
type ApubType = Note;
/// Parse an ActivityPub note received from another instance into a Lemmy Private message
fn from_apub(note: &Note, conn: &PgConnection) -> Result<PrivateMessageForm, Error> {
let oprops = &note.object_props;
let creator_actor_id = &oprops.get_attributed_to_xsd_any_uri().unwrap().to_string();
let creator = get_or_fetch_and_upsert_remote_user(&creator_actor_id, &conn)?;
let recipient_actor_id = &oprops.get_to_xsd_any_uri().unwrap().to_string();
let recipient = get_or_fetch_and_upsert_remote_user(&recipient_actor_id, &conn)?;
Ok(PrivateMessageForm {
creator_id: creator.id,
recipient_id: recipient.id,
content: oprops
.get_content_xsd_string()
.map(|c| c.to_string())
.unwrap(),
published: oprops
.get_published()
.map(|u| u.as_ref().to_owned().naive_local()),
updated: oprops
.get_updated()
.map(|u| u.as_ref().to_owned().naive_local()),
deleted: None,
read: None,
ap_id: oprops.get_id().unwrap().to_string(),
local: false,
})
}
}
impl ApubObjectType for PrivateMessage {
/// Send out information about a newly created private message
fn send_create(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error> {
let note = self.to_apub(conn)?;
let id = format!("{}/create/{}", self.ap_id, uuid::Uuid::new_v4());
let recipient = User_::read(&conn, self.recipient_id)?;
let mut create = Create::new();
create
.object_props
.set_context_xsd_any_uri(context())?
.set_id(id)?;
let to = format!("{}/inbox", recipient.actor_id);
create
.create_props
.set_actor_xsd_any_uri(creator.actor_id.to_owned())?
.set_object_base_box(note)?;
insert_activity(&conn, creator.id, &create, true)?;
send_activity(&create, creator, vec![to])?;
Ok(())
}
/// Send out information about an edited post, to the followers of the community.
fn send_update(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error> {
let note = self.to_apub(conn)?;
let id = format!("{}/update/{}", self.ap_id, uuid::Uuid::new_v4());
let recipient = User_::read(&conn, self.recipient_id)?;
let mut update = Update::new();
update
.object_props
.set_context_xsd_any_uri(context())?
.set_id(id)?;
let to = format!("{}/inbox", recipient.actor_id);
update
.update_props
.set_actor_xsd_any_uri(creator.actor_id.to_owned())?
.set_object_base_box(note)?;
insert_activity(&conn, creator.id, &update, true)?;
send_activity(&update, creator, vec![to])?;
Ok(())
}
fn send_delete(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error> {
let note = self.to_apub(conn)?;
let id = format!("{}/delete/{}", self.ap_id, uuid::Uuid::new_v4());
let recipient = User_::read(&conn, self.recipient_id)?;
let mut delete = Delete::new();
delete
.object_props
.set_context_xsd_any_uri(context())?
.set_id(id)?;
let to = format!("{}/inbox", recipient.actor_id);
delete
.delete_props
.set_actor_xsd_any_uri(creator.actor_id.to_owned())?
.set_object_base_box(note)?;
insert_activity(&conn, creator.id, &delete, true)?;
send_activity(&delete, creator, vec![to])?;
Ok(())
}
fn send_undo_delete(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error> {
let note = self.to_apub(conn)?;
let id = format!("{}/delete/{}", self.ap_id, uuid::Uuid::new_v4());
let recipient = User_::read(&conn, self.recipient_id)?;
let mut delete = Delete::new();
delete
.object_props
.set_context_xsd_any_uri(context())?
.set_id(id)?;
let to = format!("{}/inbox", recipient.actor_id);
delete
.delete_props
.set_actor_xsd_any_uri(creator.actor_id.to_owned())?
.set_object_base_box(note)?;
// TODO
// Undo that fake activity
let undo_id = format!("{}/undo/delete/{}", self.ap_id, uuid::Uuid::new_v4());
let mut undo = Undo::default();
undo
.object_props
.set_context_xsd_any_uri(context())?
.set_id(undo_id)?;
undo
.undo_props
.set_actor_xsd_any_uri(creator.actor_id.to_owned())?
.set_object_base_box(delete)?;
insert_activity(&conn, creator.id, &undo, true)?;
send_activity(&undo, creator, vec![to])?;
Ok(())
}
fn send_remove(&self, _mod_: &User_, _conn: &PgConnection) -> Result<(), Error> {
unimplemented!()
}
fn send_undo_remove(&self, _mod_: &User_, _conn: &PgConnection) -> Result<(), Error> {
unimplemented!()
}
}

File diff suppressed because it is too large Load diff

View file

@ -1,74 +1,219 @@
use crate::apub::make_apub_endpoint;
use crate::db::establish_unpooled_connection;
use crate::db::user::User_;
use crate::to_datetime_utc;
use activitypub::{actor::Person, context};
use actix_web::body::Body;
use actix_web::web::Path;
use actix_web::HttpResponse;
use crate::{
apub::{
activities::send_activity,
create_apub_response,
extensions::signatures::PublicKey,
ActorType,
FromApub,
PersonExt,
ToApub,
},
convert_datetime,
db::{
activity::insert_activity,
user::{UserForm, User_},
},
naive_now,
routes::DbPoolParam,
};
use activitystreams::{
actor::{properties::ApActorProperties, Person},
context,
endpoint::EndpointProperties,
object::{properties::ObjectProperties, AnyImage, Image},
primitives::XsdAnyUri,
};
use activitystreams_ext::Ext2;
use activitystreams_new::{
activity::{Follow, Undo},
object::Tombstone,
prelude::*,
};
use actix_web::{body::Body, web::Path, HttpResponse, Result};
use diesel::PgConnection;
use failure::Error;
use serde::Deserialize;
impl User_ {
pub fn as_person(&self) -> Person {
let base_url = make_apub_endpoint("u", &self.name);
let mut person = Person::default();
person.object_props.set_context_object(context()).ok();
person.object_props.set_id_string(base_url.to_string()).ok();
person
.object_props
.set_name_string(self.name.to_owned())
.ok();
person
.object_props
.set_published_utctime(to_datetime_utc(self.published))
.ok();
if let Some(updated) = self.updated {
person
.object_props
.set_updated_utctime(to_datetime_utc(updated))
.ok();
}
person
.ap_actor_props
.set_inbox_string(format!("{}/inbox", &base_url))
.ok();
person
.ap_actor_props
.set_outbox_string(format!("{}/outbox", &base_url))
.ok();
person
.ap_actor_props
.set_following_string(format!("{}/following", &base_url))
.ok();
person
.ap_actor_props
.set_liked_string(format!("{}/liked", &base_url))
.ok();
if let Some(i) = &self.preferred_username {
person
.ap_actor_props
.set_preferred_username_string(i.to_string())
.ok();
}
person
}
}
#[derive(Deserialize)]
pub struct UserQuery {
user_name: String,
}
pub async fn get_apub_user(info: Path<UserQuery>) -> HttpResponse<Body> {
let connection = establish_unpooled_connection();
impl ToApub for User_ {
type Response = PersonExt;
if let Ok(user) = User_::find_by_email_or_username(&connection, &info.user_name) {
HttpResponse::Ok()
.content_type("application/activity+json")
.body(serde_json::to_string(&user.as_person()).unwrap())
} else {
HttpResponse::NotFound().finish()
// Turn a Lemmy Community into an ActivityPub group that can be sent out over the network.
fn to_apub(&self, _conn: &PgConnection) -> Result<PersonExt, Error> {
// TODO go through all these to_string and to_owned()
let mut person = Person::default();
let oprops: &mut ObjectProperties = person.as_mut();
oprops
.set_context_xsd_any_uri(context())?
.set_id(self.actor_id.to_string())?
.set_name_xsd_string(self.name.to_owned())?
.set_published(convert_datetime(self.published))?;
if let Some(u) = self.updated {
oprops.set_updated(convert_datetime(u))?;
}
if let Some(i) = &self.preferred_username {
oprops.set_name_xsd_string(i.to_owned())?;
}
if let Some(avatar_url) = &self.avatar {
let mut image = Image::new();
image
.object_props
.set_url_xsd_any_uri(avatar_url.to_owned())?;
let any_image = AnyImage::from_concrete(image)?;
oprops.set_icon_any_image(any_image)?;
}
let mut endpoint_props = EndpointProperties::default();
endpoint_props.set_shared_inbox(self.get_shared_inbox_url())?;
let mut actor_props = ApActorProperties::default();
actor_props
.set_inbox(self.get_inbox_url())?
.set_outbox(self.get_outbox_url())?
.set_endpoints(endpoint_props)?
.set_followers(self.get_followers_url())?
.set_following(self.get_following_url())?
.set_liked(self.get_liked_url())?;
Ok(Ext2::new(person, actor_props, self.get_public_key_ext()))
}
fn to_tombstone(&self) -> Result<Tombstone, Error> {
unimplemented!()
}
}
impl ActorType for User_ {
fn actor_id(&self) -> String {
self.actor_id.to_owned()
}
fn public_key(&self) -> String {
self.public_key.to_owned().unwrap()
}
fn private_key(&self) -> String {
self.private_key.to_owned().unwrap()
}
/// As a given local user, send out a follow request to a remote community.
fn send_follow(&self, follow_actor_id: &str, conn: &PgConnection) -> Result<(), Error> {
let id = format!("{}/follow/{}", self.actor_id, uuid::Uuid::new_v4());
let mut follow = Follow::new(self.actor_id.to_owned(), follow_actor_id);
follow.set_context(context()).set_id(id.parse()?);
let to = format!("{}/inbox", follow_actor_id);
insert_activity(&conn, self.id, &follow, true)?;
send_activity(&follow, self, vec![to])?;
Ok(())
}
fn send_unfollow(&self, follow_actor_id: &str, conn: &PgConnection) -> Result<(), Error> {
let id = format!("{}/follow/{}", self.actor_id, uuid::Uuid::new_v4());
let mut follow = Follow::new(self.actor_id.to_owned(), follow_actor_id);
follow.set_context(context()).set_id(id.parse()?);
let to = format!("{}/inbox", follow_actor_id);
// TODO
// Undo that fake activity
let undo_id = format!("{}/undo/follow/{}", self.actor_id, uuid::Uuid::new_v4());
let mut undo = Undo::new(self.actor_id.parse::<XsdAnyUri>()?, follow.into_any_base()?);
undo.set_context(context()).set_id(undo_id.parse()?);
insert_activity(&conn, self.id, &undo, true)?;
send_activity(&undo, self, vec![to])?;
Ok(())
}
fn send_delete(&self, _creator: &User_, _conn: &PgConnection) -> Result<(), Error> {
unimplemented!()
}
fn send_undo_delete(&self, _creator: &User_, _conn: &PgConnection) -> Result<(), Error> {
unimplemented!()
}
fn send_remove(&self, _creator: &User_, _conn: &PgConnection) -> Result<(), Error> {
unimplemented!()
}
fn send_undo_remove(&self, _creator: &User_, _conn: &PgConnection) -> Result<(), Error> {
unimplemented!()
}
fn send_accept_follow(&self, _follow: &Follow, _conn: &PgConnection) -> Result<(), Error> {
unimplemented!()
}
fn get_follower_inboxes(&self, _conn: &PgConnection) -> Result<Vec<String>, Error> {
unimplemented!()
}
}
impl FromApub for UserForm {
type ApubType = PersonExt;
/// Parse an ActivityPub person received from another instance into a Lemmy user.
fn from_apub(person: &PersonExt, _conn: &PgConnection) -> Result<Self, Error> {
let oprops = &person.inner.object_props;
let aprops = &person.ext_one;
let public_key: &PublicKey = &person.ext_two.public_key;
let avatar = match oprops.get_icon_any_image() {
Some(any_image) => any_image
.to_owned()
.into_concrete::<Image>()?
.object_props
.get_url_xsd_any_uri()
.map(|u| u.to_string()),
None => None,
};
Ok(UserForm {
name: oprops.get_name_xsd_string().unwrap().to_string(),
preferred_username: aprops.get_preferred_username().map(|u| u.to_string()),
password_encrypted: "".to_string(),
admin: false,
banned: false,
email: None,
avatar,
updated: oprops
.get_updated()
.map(|u| u.as_ref().to_owned().naive_local()),
show_nsfw: false,
theme: "".to_string(),
default_sort_type: 0,
default_listing_type: 0,
lang: "".to_string(),
show_avatars: false,
send_notifications_to_email: false,
matrix_user_id: None,
actor_id: oprops.get_id().unwrap().to_string(),
bio: oprops.get_summary_xsd_string().map(|s| s.to_string()),
local: false,
private_key: None,
public_key: Some(public_key.to_owned().public_key_pem),
last_refreshed_at: Some(naive_now()),
})
}
}
/// Return the user json over HTTP.
pub async fn get_apub_user_http(
info: Path<UserQuery>,
db: DbPoolParam,
) -> Result<HttpResponse<Body>, Error> {
let user = User_::find_by_email_or_username(&&db.get()?, &info.user_name)?;
let u = user.to_apub(&db.get().unwrap())?;
Ok(create_apub_response(&u))
}

View file

@ -0,0 +1,312 @@
use crate::{
api::user::PrivateMessageResponse,
apub::{
extensions::signatures::verify,
fetcher::{get_or_fetch_and_upsert_remote_community, get_or_fetch_and_upsert_remote_user},
FromApub,
},
db::{
activity::insert_activity,
community::{CommunityFollower, CommunityFollowerForm},
private_message::{PrivateMessage, PrivateMessageForm},
private_message_view::PrivateMessageView,
user::User_,
Crud,
Followable,
},
naive_now,
routes::{ChatServerParam, DbPoolParam},
websocket::{server::SendUserRoomMessage, UserOperation},
};
use activitystreams::{
activity::{Accept, Create, Delete, Undo, Update},
object::Note,
};
use actix_web::{web, HttpRequest, HttpResponse, Result};
use diesel::PgConnection;
use failure::{Error, _core::fmt::Debug};
use log::debug;
use serde::Deserialize;
#[serde(untagged)]
#[derive(Deserialize, Debug)]
pub enum UserAcceptedObjects {
Accept(Box<Accept>),
Create(Box<Create>),
Update(Box<Update>),
Delete(Box<Delete>),
Undo(Box<Undo>),
}
/// Handler for all incoming activities to user inboxes.
pub async fn user_inbox(
request: HttpRequest,
input: web::Json<UserAcceptedObjects>,
path: web::Path<String>,
db: DbPoolParam,
chat_server: ChatServerParam,
) -> Result<HttpResponse, Error> {
// TODO: would be nice if we could do the signature check here, but we cant access the actor property
let input = input.into_inner();
let conn = &db.get().unwrap();
let username = path.into_inner();
debug!("User {} received activity: {:?}", &username, &input);
match input {
UserAcceptedObjects::Accept(a) => receive_accept(&a, &request, &username, &conn),
UserAcceptedObjects::Create(c) => {
receive_create_private_message(&c, &request, &conn, chat_server)
}
UserAcceptedObjects::Update(u) => {
receive_update_private_message(&u, &request, &conn, chat_server)
}
UserAcceptedObjects::Delete(d) => {
receive_delete_private_message(&d, &request, &conn, chat_server)
}
UserAcceptedObjects::Undo(u) => {
receive_undo_delete_private_message(&u, &request, &conn, chat_server)
}
}
}
/// Handle accepted follows.
fn receive_accept(
accept: &Accept,
request: &HttpRequest,
username: &str,
conn: &PgConnection,
) -> Result<HttpResponse, Error> {
let community_uri = accept
.accept_props
.get_actor_xsd_any_uri()
.unwrap()
.to_string();
let community = get_or_fetch_and_upsert_remote_community(&community_uri, conn)?;
verify(request, &community)?;
let user = User_::read_from_name(&conn, username)?;
insert_activity(&conn, community.creator_id, &accept, false)?;
// Now you need to add this to the community follower
let community_follower_form = CommunityFollowerForm {
community_id: community.id,
user_id: user.id,
};
// This will fail if they're already a follower
CommunityFollower::follow(&conn, &community_follower_form)?;
// TODO: make sure that we actually requested a follow
Ok(HttpResponse::Ok().finish())
}
fn receive_create_private_message(
create: &Create,
request: &HttpRequest,
conn: &PgConnection,
chat_server: ChatServerParam,
) -> Result<HttpResponse, Error> {
let note = create
.create_props
.get_object_base_box()
.to_owned()
.unwrap()
.to_owned()
.into_concrete::<Note>()?;
let user_uri = create
.create_props
.get_actor_xsd_any_uri()
.unwrap()
.to_string();
let user = get_or_fetch_and_upsert_remote_user(&user_uri, &conn)?;
verify(request, &user)?;
insert_activity(&conn, user.id, &create, false)?;
let private_message = PrivateMessageForm::from_apub(&note, &conn)?;
let inserted_private_message = PrivateMessage::create(&conn, &private_message)?;
let message = PrivateMessageView::read(&conn, inserted_private_message.id)?;
let res = PrivateMessageResponse {
message: message.to_owned(),
};
chat_server.do_send(SendUserRoomMessage {
op: UserOperation::CreatePrivateMessage,
response: res,
recipient_id: message.recipient_id,
my_id: None,
});
Ok(HttpResponse::Ok().finish())
}
fn receive_update_private_message(
update: &Update,
request: &HttpRequest,
conn: &PgConnection,
chat_server: ChatServerParam,
) -> Result<HttpResponse, Error> {
let note = update
.update_props
.get_object_base_box()
.to_owned()
.unwrap()
.to_owned()
.into_concrete::<Note>()?;
let user_uri = update
.update_props
.get_actor_xsd_any_uri()
.unwrap()
.to_string();
let user = get_or_fetch_and_upsert_remote_user(&user_uri, &conn)?;
verify(request, &user)?;
insert_activity(&conn, user.id, &update, false)?;
let private_message = PrivateMessageForm::from_apub(&note, &conn)?;
let private_message_id = PrivateMessage::read_from_apub_id(&conn, &private_message.ap_id)?.id;
PrivateMessage::update(conn, private_message_id, &private_message)?;
let message = PrivateMessageView::read(&conn, private_message_id)?;
let res = PrivateMessageResponse {
message: message.to_owned(),
};
chat_server.do_send(SendUserRoomMessage {
op: UserOperation::EditPrivateMessage,
response: res,
recipient_id: message.recipient_id,
my_id: None,
});
Ok(HttpResponse::Ok().finish())
}
fn receive_delete_private_message(
delete: &Delete,
request: &HttpRequest,
conn: &PgConnection,
chat_server: ChatServerParam,
) -> Result<HttpResponse, Error> {
let note = delete
.delete_props
.get_object_base_box()
.to_owned()
.unwrap()
.to_owned()
.into_concrete::<Note>()?;
let user_uri = delete
.delete_props
.get_actor_xsd_any_uri()
.unwrap()
.to_string();
let user = get_or_fetch_and_upsert_remote_user(&user_uri, &conn)?;
verify(request, &user)?;
insert_activity(&conn, user.id, &delete, false)?;
let private_message = PrivateMessageForm::from_apub(&note, &conn)?;
let private_message_id = PrivateMessage::read_from_apub_id(&conn, &private_message.ap_id)?.id;
let private_message_form = PrivateMessageForm {
content: private_message.content,
recipient_id: private_message.recipient_id,
creator_id: private_message.creator_id,
deleted: Some(true),
read: None,
ap_id: private_message.ap_id,
local: private_message.local,
published: None,
updated: Some(naive_now()),
};
PrivateMessage::update(conn, private_message_id, &private_message_form)?;
let message = PrivateMessageView::read(&conn, private_message_id)?;
let res = PrivateMessageResponse {
message: message.to_owned(),
};
chat_server.do_send(SendUserRoomMessage {
op: UserOperation::EditPrivateMessage,
response: res,
recipient_id: message.recipient_id,
my_id: None,
});
Ok(HttpResponse::Ok().finish())
}
fn receive_undo_delete_private_message(
undo: &Undo,
request: &HttpRequest,
conn: &PgConnection,
chat_server: ChatServerParam,
) -> Result<HttpResponse, Error> {
let delete = undo
.undo_props
.get_object_base_box()
.to_owned()
.unwrap()
.to_owned()
.into_concrete::<Delete>()?;
let note = delete
.delete_props
.get_object_base_box()
.to_owned()
.unwrap()
.to_owned()
.into_concrete::<Note>()?;
let user_uri = delete
.delete_props
.get_actor_xsd_any_uri()
.unwrap()
.to_string();
let user = get_or_fetch_and_upsert_remote_user(&user_uri, &conn)?;
verify(request, &user)?;
insert_activity(&conn, user.id, &delete, false)?;
let private_message = PrivateMessageForm::from_apub(&note, &conn)?;
let private_message_id = PrivateMessage::read_from_apub_id(&conn, &private_message.ap_id)?.id;
let private_message_form = PrivateMessageForm {
content: private_message.content,
recipient_id: private_message.recipient_id,
creator_id: private_message.creator_id,
deleted: Some(false),
read: None,
ap_id: private_message.ap_id,
local: private_message.local,
published: None,
updated: Some(naive_now()),
};
PrivateMessage::update(conn, private_message_id, &private_message_form)?;
let message = PrivateMessageView::read(&conn, private_message_id)?;
let res = PrivateMessageResponse {
message: message.to_owned(),
};
chat_server.do_send(SendUserRoomMessage {
op: UserOperation::EditPrivateMessage,
response: res,
recipient_id: message.recipient_id,
my_id: None,
});
Ok(HttpResponse::Ok().finish())
}

149
server/src/db/activity.rs Normal file
View file

@ -0,0 +1,149 @@
use crate::{db::Crud, schema::activity};
use diesel::{dsl::*, result::Error, *};
use failure::_core::fmt::Debug;
use log::debug;
use serde::{Deserialize, Serialize};
use serde_json::Value;
#[derive(Queryable, Identifiable, PartialEq, Debug, Serialize, Deserialize)]
#[table_name = "activity"]
pub struct Activity {
pub id: i32,
pub user_id: i32,
pub data: Value,
pub local: bool,
pub published: chrono::NaiveDateTime,
pub updated: Option<chrono::NaiveDateTime>,
}
#[derive(Insertable, AsChangeset, Clone, Serialize, Deserialize)]
#[table_name = "activity"]
pub struct ActivityForm {
pub user_id: i32,
pub data: Value,
pub local: bool,
pub updated: Option<chrono::NaiveDateTime>,
}
impl Crud<ActivityForm> for Activity {
fn read(conn: &PgConnection, activity_id: i32) -> Result<Self, Error> {
use crate::schema::activity::dsl::*;
activity.find(activity_id).first::<Self>(conn)
}
fn delete(conn: &PgConnection, activity_id: i32) -> Result<usize, Error> {
use crate::schema::activity::dsl::*;
diesel::delete(activity.find(activity_id)).execute(conn)
}
fn create(conn: &PgConnection, new_activity: &ActivityForm) -> Result<Self, Error> {
use crate::schema::activity::dsl::*;
insert_into(activity)
.values(new_activity)
.get_result::<Self>(conn)
}
fn update(
conn: &PgConnection,
activity_id: i32,
new_activity: &ActivityForm,
) -> Result<Self, Error> {
use crate::schema::activity::dsl::*;
diesel::update(activity.find(activity_id))
.set(new_activity)
.get_result::<Self>(conn)
}
}
pub fn insert_activity<T>(
conn: &PgConnection,
user_id: i32,
data: &T,
local: bool,
) -> Result<(), failure::Error>
where
T: Serialize + Debug,
{
let activity_form = ActivityForm {
user_id,
data: serde_json::to_value(&data)?,
local,
updated: None,
};
debug!("inserting activity for user {}, data {:?}", user_id, data);
Activity::create(&conn, &activity_form)?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::{super::user::*, *};
use crate::db::{establish_unpooled_connection, Crud, ListingType, SortType};
#[test]
fn test_crud() {
let conn = establish_unpooled_connection();
let creator_form = UserForm {
name: "activity_creator_pm".into(),
preferred_username: None,
password_encrypted: "nope".into(),
email: None,
matrix_user_id: None,
avatar: None,
admin: false,
banned: false,
updated: None,
show_nsfw: false,
theme: "darkly".into(),
default_sort_type: SortType::Hot as i16,
default_listing_type: ListingType::Subscribed as i16,
lang: "browser".into(),
show_avatars: true,
send_notifications_to_email: false,
actor_id: "changeme".into(),
bio: None,
local: true,
private_key: None,
public_key: None,
last_refreshed_at: None,
};
let inserted_creator = User_::create(&conn, &creator_form).unwrap();
let test_json: Value = serde_json::from_str(
r#"{
"street": "Article Circle Expressway 1",
"city": "North Pole",
"postcode": "99705",
"state": "Alaska"
}"#,
)
.unwrap();
let activity_form = ActivityForm {
user_id: inserted_creator.id,
data: test_json.to_owned(),
local: true,
updated: None,
};
let inserted_activity = Activity::create(&conn, &activity_form).unwrap();
let expected_activity = Activity {
id: inserted_activity.id,
user_id: inserted_creator.id,
data: test_json,
local: true,
published: inserted_activity.published,
updated: None,
};
let read_activity = Activity::read(&conn, inserted_activity.id).unwrap();
let num_deleted = Activity::delete(&conn, inserted_activity.id).unwrap();
User_::delete(&conn, inserted_creator.id).unwrap();
assert_eq!(expected_activity, read_activity);
assert_eq!(expected_activity, inserted_activity);
assert_eq!(1, num_deleted);
}
}

View file

@ -1,6 +1,9 @@
use super::*;
use crate::schema::category;
use crate::schema::category::dsl::*;
use crate::{
db::Crud,
schema::{category, category::dsl::*},
};
use diesel::{dsl::*, result::Error, *};
use serde::{Deserialize, Serialize};
#[derive(Queryable, Identifiable, PartialEq, Debug, Serialize, Deserialize)]
#[table_name = "category"]
@ -50,6 +53,8 @@ impl Category {
#[cfg(test)]
mod tests {
use super::*;
use crate::db::establish_unpooled_connection;
#[test]
fn test_crud() {
let conn = establish_unpooled_connection();

View file

@ -0,0 +1,173 @@
// This is for db migrations that require code
use super::{
comment::Comment,
community::{Community, CommunityForm},
post::Post,
private_message::PrivateMessage,
user::{UserForm, User_},
};
use crate::{
apub::{extensions::signatures::generate_actor_keypair, make_apub_endpoint, EndpointType},
db::Crud,
naive_now,
};
use diesel::*;
use failure::Error;
use log::info;
pub fn run_advanced_migrations(conn: &PgConnection) -> Result<(), Error> {
user_updates_2020_04_02(conn)?;
community_updates_2020_04_02(conn)?;
post_updates_2020_04_03(conn)?;
comment_updates_2020_04_03(conn)?;
private_message_updates_2020_05_05(conn)?;
Ok(())
}
fn user_updates_2020_04_02(conn: &PgConnection) -> Result<(), Error> {
use crate::schema::user_::dsl::*;
info!("Running user_updates_2020_04_02");
// Update the actor_id, private_key, and public_key, last_refreshed_at
let incorrect_users = user_
.filter(actor_id.eq("changeme"))
.filter(local.eq(true))
.load::<User_>(conn)?;
for cuser in &incorrect_users {
let keypair = generate_actor_keypair()?;
let form = UserForm {
name: cuser.name.to_owned(),
email: cuser.email.to_owned(),
matrix_user_id: cuser.matrix_user_id.to_owned(),
avatar: cuser.avatar.to_owned(),
password_encrypted: cuser.password_encrypted.to_owned(),
preferred_username: cuser.preferred_username.to_owned(),
updated: None,
admin: cuser.admin,
banned: cuser.banned,
show_nsfw: cuser.show_nsfw,
theme: cuser.theme.to_owned(),
default_sort_type: cuser.default_sort_type,
default_listing_type: cuser.default_listing_type,
lang: cuser.lang.to_owned(),
show_avatars: cuser.show_avatars,
send_notifications_to_email: cuser.send_notifications_to_email,
actor_id: make_apub_endpoint(EndpointType::User, &cuser.name).to_string(),
bio: cuser.bio.to_owned(),
local: cuser.local,
private_key: Some(keypair.private_key),
public_key: Some(keypair.public_key),
last_refreshed_at: Some(naive_now()),
};
User_::update(&conn, cuser.id, &form)?;
}
info!("{} user rows updated.", incorrect_users.len());
Ok(())
}
fn community_updates_2020_04_02(conn: &PgConnection) -> Result<(), Error> {
use crate::schema::community::dsl::*;
info!("Running community_updates_2020_04_02");
// Update the actor_id, private_key, and public_key, last_refreshed_at
let incorrect_communities = community
.filter(actor_id.eq("changeme"))
.filter(local.eq(true))
.load::<Community>(conn)?;
for ccommunity in &incorrect_communities {
let keypair = generate_actor_keypair()?;
let form = CommunityForm {
name: ccommunity.name.to_owned(),
title: ccommunity.title.to_owned(),
description: ccommunity.description.to_owned(),
category_id: ccommunity.category_id,
creator_id: ccommunity.creator_id,
removed: None,
deleted: None,
nsfw: ccommunity.nsfw,
updated: None,
actor_id: make_apub_endpoint(EndpointType::Community, &ccommunity.name).to_string(),
local: ccommunity.local,
private_key: Some(keypair.private_key),
public_key: Some(keypair.public_key),
last_refreshed_at: Some(naive_now()),
published: None,
};
Community::update(&conn, ccommunity.id, &form)?;
}
info!("{} community rows updated.", incorrect_communities.len());
Ok(())
}
fn post_updates_2020_04_03(conn: &PgConnection) -> Result<(), Error> {
use crate::schema::post::dsl::*;
info!("Running post_updates_2020_04_03");
// Update the ap_id
let incorrect_posts = post
.filter(ap_id.eq("changeme"))
.filter(local.eq(true))
.load::<Post>(conn)?;
for cpost in &incorrect_posts {
Post::update_ap_id(&conn, cpost.id)?;
}
info!("{} post rows updated.", incorrect_posts.len());
Ok(())
}
fn comment_updates_2020_04_03(conn: &PgConnection) -> Result<(), Error> {
use crate::schema::comment::dsl::*;
info!("Running comment_updates_2020_04_03");
// Update the ap_id
let incorrect_comments = comment
.filter(ap_id.eq("changeme"))
.filter(local.eq(true))
.load::<Comment>(conn)?;
for ccomment in &incorrect_comments {
Comment::update_ap_id(&conn, ccomment.id)?;
}
info!("{} comment rows updated.", incorrect_comments.len());
Ok(())
}
fn private_message_updates_2020_05_05(conn: &PgConnection) -> Result<(), Error> {
use crate::schema::private_message::dsl::*;
info!("Running private_message_updates_2020_05_05");
// Update the ap_id
let incorrect_pms = private_message
.filter(ap_id.eq("changeme"))
.filter(local.eq(true))
.load::<PrivateMessage>(conn)?;
for cpm in &incorrect_pms {
PrivateMessage::update_ap_id(&conn, cpm.id)?;
}
info!("{} private message rows updated.", incorrect_pms.len());
Ok(())
}

View file

@ -1,6 +1,9 @@
use super::post::Post;
use super::*;
use crate::schema::{comment, comment_like, comment_saved};
use super::{post::Post, *};
use crate::{
apub::{make_apub_endpoint, EndpointType},
naive_now,
schema::{comment, comment_like, comment_saved},
};
// WITH RECURSIVE MyTree AS (
// SELECT * FROM comment WHERE parent_id IS NULL
@ -19,10 +22,12 @@ pub struct Comment {
pub parent_id: Option<i32>,
pub content: String,
pub removed: bool,
pub read: bool,
pub read: bool, // Whether the recipient has read the comment or not
pub published: chrono::NaiveDateTime,
pub updated: Option<chrono::NaiveDateTime>,
pub deleted: bool,
pub ap_id: String,
pub local: bool,
}
#[derive(Insertable, AsChangeset, Clone)]
@ -34,8 +39,11 @@ pub struct CommentForm {
pub content: String,
pub removed: Option<bool>,
pub read: Option<bool>,
pub published: Option<chrono::NaiveDateTime>,
pub updated: Option<chrono::NaiveDateTime>,
pub deleted: Option<bool>,
pub ap_id: String,
pub local: bool,
}
impl Crud<CommentForm> for Comment {
@ -68,6 +76,42 @@ impl Crud<CommentForm> for Comment {
}
}
impl Comment {
pub fn update_ap_id(conn: &PgConnection, comment_id: i32) -> Result<Self, Error> {
use crate::schema::comment::dsl::*;
let apid = make_apub_endpoint(EndpointType::Comment, &comment_id.to_string()).to_string();
diesel::update(comment.find(comment_id))
.set(ap_id.eq(apid))
.get_result::<Self>(conn)
}
pub fn read_from_apub_id(conn: &PgConnection, object_id: &str) -> Result<Self, Error> {
use crate::schema::comment::dsl::*;
comment.filter(ap_id.eq(object_id)).first::<Self>(conn)
}
pub fn mark_as_read(conn: &PgConnection, comment_id: i32) -> Result<Self, Error> {
use crate::schema::comment::dsl::*;
diesel::update(comment.find(comment_id))
.set(read.eq(true))
.get_result::<Self>(conn)
}
pub fn permadelete(conn: &PgConnection, comment_id: i32) -> Result<Self, Error> {
use crate::schema::comment::dsl::*;
diesel::update(comment.find(comment_id))
.set((
content.eq("*Permananently Deleted*"),
deleted.eq(true),
updated.eq(naive_now()),
))
.get_result::<Self>(conn)
}
}
#[derive(Identifiable, Queryable, Associations, PartialEq, Debug, Clone)]
#[belongs_to(Comment)]
#[table_name = "comment_like"]
@ -160,17 +204,16 @@ impl Saveable<CommentSavedForm> for CommentSaved {
#[cfg(test)]
mod tests {
use super::super::community::*;
use super::super::post::*;
use super::super::user::*;
use super::*;
use super::{
super::{community::*, post::*, user::*},
*,
};
#[test]
fn test_crud() {
let conn = establish_unpooled_connection();
let new_user = UserForm {
name: "terry".into(),
fedi_name: "rrf".into(),
preferred_username: None,
password_encrypted: "nope".into(),
email: None,
@ -186,6 +229,12 @@ mod tests {
lang: "browser".into(),
show_avatars: true,
send_notifications_to_email: false,
actor_id: "changeme".into(),
bio: None,
local: true,
private_key: None,
public_key: None,
last_refreshed_at: None,
};
let inserted_user = User_::create(&conn, &new_user).unwrap();
@ -200,6 +249,12 @@ mod tests {
deleted: None,
updated: None,
nsfw: false,
actor_id: "changeme".into(),
local: true,
private_key: None,
public_key: None,
last_refreshed_at: None,
published: None,
};
let inserted_community = Community::create(&conn, &new_community).unwrap();
@ -220,6 +275,9 @@ mod tests {
embed_description: None,
embed_html: None,
thumbnail_url: None,
ap_id: "changeme".into(),
local: true,
published: None,
};
let inserted_post = Post::create(&conn, &new_post).unwrap();
@ -232,7 +290,10 @@ mod tests {
deleted: None,
read: None,
parent_id: None,
published: None,
updated: None,
ap_id: "changeme".into(),
local: true,
};
let inserted_comment = Comment::create(&conn, &comment_form).unwrap();
@ -248,6 +309,8 @@ mod tests {
parent_id: None,
published: inserted_comment.published,
updated: None,
ap_id: "changeme".into(),
local: true,
};
let child_comment_form = CommentForm {
@ -258,7 +321,10 @@ mod tests {
removed: None,
deleted: None,
read: None,
published: None,
updated: None,
ap_id: "changeme".into(),
local: true,
};
let inserted_child_comment = Comment::create(&conn, &child_comment_form).unwrap();

View file

@ -1,5 +1,6 @@
use super::*;
use diesel::pg::Pg;
use crate::db::{fuzzy_search, limit_and_offset, ListingType, MaybeOptional, SortType};
use diesel::{dsl::*, pg::Pg, result::Error, *};
use serde::{Deserialize, Serialize};
// The faked schema since diesel doesn't do views
table! {
@ -14,10 +15,16 @@ table! {
published -> Timestamp,
updated -> Nullable<Timestamp>,
deleted -> Bool,
ap_id -> Text,
local -> Bool,
community_id -> Int4,
community_actor_id -> Text,
community_local -> Bool,
community_name -> Varchar,
banned -> Bool,
banned_from_community -> Bool,
creator_actor_id -> Text,
creator_local -> Bool,
creator_name -> Varchar,
creator_avatar -> Nullable<Text>,
score -> BigInt,
@ -43,10 +50,16 @@ table! {
published -> Timestamp,
updated -> Nullable<Timestamp>,
deleted -> Bool,
ap_id -> Text,
local -> Bool,
community_id -> Int4,
community_actor_id -> Text,
community_local -> Bool,
community_name -> Varchar,
banned -> Bool,
banned_from_community -> Bool,
creator_actor_id -> Text,
creator_local -> Bool,
creator_name -> Varchar,
creator_avatar -> Nullable<Text>,
score -> BigInt,
@ -75,10 +88,16 @@ pub struct CommentView {
pub published: chrono::NaiveDateTime,
pub updated: Option<chrono::NaiveDateTime>,
pub deleted: bool,
pub ap_id: String,
pub local: bool,
pub community_id: i32,
pub community_actor_id: String,
pub community_local: bool,
pub community_name: String,
pub banned: bool,
pub banned_from_community: bool,
pub creator_actor_id: String,
pub creator_local: bool,
pub creator_name: String,
pub creator_avatar: Option<String>,
pub score: i64,
@ -282,10 +301,16 @@ table! {
published -> Timestamp,
updated -> Nullable<Timestamp>,
deleted -> Bool,
ap_id -> Text,
local -> Bool,
community_id -> Int4,
community_actor_id -> Text,
community_local -> Bool,
community_name -> Varchar,
banned -> Bool,
banned_from_community -> Bool,
creator_actor_id -> Text,
creator_local -> Bool,
creator_name -> Varchar,
creator_avatar -> Nullable<Text>,
score -> BigInt,
@ -315,10 +340,16 @@ pub struct ReplyView {
pub published: chrono::NaiveDateTime,
pub updated: Option<chrono::NaiveDateTime>,
pub deleted: bool,
pub ap_id: String,
pub local: bool,
pub community_id: i32,
pub community_actor_id: String,
pub community_local: bool,
pub community_name: String,
pub banned: bool,
pub banned_from_community: bool,
pub creator_actor_id: String,
pub creator_local: bool,
pub creator_name: String,
pub creator_avatar: Option<String>,
pub score: i64,
@ -423,18 +454,18 @@ impl<'a> ReplyQueryBuilder<'a> {
#[cfg(test)]
mod tests {
use super::super::comment::*;
use super::super::community::*;
use super::super::post::*;
use super::super::user::*;
use super::*;
use super::{
super::{comment::*, community::*, post::*, user::*},
*,
};
use crate::db::{establish_unpooled_connection, Crud, Likeable};
#[test]
fn test_crud() {
let conn = establish_unpooled_connection();
let new_user = UserForm {
name: "timmy".into(),
fedi_name: "rrf".into(),
preferred_username: None,
password_encrypted: "nope".into(),
email: None,
@ -450,6 +481,12 @@ mod tests {
lang: "browser".into(),
show_avatars: true,
send_notifications_to_email: false,
actor_id: "changeme".into(),
bio: None,
local: true,
private_key: None,
public_key: None,
last_refreshed_at: None,
};
let inserted_user = User_::create(&conn, &new_user).unwrap();
@ -464,6 +501,12 @@ mod tests {
deleted: None,
updated: None,
nsfw: false,
actor_id: "changeme".into(),
local: true,
private_key: None,
public_key: None,
last_refreshed_at: None,
published: None,
};
let inserted_community = Community::create(&conn, &new_community).unwrap();
@ -484,6 +527,9 @@ mod tests {
embed_description: None,
embed_html: None,
thumbnail_url: None,
ap_id: "changeme".into(),
local: true,
published: None,
};
let inserted_post = Post::create(&conn, &new_post).unwrap();
@ -496,7 +542,10 @@ mod tests {
removed: None,
deleted: None,
read: None,
published: None,
updated: None,
ap_id: "changeme".into(),
local: true,
};
let inserted_comment = Comment::create(&conn, &comment_form).unwrap();
@ -535,6 +584,12 @@ mod tests {
my_vote: None,
subscribed: None,
saved: None,
ap_id: "changeme".to_string(),
local: true,
community_actor_id: inserted_community.actor_id.to_owned(),
community_local: true,
creator_actor_id: inserted_user.actor_id.to_owned(),
creator_local: true,
};
let expected_comment_view_with_user = CommentView {
@ -562,6 +617,12 @@ mod tests {
my_vote: Some(1),
subscribed: None,
saved: None,
ap_id: "changeme".to_string(),
local: true,
community_actor_id: inserted_community.actor_id.to_owned(),
community_local: true,
creator_actor_id: inserted_user.actor_id.to_owned(),
creator_local: true,
};
let mut read_comment_views_no_user = CommentQueryBuilder::create(&conn)

View file

@ -1,5 +1,9 @@
use super::*;
use crate::schema::{community, community_follower, community_moderator, community_user_ban};
use crate::{
db::{Bannable, Crud, Followable, Joinable},
schema::{community, community_follower, community_moderator, community_user_ban},
};
use diesel::{dsl::*, result::Error, *};
use serde::{Deserialize, Serialize};
#[derive(Queryable, Identifiable, PartialEq, Debug, Serialize, Deserialize)]
#[table_name = "community"]
@ -15,9 +19,15 @@ pub struct Community {
pub updated: Option<chrono::NaiveDateTime>,
pub deleted: bool,
pub nsfw: bool,
pub actor_id: String,
pub local: bool,
pub private_key: Option<String>,
pub public_key: Option<String>,
pub last_refreshed_at: chrono::NaiveDateTime,
}
#[derive(Insertable, AsChangeset, Clone, Serialize, Deserialize)]
// TODO add better delete, remove, lock actions here.
#[derive(Insertable, AsChangeset, Clone, Serialize, Deserialize, Debug)]
#[table_name = "community"]
pub struct CommunityForm {
pub name: String,
@ -26,9 +36,15 @@ pub struct CommunityForm {
pub category_id: i32,
pub creator_id: i32,
pub removed: Option<bool>,
pub published: Option<chrono::NaiveDateTime>,
pub updated: Option<chrono::NaiveDateTime>,
pub deleted: Option<bool>,
pub nsfw: bool,
pub actor_id: String,
pub local: bool,
pub private_key: Option<String>,
pub public_key: Option<String>,
pub last_refreshed_at: Option<chrono::NaiveDateTime>,
}
impl Crud<CommunityForm> for Community {
@ -62,15 +78,23 @@ impl Crud<CommunityForm> for Community {
}
impl Community {
pub fn read_from_name(conn: &PgConnection, community_name: String) -> Result<Self, Error> {
pub fn read_from_name(conn: &PgConnection, community_name: &str) -> Result<Self, Error> {
use crate::schema::community::dsl::*;
community
.filter(name.eq(community_name))
.first::<Self>(conn)
}
pub fn get_url(&self) -> String {
format!("https://{}/c/{}", Settings::get().hostname, self.name)
pub fn read_from_actor_id(conn: &PgConnection, community_id: &str) -> Result<Self, Error> {
use crate::schema::community::dsl::*;
community
.filter(actor_id.eq(community_id))
.first::<Self>(conn)
}
pub fn list_local(conn: &PgConnection) -> Result<Vec<Self>, Error> {
use crate::schema::community::dsl::*;
community.filter(local.eq(true)).load::<Community>(conn)
}
}
@ -192,7 +216,7 @@ impl Followable<CommunityFollowerForm> for CommunityFollower {
.values(community_follower_form)
.get_result::<Self>(conn)
}
fn ignore(
fn unfollow(
conn: &PgConnection,
community_follower_form: &CommunityFollowerForm,
) -> Result<usize, Error> {
@ -208,15 +232,15 @@ impl Followable<CommunityFollowerForm> for CommunityFollower {
#[cfg(test)]
mod tests {
use super::super::user::*;
use super::*;
use super::{super::user::*, *};
use crate::db::{establish_unpooled_connection, ListingType, SortType};
#[test]
fn test_crud() {
let conn = establish_unpooled_connection();
let new_user = UserForm {
name: "bobbee".into(),
fedi_name: "rrf".into(),
preferred_username: None,
password_encrypted: "nope".into(),
email: None,
@ -232,6 +256,12 @@ mod tests {
lang: "browser".into(),
show_avatars: true,
send_notifications_to_email: false,
actor_id: "changeme".into(),
bio: None,
local: true,
private_key: None,
public_key: None,
last_refreshed_at: None,
};
let inserted_user = User_::create(&conn, &new_user).unwrap();
@ -246,6 +276,12 @@ mod tests {
removed: None,
deleted: None,
updated: None,
actor_id: "changeme".into(),
local: true,
private_key: None,
public_key: None,
last_refreshed_at: None,
published: None,
};
let inserted_community = Community::create(&conn, &new_community).unwrap();
@ -262,6 +298,11 @@ mod tests {
deleted: false,
published: inserted_community.published,
updated: None,
actor_id: "changeme".into(),
local: true,
private_key: None,
public_key: None,
last_refreshed_at: inserted_community.published,
};
let community_follower_form = CommunityFollowerForm {
@ -311,7 +352,7 @@ mod tests {
let read_community = Community::read(&conn, inserted_community.id).unwrap();
let updated_community =
Community::update(&conn, inserted_community.id, &new_community).unwrap();
let ignored_community = CommunityFollower::ignore(&conn, &community_follower_form).unwrap();
let ignored_community = CommunityFollower::unfollow(&conn, &community_follower_form).unwrap();
let left_community = CommunityModerator::leave(&conn, &community_user_form).unwrap();
let unban = CommunityUserBan::unban(&conn, &community_user_ban_form).unwrap();
let num_deleted = Community::delete(&conn, inserted_community.id).unwrap();

View file

@ -1,6 +1,7 @@
use super::community_view::community_mview::BoxedQuery;
use super::*;
use diesel::pg::Pg;
use crate::db::{fuzzy_search, limit_and_offset, MaybeOptional, SortType};
use diesel::{pg::Pg, result::Error, *};
use serde::{Deserialize, Serialize};
table! {
community_view (id) {
@ -15,6 +16,11 @@ table! {
updated -> Nullable<Timestamp>,
deleted -> Bool,
nsfw -> Bool,
actor_id -> Text,
local -> Bool,
last_refreshed_at -> Timestamp,
creator_actor_id -> Text,
creator_local -> Bool,
creator_name -> Varchar,
creator_avatar -> Nullable<Text>,
category_name -> Varchar,
@ -40,6 +46,11 @@ table! {
updated -> Nullable<Timestamp>,
deleted -> Bool,
nsfw -> Bool,
actor_id -> Text,
local -> Bool,
last_refreshed_at -> Timestamp,
creator_actor_id -> Text,
creator_local -> Bool,
creator_name -> Varchar,
creator_avatar -> Nullable<Text>,
category_name -> Varchar,
@ -58,8 +69,12 @@ table! {
community_id -> Int4,
user_id -> Int4,
published -> Timestamp,
user_actor_id -> Text,
user_local -> Bool,
user_name -> Varchar,
avatar -> Nullable<Text>,
community_actor_id -> Text,
community_local -> Bool,
community_name -> Varchar,
}
}
@ -70,8 +85,12 @@ table! {
community_id -> Int4,
user_id -> Int4,
published -> Timestamp,
user_actor_id -> Text,
user_local -> Bool,
user_name -> Varchar,
avatar -> Nullable<Text>,
community_actor_id -> Text,
community_local -> Bool,
community_name -> Varchar,
}
}
@ -82,8 +101,12 @@ table! {
community_id -> Int4,
user_id -> Int4,
published -> Timestamp,
user_actor_id -> Text,
user_local -> Bool,
user_name -> Varchar,
avatar -> Nullable<Text>,
community_actor_id -> Text,
community_local -> Bool,
community_name -> Varchar,
}
}
@ -104,6 +127,11 @@ pub struct CommunityView {
pub updated: Option<chrono::NaiveDateTime>,
pub deleted: bool,
pub nsfw: bool,
pub actor_id: String,
pub local: bool,
pub last_refreshed_at: chrono::NaiveDateTime,
pub creator_actor_id: String,
pub creator_local: bool,
pub creator_name: String,
pub creator_avatar: Option<String>,
pub category_name: String,
@ -257,8 +285,12 @@ pub struct CommunityModeratorView {
pub community_id: i32,
pub user_id: i32,
pub published: chrono::NaiveDateTime,
pub user_actor_id: String,
pub user_local: bool,
pub user_name: String,
pub avatar: Option<String>,
pub community_actor_id: String,
pub community_local: bool,
pub community_name: String,
}
@ -287,8 +319,12 @@ pub struct CommunityFollowerView {
pub community_id: i32,
pub user_id: i32,
pub published: chrono::NaiveDateTime,
pub user_actor_id: String,
pub user_local: bool,
pub user_name: String,
pub avatar: Option<String>,
pub community_actor_id: String,
pub community_local: bool,
pub community_name: String,
}
@ -317,8 +353,12 @@ pub struct CommunityUserBanView {
pub community_id: i32,
pub user_id: i32,
pub published: chrono::NaiveDateTime,
pub user_actor_id: String,
pub user_local: bool,
pub user_name: String,
pub avatar: Option<String>,
pub community_actor_id: String,
pub community_local: bool,
pub community_name: String,
}

View file

@ -1,10 +1,10 @@
use crate::settings::Settings;
use diesel::dsl::*;
use diesel::result::Error;
use diesel::*;
use diesel::{dsl::*, result::Error, *};
use serde::{Deserialize, Serialize};
pub mod activity;
pub mod category;
pub mod code_migrations;
pub mod comment;
pub mod comment_view;
pub mod community;
@ -42,7 +42,7 @@ pub trait Followable<T> {
fn follow(conn: &PgConnection, form: &T) -> Result<Self, Error>
where
Self: Sized;
fn ignore(conn: &PgConnection, form: &T) -> Result<usize, Error>
fn unfollow(conn: &PgConnection, form: &T) -> Result<usize, Error>
where
Self: Sized;
}

View file

@ -1,8 +1,19 @@
use super::*;
use crate::schema::{
mod_add, mod_add_community, mod_ban, mod_ban_from_community, mod_lock_post, mod_remove_comment,
mod_remove_community, mod_remove_post, mod_sticky_post,
use crate::{
db::Crud,
schema::{
mod_add,
mod_add_community,
mod_ban,
mod_ban_from_community,
mod_lock_post,
mod_remove_comment,
mod_remove_community,
mod_remove_post,
mod_sticky_post,
},
};
use diesel::{dsl::*, result::Error, *};
use serde::{Deserialize, Serialize};
#[derive(Queryable, Identifiable, PartialEq, Debug, Serialize, Deserialize)]
#[table_name = "mod_remove_post"]
@ -426,11 +437,12 @@ impl Crud<ModAddForm> for ModAdd {
#[cfg(test)]
mod tests {
use super::super::comment::*;
use super::super::community::*;
use super::super::post::*;
use super::super::user::*;
use super::*;
use super::{
super::{comment::*, community::*, post::*, user::*},
*,
};
use crate::db::{establish_unpooled_connection, ListingType, SortType};
// use Crud;
#[test]
fn test_crud() {
@ -438,7 +450,6 @@ mod tests {
let new_mod = UserForm {
name: "the mod".into(),
fedi_name: "rrf".into(),
preferred_username: None,
password_encrypted: "nope".into(),
email: None,
@ -454,13 +465,18 @@ mod tests {
lang: "browser".into(),
show_avatars: true,
send_notifications_to_email: false,
actor_id: "changeme".into(),
bio: None,
local: true,
private_key: None,
public_key: None,
last_refreshed_at: None,
};
let inserted_mod = User_::create(&conn, &new_mod).unwrap();
let new_user = UserForm {
name: "jim2".into(),
fedi_name: "rrf".into(),
preferred_username: None,
password_encrypted: "nope".into(),
email: None,
@ -476,6 +492,12 @@ mod tests {
lang: "browser".into(),
show_avatars: true,
send_notifications_to_email: false,
actor_id: "changeme".into(),
bio: None,
local: true,
private_key: None,
public_key: None,
last_refreshed_at: None,
};
let inserted_user = User_::create(&conn, &new_user).unwrap();
@ -490,6 +512,12 @@ mod tests {
deleted: None,
updated: None,
nsfw: false,
actor_id: "changeme".into(),
local: true,
private_key: None,
public_key: None,
last_refreshed_at: None,
published: None,
};
let inserted_community = Community::create(&conn, &new_community).unwrap();
@ -510,6 +538,9 @@ mod tests {
embed_description: None,
embed_html: None,
thumbnail_url: None,
ap_id: "changeme".into(),
local: true,
published: None,
};
let inserted_post = Post::create(&conn, &new_post).unwrap();
@ -522,7 +553,10 @@ mod tests {
deleted: None,
read: None,
parent_id: None,
published: None,
updated: None,
ap_id: "changeme".into(),
local: true,
};
let inserted_comment = Comment::create(&conn, &comment_form).unwrap();

View file

@ -1,4 +1,6 @@
use super::*;
use crate::db::limit_and_offset;
use diesel::{result::Error, *};
use serde::{Deserialize, Serialize};
table! {
mod_remove_post_view (id) {

View file

@ -1,6 +1,8 @@
use super::*;
use crate::schema::password_reset_request;
use crate::schema::password_reset_request::dsl::*;
use crate::{
db::Crud,
schema::{password_reset_request, password_reset_request::dsl::*},
};
use diesel::{dsl::*, result::Error, *};
use sha2::{Digest, Sha256};
#[derive(Queryable, Identifiable, PartialEq, Debug)]
@ -79,8 +81,8 @@ impl PasswordResetRequest {
#[cfg(test)]
mod tests {
use super::super::user::*;
use super::*;
use super::{super::user::*, *};
use crate::db::{establish_unpooled_connection, ListingType, SortType};
#[test]
fn test_crud() {
@ -88,7 +90,6 @@ mod tests {
let new_user = UserForm {
name: "thommy prw".into(),
fedi_name: "rrf".into(),
preferred_username: None,
password_encrypted: "nope".into(),
email: None,
@ -104,6 +105,12 @@ mod tests {
lang: "browser".into(),
show_avatars: true,
send_notifications_to_email: false,
actor_id: "changeme".into(),
bio: None,
local: true,
private_key: None,
public_key: None,
last_refreshed_at: None,
};
let inserted_user = User_::create(&conn, &new_user).unwrap();

View file

@ -1,5 +1,11 @@
use super::*;
use crate::schema::{post, post_like, post_read, post_saved};
use crate::{
apub::{make_apub_endpoint, EndpointType},
db::{Crud, Likeable, Readable, Saveable},
naive_now,
schema::{post, post_like, post_read, post_saved},
};
use diesel::{dsl::*, result::Error, *};
use serde::{Deserialize, Serialize};
#[derive(Queryable, Identifiable, PartialEq, Debug, Serialize, Deserialize)]
#[table_name = "post"]
@ -21,9 +27,11 @@ pub struct Post {
pub embed_description: Option<String>,
pub embed_html: Option<String>,
pub thumbnail_url: Option<String>,
pub ap_id: String,
pub local: bool,
}
#[derive(Insertable, AsChangeset, Clone)]
#[derive(Insertable, AsChangeset, Clone, Debug)]
#[table_name = "post"]
pub struct PostForm {
pub name: String,
@ -33,6 +41,7 @@ pub struct PostForm {
pub community_id: i32,
pub removed: Option<bool>,
pub locked: Option<bool>,
pub published: Option<chrono::NaiveDateTime>,
pub updated: Option<chrono::NaiveDateTime>,
pub deleted: Option<bool>,
pub nsfw: bool,
@ -41,6 +50,56 @@ pub struct PostForm {
pub embed_description: Option<String>,
pub embed_html: Option<String>,
pub thumbnail_url: Option<String>,
pub ap_id: String,
pub local: bool,
}
impl Post {
pub fn read(conn: &PgConnection, post_id: i32) -> Result<Self, Error> {
use crate::schema::post::dsl::*;
post.filter(id.eq(post_id)).first::<Self>(conn)
}
pub fn list_for_community(
conn: &PgConnection,
the_community_id: i32,
) -> Result<Vec<Self>, Error> {
use crate::schema::post::dsl::*;
post
.filter(community_id.eq(the_community_id))
.load::<Self>(conn)
}
pub fn read_from_apub_id(conn: &PgConnection, object_id: &str) -> Result<Self, Error> {
use crate::schema::post::dsl::*;
post.filter(ap_id.eq(object_id)).first::<Self>(conn)
}
pub fn update_ap_id(conn: &PgConnection, post_id: i32) -> Result<Self, Error> {
use crate::schema::post::dsl::*;
let apid = make_apub_endpoint(EndpointType::Post, &post_id.to_string()).to_string();
diesel::update(post.find(post_id))
.set(ap_id.eq(apid))
.get_result::<Self>(conn)
}
pub fn permadelete(conn: &PgConnection, post_id: i32) -> Result<Self, Error> {
use crate::schema::post::dsl::*;
let perma_deleted = "*Permananently Deleted*";
let perma_deleted_url = "https://deleted.com";
diesel::update(post.find(post_id))
.set((
name.eq(perma_deleted),
url.eq(perma_deleted_url),
body.eq(perma_deleted),
deleted.eq(true),
updated.eq(naive_now()),
))
.get_result::<Self>(conn)
}
}
impl Crud<PostForm> for Post {
@ -182,16 +241,18 @@ impl Readable<PostReadForm> for PostRead {
#[cfg(test)]
mod tests {
use super::super::community::*;
use super::super::user::*;
use super::*;
use super::{
super::{community::*, user::*},
*,
};
use crate::db::{establish_unpooled_connection, ListingType, SortType};
#[test]
fn test_crud() {
let conn = establish_unpooled_connection();
let new_user = UserForm {
name: "jim".into(),
fedi_name: "rrf".into(),
preferred_username: None,
password_encrypted: "nope".into(),
email: None,
@ -207,6 +268,12 @@ mod tests {
lang: "browser".into(),
show_avatars: true,
send_notifications_to_email: false,
actor_id: "changeme".into(),
bio: None,
local: true,
private_key: None,
public_key: None,
last_refreshed_at: None,
};
let inserted_user = User_::create(&conn, &new_user).unwrap();
@ -221,6 +288,12 @@ mod tests {
deleted: None,
updated: None,
nsfw: false,
actor_id: "changeme".into(),
local: true,
private_key: None,
public_key: None,
last_refreshed_at: None,
published: None,
};
let inserted_community = Community::create(&conn, &new_community).unwrap();
@ -241,6 +314,9 @@ mod tests {
embed_description: None,
embed_html: None,
thumbnail_url: None,
ap_id: "changeme".into(),
local: true,
published: None,
};
let inserted_post = Post::create(&conn, &new_post).unwrap();
@ -263,6 +339,8 @@ mod tests {
embed_description: None,
embed_html: None,
thumbnail_url: None,
ap_id: "changeme".into(),
local: true,
};
// Post Like

View file

@ -1,6 +1,7 @@
use super::post_view::post_mview::BoxedQuery;
use super::*;
use diesel::pg::Pg;
use crate::db::{fuzzy_search, limit_and_offset, ListingType, MaybeOptional, SortType};
use diesel::{dsl::*, pg::Pg, result::Error, *};
use serde::{Deserialize, Serialize};
// The faked schema since diesel doesn't do views
table! {
@ -22,10 +23,16 @@ table! {
embed_description -> Nullable<Text>,
embed_html -> Nullable<Text>,
thumbnail_url -> Nullable<Text>,
ap_id -> Text,
local -> Bool,
banned -> Bool,
banned_from_community -> Bool,
creator_actor_id -> Text,
creator_local -> Bool,
creator_name -> Varchar,
creator_avatar -> Nullable<Text>,
community_actor_id -> Text,
community_local -> Bool,
community_name -> Varchar,
community_removed -> Bool,
community_deleted -> Bool,
@ -63,10 +70,16 @@ table! {
embed_description -> Nullable<Text>,
embed_html -> Nullable<Text>,
thumbnail_url -> Nullable<Text>,
ap_id -> Text,
local -> Bool,
banned -> Bool,
banned_from_community -> Bool,
creator_actor_id -> Text,
creator_local -> Bool,
creator_name -> Varchar,
creator_avatar -> Nullable<Text>,
community_actor_id -> Text,
community_local -> Bool,
community_name -> Varchar,
community_removed -> Bool,
community_deleted -> Bool,
@ -107,10 +120,16 @@ pub struct PostView {
pub embed_description: Option<String>,
pub embed_html: Option<String>,
pub thumbnail_url: Option<String>,
pub ap_id: String,
pub local: bool,
pub banned: bool,
pub banned_from_community: bool,
pub creator_actor_id: String,
pub creator_local: bool,
pub creator_name: String,
pub creator_avatar: Option<String>,
pub community_actor_id: String,
pub community_local: bool,
pub community_name: String,
pub community_removed: bool,
pub community_deleted: bool,
@ -345,10 +364,12 @@ impl PostView {
#[cfg(test)]
mod tests {
use super::super::community::*;
use super::super::post::*;
use super::super::user::*;
use super::*;
use super::{
super::{community::*, post::*, user::*},
*,
};
use crate::db::{establish_unpooled_connection, Crud, Likeable};
#[test]
fn test_crud() {
let conn = establish_unpooled_connection();
@ -359,7 +380,6 @@ mod tests {
let new_user = UserForm {
name: user_name.to_owned(),
fedi_name: "rrf".into(),
preferred_username: None,
password_encrypted: "nope".into(),
email: None,
@ -375,6 +395,12 @@ mod tests {
lang: "browser".into(),
show_avatars: true,
send_notifications_to_email: false,
actor_id: "changeme".into(),
bio: None,
local: true,
private_key: None,
public_key: None,
last_refreshed_at: None,
};
let inserted_user = User_::create(&conn, &new_user).unwrap();
@ -389,6 +415,12 @@ mod tests {
deleted: None,
updated: None,
nsfw: false,
actor_id: "changeme".into(),
local: true,
private_key: None,
public_key: None,
last_refreshed_at: None,
published: None,
};
let inserted_community = Community::create(&conn, &new_community).unwrap();
@ -409,6 +441,9 @@ mod tests {
embed_description: None,
embed_html: None,
thumbnail_url: None,
ap_id: "changeme".into(),
local: true,
published: None,
};
let inserted_post = Post::create(&conn, &new_post).unwrap();
@ -473,6 +508,12 @@ mod tests {
embed_description: None,
embed_html: None,
thumbnail_url: None,
ap_id: "changeme".to_string(),
local: true,
creator_actor_id: inserted_user.actor_id.to_owned(),
creator_local: true,
community_actor_id: inserted_community.actor_id.to_owned(),
community_local: true,
};
let expected_post_listing_with_user = PostView {
@ -512,6 +553,12 @@ mod tests {
embed_description: None,
embed_html: None,
thumbnail_url: None,
ap_id: "changeme".to_string(),
local: true,
creator_actor_id: inserted_user.actor_id.to_owned(),
creator_local: true,
community_actor_id: inserted_community.actor_id.to_owned(),
community_local: true,
};
let read_post_listings_with_user = PostQueryBuilder::create(&conn)

View file

@ -1,5 +1,10 @@
use super::*;
use crate::schema::private_message;
use crate::{
apub::{make_apub_endpoint, EndpointType},
db::Crud,
schema::private_message,
};
use diesel::{dsl::*, result::Error, *};
use serde::{Deserialize, Serialize};
#[derive(Queryable, Identifiable, PartialEq, Debug, Serialize, Deserialize)]
#[table_name = "private_message"]
@ -12,6 +17,8 @@ pub struct PrivateMessage {
pub read: bool,
pub published: chrono::NaiveDateTime,
pub updated: Option<chrono::NaiveDateTime>,
pub ap_id: String,
pub local: bool,
}
#[derive(Insertable, AsChangeset, Clone)]
@ -19,10 +26,13 @@ pub struct PrivateMessage {
pub struct PrivateMessageForm {
pub creator_id: i32,
pub recipient_id: i32,
pub content: Option<String>,
pub content: String,
pub deleted: Option<bool>,
pub read: Option<bool>,
pub published: Option<chrono::NaiveDateTime>,
pub updated: Option<chrono::NaiveDateTime>,
pub ap_id: String,
pub local: bool,
}
impl Crud<PrivateMessageForm> for PrivateMessage {
@ -55,17 +65,39 @@ impl Crud<PrivateMessageForm> for PrivateMessage {
}
}
impl PrivateMessage {
pub fn update_ap_id(conn: &PgConnection, private_message_id: i32) -> Result<Self, Error> {
use crate::schema::private_message::dsl::*;
let apid = make_apub_endpoint(
EndpointType::PrivateMessage,
&private_message_id.to_string(),
)
.to_string();
diesel::update(private_message.find(private_message_id))
.set(ap_id.eq(apid))
.get_result::<Self>(conn)
}
pub fn read_from_apub_id(conn: &PgConnection, object_id: &str) -> Result<Self, Error> {
use crate::schema::private_message::dsl::*;
private_message
.filter(ap_id.eq(object_id))
.first::<Self>(conn)
}
}
#[cfg(test)]
mod tests {
use super::super::user::*;
use super::*;
use super::{super::user::*, *};
use crate::db::{establish_unpooled_connection, ListingType, SortType};
#[test]
fn test_crud() {
let conn = establish_unpooled_connection();
let creator_form = UserForm {
name: "creator_pm".into(),
fedi_name: "rrf".into(),
preferred_username: None,
password_encrypted: "nope".into(),
email: None,
@ -81,13 +113,18 @@ mod tests {
lang: "browser".into(),
show_avatars: true,
send_notifications_to_email: false,
actor_id: "changeme".into(),
bio: None,
local: true,
private_key: None,
public_key: None,
last_refreshed_at: None,
};
let inserted_creator = User_::create(&conn, &creator_form).unwrap();
let recipient_form = UserForm {
name: "recipient_pm".into(),
fedi_name: "rrf".into(),
preferred_username: None,
password_encrypted: "nope".into(),
email: None,
@ -103,17 +140,26 @@ mod tests {
lang: "browser".into(),
show_avatars: true,
send_notifications_to_email: false,
actor_id: "changeme".into(),
bio: None,
local: true,
private_key: None,
public_key: None,
last_refreshed_at: None,
};
let inserted_recipient = User_::create(&conn, &recipient_form).unwrap();
let private_message_form = PrivateMessageForm {
content: Some("A test private message".into()),
content: "A test private message".into(),
creator_id: inserted_creator.id,
recipient_id: inserted_recipient.id,
deleted: None,
read: None,
published: None,
updated: None,
ap_id: "changeme".into(),
local: true,
};
let inserted_private_message = PrivateMessage::create(&conn, &private_message_form).unwrap();
@ -127,6 +173,8 @@ mod tests {
read: false,
updated: None,
published: inserted_private_message.published,
ap_id: "changeme".into(),
local: true,
};
let read_private_message = PrivateMessage::read(&conn, inserted_private_message.id).unwrap();

View file

@ -1,5 +1,6 @@
use super::*;
use diesel::pg::Pg;
use crate::db::{limit_and_offset, MaybeOptional};
use diesel::{pg::Pg, result::Error, *};
use serde::{Deserialize, Serialize};
// The faked schema since diesel doesn't do views
table! {
@ -12,10 +13,16 @@ table! {
read -> Bool,
published -> Timestamp,
updated -> Nullable<Timestamp>,
ap_id -> Text,
local -> Bool,
creator_name -> Varchar,
creator_avatar -> Nullable<Text>,
creator_actor_id -> Text,
creator_local -> Bool,
recipient_name -> Varchar,
recipient_avatar -> Nullable<Text>,
recipient_actor_id -> Text,
recipient_local -> Bool,
}
}
@ -29,10 +36,16 @@ table! {
read -> Bool,
published -> Timestamp,
updated -> Nullable<Timestamp>,
ap_id -> Text,
local -> Bool,
creator_name -> Varchar,
creator_avatar -> Nullable<Text>,
creator_actor_id -> Text,
creator_local -> Bool,
recipient_name -> Varchar,
recipient_avatar -> Nullable<Text>,
recipient_actor_id -> Text,
recipient_local -> Bool,
}
}
@ -49,10 +62,16 @@ pub struct PrivateMessageView {
pub read: bool,
pub published: chrono::NaiveDateTime,
pub updated: Option<chrono::NaiveDateTime>,
pub ap_id: String,
pub local: bool,
pub creator_name: String,
pub creator_avatar: Option<String>,
pub creator_actor_id: String,
pub creator_local: bool,
pub recipient_name: String,
pub recipient_avatar: Option<String>,
pub recipient_actor_id: String,
pub recipient_local: bool,
}
pub struct PrivateMessageQueryBuilder<'a> {

View file

@ -1,5 +1,6 @@
use super::*;
use crate::schema::site;
use crate::{db::Crud, schema::site};
use diesel::{dsl::*, result::Error, *};
use serde::{Deserialize, Serialize};
#[derive(Queryable, Identifiable, PartialEq, Debug, Serialize, Deserialize)]
#[table_name = "site"]

View file

@ -1,4 +1,5 @@
use super::*;
use diesel::{result::Error, *};
use serde::{Deserialize, Serialize};
table! {
site_view (id) {

View file

@ -1,16 +1,20 @@
use super::*;
use crate::schema::user_;
use crate::schema::user_::dsl::*;
use crate::{is_email_regex, Settings};
use crate::{
db::Crud,
is_email_regex,
naive_now,
schema::{user_, user_::dsl::*},
settings::Settings,
};
use bcrypt::{hash, DEFAULT_COST};
use diesel::{dsl::*, result::Error, *};
use jsonwebtoken::{decode, encode, DecodingKey, EncodingKey, Header, TokenData, Validation};
use serde::{Deserialize, Serialize};
#[derive(Queryable, Identifiable, PartialEq, Debug)]
#[table_name = "user_"]
pub struct User_ {
pub id: i32,
pub name: String,
pub fedi_name: String,
pub preferred_username: Option<String>,
pub password_encrypted: String,
pub email: Option<String>,
@ -27,13 +31,18 @@ pub struct User_ {
pub show_avatars: bool,
pub send_notifications_to_email: bool,
pub matrix_user_id: Option<String>,
pub actor_id: String,
pub bio: Option<String>,
pub local: bool,
pub private_key: Option<String>,
pub public_key: Option<String>,
pub last_refreshed_at: chrono::NaiveDateTime,
}
#[derive(Insertable, AsChangeset, Clone)]
#[derive(Insertable, AsChangeset, Clone, Debug)]
#[table_name = "user_"]
pub struct UserForm {
pub name: String,
pub fedi_name: String,
pub preferred_username: Option<String>,
pub password_encrypted: String,
pub admin: bool,
@ -49,6 +58,12 @@ pub struct UserForm {
pub show_avatars: bool,
pub send_notifications_to_email: bool,
pub matrix_user_id: Option<String>,
pub actor_id: String,
pub bio: Option<String>,
pub local: bool,
pub private_key: Option<String>,
pub public_key: Option<String>,
pub last_refreshed_at: Option<chrono::NaiveDateTime>,
}
impl Crud<UserForm> for User_ {
@ -78,6 +93,7 @@ impl User_ {
Self::create(&conn, &edited_user)
}
// TODO do more individual updates like these
pub fn update_password(
conn: &PgConnection,
user_id: i32,
@ -86,13 +102,33 @@ impl User_ {
let password_hash = hash(new_password, DEFAULT_COST).expect("Couldn't hash password");
diesel::update(user_.find(user_id))
.set(password_encrypted.eq(password_hash))
.set((
password_encrypted.eq(password_hash),
updated.eq(naive_now()),
))
.get_result::<Self>(conn)
}
pub fn read_from_name(conn: &PgConnection, from_user_name: String) -> Result<Self, Error> {
pub fn read_from_name(conn: &PgConnection, from_user_name: &str) -> Result<Self, Error> {
user_.filter(name.eq(from_user_name)).first::<Self>(conn)
}
pub fn add_admin(conn: &PgConnection, user_id: i32, added: bool) -> Result<Self, Error> {
diesel::update(user_.find(user_id))
.set(admin.eq(added))
.get_result::<Self>(conn)
}
pub fn ban_user(conn: &PgConnection, user_id: i32, ban: bool) -> Result<Self, Error> {
diesel::update(user_.find(user_id))
.set(banned.eq(ban))
.get_result::<Self>(conn)
}
pub fn read_from_actor_id(conn: &PgConnection, object_id: &str) -> Result<Self, Error> {
use crate::schema::user_::dsl::*;
user_.filter(actor_id.eq(object_id)).first::<Self>(conn)
}
}
#[derive(Debug, Serialize, Deserialize)]
@ -129,7 +165,7 @@ impl User_ {
let my_claims = Claims {
id: self.id,
username: self.name.to_owned(),
iss: self.fedi_name.to_owned(),
iss: Settings::get().hostname,
show_nsfw: self.show_nsfw,
theme: self.theme.to_owned(),
default_sort_type: self.default_sort_type,
@ -177,8 +213,8 @@ impl User_ {
#[cfg(test)]
mod tests {
use super::User_;
use super::*;
use super::{User_, *};
use crate::db::{establish_unpooled_connection, ListingType, SortType};
#[test]
fn test_crud() {
@ -186,7 +222,6 @@ mod tests {
let new_user = UserForm {
name: "thommy".into(),
fedi_name: "rrf".into(),
preferred_username: None,
password_encrypted: "nope".into(),
email: None,
@ -202,6 +237,12 @@ mod tests {
lang: "browser".into(),
show_avatars: true,
send_notifications_to_email: false,
actor_id: "changeme".into(),
bio: None,
local: true,
private_key: None,
public_key: None,
last_refreshed_at: None,
};
let inserted_user = User_::create(&conn, &new_user).unwrap();
@ -209,7 +250,6 @@ mod tests {
let expected_user = User_ {
id: inserted_user.id,
name: "thommy".into(),
fedi_name: "rrf".into(),
preferred_username: None,
password_encrypted: "nope".into(),
email: None,
@ -226,6 +266,12 @@ mod tests {
lang: "browser".into(),
show_avatars: true,
send_notifications_to_email: false,
actor_id: "changeme".into(),
bio: None,
local: true,
private_key: None,
public_key: None,
last_refreshed_at: inserted_user.published,
};
let read_user = User_::read(&conn, inserted_user.id).unwrap();

View file

@ -1,6 +1,7 @@
use super::comment::Comment;
use super::*;
use crate::schema::user_mention;
use crate::{db::Crud, schema::user_mention};
use diesel::{dsl::*, result::Error, *};
use serde::{Deserialize, Serialize};
#[derive(Queryable, Associations, Identifiable, PartialEq, Debug, Serialize, Deserialize)]
#[belongs_to(Comment)]
@ -53,18 +54,18 @@ impl Crud<UserMentionForm> for UserMention {
#[cfg(test)]
mod tests {
use super::super::comment::*;
use super::super::community::*;
use super::super::post::*;
use super::super::user::*;
use super::*;
use super::{
super::{comment::*, community::*, post::*, user::*},
*,
};
use crate::db::{establish_unpooled_connection, ListingType, SortType};
#[test]
fn test_crud() {
let conn = establish_unpooled_connection();
let new_user = UserForm {
name: "terrylake".into(),
fedi_name: "rrf".into(),
preferred_username: None,
password_encrypted: "nope".into(),
email: None,
@ -80,13 +81,18 @@ mod tests {
lang: "browser".into(),
show_avatars: true,
send_notifications_to_email: false,
actor_id: "changeme".into(),
bio: None,
local: true,
private_key: None,
public_key: None,
last_refreshed_at: None,
};
let inserted_user = User_::create(&conn, &new_user).unwrap();
let recipient_form = UserForm {
name: "terrylakes recipient".into(),
fedi_name: "rrf".into(),
preferred_username: None,
password_encrypted: "nope".into(),
email: None,
@ -102,6 +108,12 @@ mod tests {
lang: "browser".into(),
show_avatars: true,
send_notifications_to_email: false,
actor_id: "changeme".into(),
bio: None,
local: true,
private_key: None,
public_key: None,
last_refreshed_at: None,
};
let inserted_recipient = User_::create(&conn, &recipient_form).unwrap();
@ -116,6 +128,12 @@ mod tests {
deleted: None,
updated: None,
nsfw: false,
actor_id: "changeme".into(),
local: true,
private_key: None,
public_key: None,
last_refreshed_at: None,
published: None,
};
let inserted_community = Community::create(&conn, &new_community).unwrap();
@ -136,6 +154,9 @@ mod tests {
embed_description: None,
embed_html: None,
thumbnail_url: None,
ap_id: "changeme".into(),
local: true,
published: None,
};
let inserted_post = Post::create(&conn, &new_post).unwrap();
@ -148,7 +169,10 @@ mod tests {
deleted: None,
read: None,
parent_id: None,
published: None,
updated: None,
ap_id: "changeme".into(),
local: true,
};
let inserted_comment = Comment::create(&conn, &comment_form).unwrap();

View file

@ -1,5 +1,6 @@
use super::*;
use diesel::pg::Pg;
use crate::db::{limit_and_offset, MaybeOptional, SortType};
use diesel::{dsl::*, pg::Pg, result::Error, *};
use serde::{Deserialize, Serialize};
// The faked schema since diesel doesn't do views
table! {
@ -7,6 +8,8 @@ table! {
id -> Int4,
user_mention_id -> Int4,
creator_id -> Int4,
creator_actor_id -> Text,
creator_local -> Bool,
post_id -> Int4,
parent_id -> Nullable<Int4>,
content -> Text,
@ -16,6 +19,8 @@ table! {
updated -> Nullable<Timestamp>,
deleted -> Bool,
community_id -> Int4,
community_actor_id -> Text,
community_local -> Bool,
community_name -> Varchar,
banned -> Bool,
banned_from_community -> Bool,
@ -29,6 +34,8 @@ table! {
my_vote -> Nullable<Int4>,
saved -> Nullable<Bool>,
recipient_id -> Int4,
recipient_actor_id -> Text,
recipient_local -> Bool,
}
}
@ -37,6 +44,8 @@ table! {
id -> Int4,
user_mention_id -> Int4,
creator_id -> Int4,
creator_actor_id -> Text,
creator_local -> Bool,
post_id -> Int4,
parent_id -> Nullable<Int4>,
content -> Text,
@ -46,6 +55,8 @@ table! {
updated -> Nullable<Timestamp>,
deleted -> Bool,
community_id -> Int4,
community_actor_id -> Text,
community_local -> Bool,
community_name -> Varchar,
banned -> Bool,
banned_from_community -> Bool,
@ -59,6 +70,8 @@ table! {
my_vote -> Nullable<Int4>,
saved -> Nullable<Bool>,
recipient_id -> Int4,
recipient_actor_id -> Text,
recipient_local -> Bool,
}
}
@ -70,6 +83,8 @@ pub struct UserMentionView {
pub id: i32,
pub user_mention_id: i32,
pub creator_id: i32,
pub creator_actor_id: String,
pub creator_local: bool,
pub post_id: i32,
pub parent_id: Option<i32>,
pub content: String,
@ -79,6 +94,8 @@ pub struct UserMentionView {
pub updated: Option<chrono::NaiveDateTime>,
pub deleted: bool,
pub community_id: i32,
pub community_actor_id: String,
pub community_local: bool,
pub community_name: String,
pub banned: bool,
pub banned_from_community: bool,
@ -92,6 +109,8 @@ pub struct UserMentionView {
pub my_vote: Option<i32>,
pub saved: Option<bool>,
pub recipient_id: i32,
pub recipient_actor_id: String,
pub recipient_local: bool,
}
pub struct UserMentionQueryBuilder<'a> {

View file

@ -1,15 +1,18 @@
use super::user_view::user_mview::BoxedQuery;
use super::*;
use diesel::pg::Pg;
use crate::db::{fuzzy_search, limit_and_offset, MaybeOptional, SortType};
use diesel::{dsl::*, pg::Pg, result::Error, *};
use serde::{Deserialize, Serialize};
table! {
user_view (id) {
id -> Int4,
actor_id -> Text,
name -> Varchar,
avatar -> Nullable<Text>,
email -> Nullable<Text>,
matrix_user_id -> Nullable<Text>,
fedi_name -> Varchar,
bio -> Nullable<Text>,
local -> Bool,
admin -> Bool,
banned -> Bool,
show_avatars -> Bool,
@ -25,11 +28,13 @@ table! {
table! {
user_mview (id) {
id -> Int4,
actor_id -> Text,
name -> Varchar,
avatar -> Nullable<Text>,
email -> Nullable<Text>,
matrix_user_id -> Nullable<Text>,
fedi_name -> Varchar,
bio -> Nullable<Text>,
local -> Bool,
admin -> Bool,
banned -> Bool,
show_avatars -> Bool,
@ -48,11 +53,13 @@ table! {
#[table_name = "user_view"]
pub struct UserView {
pub id: i32,
pub actor_id: String,
pub name: String,
pub avatar: Option<String>,
pub email: Option<String>,
pub matrix_user_id: Option<String>,
pub fedi_name: String,
pub bio: Option<String>,
pub local: bool,
pub admin: bool,
pub banned: bool,
pub show_avatars: bool,

View file

@ -16,6 +16,8 @@ pub extern crate dotenv;
pub extern crate jsonwebtoken;
pub extern crate lettre;
pub extern crate lettre_email;
extern crate log;
pub extern crate openssl;
pub extern crate rand;
pub extern crate regex;
pub extern crate rss;
@ -34,22 +36,27 @@ pub mod settings;
pub mod version;
pub mod websocket;
use crate::settings::Settings;
use actix_web::dev::ConnectionInfo;
use chrono::{DateTime, NaiveDateTime, Utc};
use lettre::smtp::authentication::{Credentials, Mechanism};
use lettre::smtp::extension::ClientId;
use lettre::smtp::ConnectionReuseParameters;
use lettre::{ClientSecurity, SmtpClient, Transport};
use chrono::{DateTime, FixedOffset, Local, NaiveDateTime, Utc};
use itertools::Itertools;
use lettre::{
smtp::{
authentication::{Credentials, Mechanism},
extension::ClientId,
ConnectionReuseParameters,
},
ClientSecurity,
SmtpClient,
Transport,
};
use lettre_email::Email;
use log::error;
use percent_encoding::{utf8_percent_encode, NON_ALPHANUMERIC};
use rand::distributions::Alphanumeric;
use rand::{thread_rng, Rng};
use rand::{distributions::Alphanumeric, thread_rng, Rng};
use regex::{Regex, RegexBuilder};
use serde::Deserialize;
use crate::settings::Settings;
pub type ConnectionId = usize;
pub type PostId = i32;
pub type CommunityId = i32;
@ -68,6 +75,11 @@ pub fn naive_from_unix(time: i64) -> NaiveDateTime {
NaiveDateTime::from_timestamp(time, 0)
}
pub fn convert_datetime(datetime: NaiveDateTime) -> DateTime<FixedOffset> {
let now = Local::now();
DateTime::<FixedOffset>::from_utc(datetime, *now.offset())
}
pub fn is_email_regex(test: &str) -> bool {
EMAIL_REGEX.is_match(test)
}
@ -111,20 +123,6 @@ pub fn slurs_vec_to_str(slurs: Vec<&str>) -> String {
[start, combined].concat()
}
pub fn extract_usernames(test: &str) -> Vec<&str> {
let mut matches: Vec<&str> = USERNAME_MATCHES_REGEX
.find_iter(test)
.map(|mat| mat.as_str())
.collect();
// Unique
matches.sort_unstable();
matches.dedup();
// Remove /u/
matches.iter().map(|t| &t[3..]).collect()
}
pub fn generate_random_string() -> String {
thread_rng().sample_iter(&Alphanumeric).take(30).collect()
}
@ -279,6 +277,33 @@ pub fn get_ip(conn_info: &ConnectionInfo) -> String {
.to_string()
}
// TODO nothing is done with community / group webfingers yet, so just ignore those for now
#[derive(Clone, PartialEq, Eq, Hash)]
pub struct MentionData {
pub name: String,
pub domain: String,
}
impl MentionData {
pub fn is_local(&self) -> bool {
Settings::get().hostname.eq(&self.domain)
}
pub fn full_name(&self) -> String {
format!("@{}@{}", &self.name, &self.domain)
}
}
pub fn scrape_text_for_mentions(text: &str) -> Vec<MentionData> {
let mut out: Vec<MentionData> = Vec::new();
for caps in WEBFINGER_USER_REGEX.captures_iter(text) {
out.push(MentionData {
name: caps["name"].to_string(),
domain: caps["domain"].to_string(),
});
}
out.into_iter().unique().collect()
}
pub fn is_valid_username(name: &str) -> bool {
VALID_USERNAME_REGEX.is_match(name)
}
@ -290,10 +315,26 @@ pub fn is_valid_community_name(name: &str) -> bool {
#[cfg(test)]
mod tests {
use crate::{
extract_usernames, is_email_regex, is_image_content_type, is_valid_community_name,
is_valid_username, remove_slurs, slur_check, slurs_vec_to_str,
is_email_regex,
is_image_content_type,
is_valid_community_name,
is_valid_username,
remove_slurs,
scrape_text_for_mentions,
slur_check,
slurs_vec_to_str,
};
#[test]
fn test_mentions_regex() {
let text = "Just read a great blog post by [@tedu@honk.teduangst.com](/u/test). And another by !test_community@fish.teduangst.com . Another [@lemmy@lemmy_alpha:8540](/u/fish)";
let mentions = scrape_text_for_mentions(text);
assert_eq!(mentions[0].name, "tedu".to_string());
assert_eq!(mentions[0].domain, "honk.teduangst.com".to_string());
assert_eq!(mentions[1].domain, "lemmy_alpha:8540".to_string());
}
#[test]
fn test_image() {
assert!(is_image_content_type("https://1734811051.rsc.cdn77.org/data/images/full/365645/as-virus-kills-navajos-in-their-homes-tribal-women-provide-lifeline.jpg?w=600?w=650").is_ok());
@ -355,13 +396,6 @@ mod tests {
}
}
#[test]
fn test_extract_usernames() {
let usernames = extract_usernames("this is a user mention for [/u/testme](/u/testme) and thats all. Oh [/u/another](/u/another) user. And the first again [/u/testme](/u/testme) okay");
let expected = vec!["another", "testme"];
assert_eq!(usernames, expected);
}
// These helped with testing
// #[test]
// fn test_iframely() {
@ -388,6 +422,9 @@ lazy_static! {
static ref EMAIL_REGEX: Regex = Regex::new(r"^[a-zA-Z0-9.!#$%&*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$").unwrap();
static ref SLUR_REGEX: Regex = RegexBuilder::new(r"(fag(g|got|tard)?|maricos?|cock\s?sucker(s|ing)?|nig(\b|g?(a|er)?(s|z)?)\b|dindu(s?)|mudslime?s?|kikes?|mongoloids?|towel\s*heads?|\bspi(c|k)s?\b|\bchinks?|niglets?|beaners?|\bnips?\b|\bcoons?\b|jungle\s*bunn(y|ies?)|jigg?aboo?s?|\bpakis?\b|rag\s*heads?|gooks?|cunts?|bitch(es|ing|y)?|puss(y|ies?)|twats?|feminazis?|whor(es?|ing)|\bslut(s|t?y)?|\btrann?(y|ies?)|ladyboy(s?)|\b(b|re|r)tard(ed)?s?)").case_insensitive(true).build().unwrap();
static ref USERNAME_MATCHES_REGEX: Regex = Regex::new(r"/u/[a-zA-Z][0-9a-zA-Z_]*").unwrap();
// TODO keep this old one, it didn't work with port well tho
// static ref WEBFINGER_USER_REGEX: Regex = Regex::new(r"@(?P<name>[\w.]+)@(?P<domain>[a-zA-Z0-9._-]+\.[a-zA-Z0-9_-]+)").unwrap();
static ref WEBFINGER_USER_REGEX: Regex = Regex::new(r"@(?P<name>[\w.]+)@(?P<domain>[a-zA-Z0-9._:-]+)").unwrap();
static ref VALID_USERNAME_REGEX: Regex = Regex::new(r"^[a-zA-Z0-9_]{3,20}$").unwrap();
static ref VALID_COMMUNITY_NAME_REGEX: Regex = Regex::new(r"^[a-z0-9_]{3,20}$").unwrap();
}

View file

@ -6,14 +6,21 @@ pub extern crate lazy_static;
use crate::lemmy_server::actix_web::dev::Service;
use actix::prelude::*;
use actix_web::body::Body;
use actix_web::dev::{ServiceRequest, ServiceResponse};
use actix_web::http::header::CONTENT_TYPE;
use actix_web::http::{header::CACHE_CONTROL, HeaderValue};
use actix_web::*;
use diesel::r2d2::{ConnectionManager, Pool};
use diesel::PgConnection;
use actix_web::{
body::Body,
dev::{ServiceRequest, ServiceResponse},
http::{
header::{CACHE_CONTROL, CONTENT_TYPE},
HeaderValue,
},
*,
};
use diesel::{
r2d2::{ConnectionManager, Pool},
PgConnection,
};
use lemmy_server::{
db::code_migrations::run_advanced_migrations,
rate_limit::{rate_limiter::RateLimiter, RateLimit},
routes::{api, federation, feeds, index, nodeinfo, webfinger},
settings::Settings,
@ -48,6 +55,7 @@ async fn main() -> io::Result<()> {
// Run the migrations from code
let conn = pool.get().unwrap();
embedded_migrations::run(&conn).unwrap();
run_advanced_migrations(&conn).unwrap();
// Set up the rate limiter
let rate_limiter = RateLimit {

View file

@ -1,23 +1,18 @@
pub mod rate_limiter;
use super::{IPAddr, Settings};
use crate::api::APIError;
use crate::get_ip;
use crate::settings::RateLimitConfig;
use crate::{api::APIError, get_ip, settings::RateLimitConfig};
use actix_web::dev::{Service, ServiceRequest, ServiceResponse, Transform};
use failure::Error;
use futures::future::{ok, Ready};
use log::debug;
use rate_limiter::{RateLimitType, RateLimiter};
use std::collections::HashMap;
use std::future::Future;
use std::pin::Pin;
use std::sync::Arc;
use std::task::{Context, Poll};
use std::time::SystemTime;
use strum::IntoEnumIterator;
use std::{
future::Future,
pin::Pin,
sync::Arc,
task::{Context, Poll},
};
use tokio::sync::Mutex;
pub mod rate_limiter;
#[derive(Debug, Clone)]
pub struct RateLimit {
pub rate_limiter: Arc<Mutex<RateLimiter>>,

View file

@ -1,4 +1,9 @@
use super::*;
use super::IPAddr;
use crate::api::APIError;
use failure::Error;
use log::debug;
use std::{collections::HashMap, time::SystemTime};
use strum::IntoEnumIterator;
#[derive(Debug, Clone)]
pub struct RateLimitBucket {

View file

@ -1,10 +1,11 @@
use super::*;
use crate::api::comment::*;
use crate::api::community::*;
use crate::api::post::*;
use crate::api::site::*;
use crate::api::user::*;
use crate::rate_limit::RateLimit;
use crate::{
api::{comment::*, community::*, post::*, site::*, user::*, Oper, Perform},
rate_limit::RateLimit,
routes::{ChatServerParam, DbPoolParam},
websocket::WebsocketInfo,
};
use actix_web::{error::ErrorBadRequest, *};
use serde::Serialize;
pub fn config(cfg: &mut web::ServiceConfig, rate_limit: &RateLimit) {
cfg.service(
@ -83,6 +84,14 @@ pub fn config(cfg: &mut web::ServiceConfig, rate_limit: &RateLimit) {
.route("/like", web::post().to(route_post::<CreateCommentLike>))
.route("/save", web::put().to(route_post::<SaveComment>)),
)
// Private Message
.service(
web::scope("/private_message")
.wrap(rate_limit.message())
.route("/list", web::get().to(route_get::<GetPrivateMessages>))
.route("", web::post().to(route_post::<CreatePrivateMessage>))
.route("", web::put().to(route_post::<EditPrivateMessage>)),
)
// User
.service(
// Account action, I don't like that it's in /user maybe /accounts

View file

@ -1,18 +1,45 @@
use super::*;
use crate::apub;
use crate::{
apub::{
comment::get_apub_comment,
community::*,
community_inbox::community_inbox,
post::get_apub_post,
shared_inbox::shared_inbox,
user::*,
user_inbox::user_inbox,
APUB_JSON_CONTENT_TYPE,
},
settings::Settings,
};
use actix_web::*;
pub fn config(cfg: &mut web::ServiceConfig) {
cfg
.route(
"/federation/c/{community_name}",
web::get().to(apub::community::get_apub_community),
)
.route(
"/federation/c/{community_name}/followers",
web::get().to(apub::community::get_apub_community_followers),
)
.route(
"/federation/u/{user_name}",
web::get().to(apub::user::get_apub_user),
);
if Settings::get().federation.enabled {
println!("federation enabled, host is {}", Settings::get().hostname);
cfg
.service(
web::scope("/")
.guard(guard::Header("Accept", APUB_JSON_CONTENT_TYPE))
.route(
"/c/{community_name}",
web::get().to(get_apub_community_http),
)
.route(
"/c/{community_name}/followers",
web::get().to(get_apub_community_followers),
)
// TODO This is only useful for history which we aren't doing right now
// .route(
// "/c/{community_name}/outbox",
// web::get().to(get_apub_community_outbox),
// )
.route("/u/{user_name}", web::get().to(get_apub_user_http))
.route("/post/{post_id}", web::get().to(get_apub_post))
.route("/comment/{comment_id}", web::get().to(get_apub_comment)),
)
// Inboxes dont work with the header guard for some reason.
.route("/c/{community_name}/inbox", web::post().to(community_inbox))
.route("/u/{user_name}/inbox", web::post().to(user_inbox))
.route("/inbox", web::post().to(shared_inbox));
}
}

View file

@ -1,11 +1,28 @@
use super::*;
use crate::db::comment_view::{ReplyQueryBuilder, ReplyView};
use crate::db::community::Community;
use crate::db::post_view::{PostQueryBuilder, PostView};
use crate::db::site_view::SiteView;
use crate::db::user::{Claims, User_};
use crate::db::user_mention_view::{UserMentionQueryBuilder, UserMentionView};
use crate::db::{ListingType, SortType};
use crate::{
db::{
comment_view::{ReplyQueryBuilder, ReplyView},
community::Community,
post_view::{PostQueryBuilder, PostView},
site_view::SiteView,
user::{Claims, User_},
user_mention_view::{UserMentionQueryBuilder, UserMentionView},
ListingType,
SortType,
},
markdown_to_html,
routes::DbPoolParam,
settings::Settings,
};
use actix_web::{error::ErrorBadRequest, *};
use chrono::{DateTime, NaiveDateTime, Utc};
use diesel::{
r2d2::{ConnectionManager, Pool},
PgConnection,
};
use rss::{CategoryBuilder, ChannelBuilder, GuidBuilder, Item, ItemBuilder};
use serde::Deserialize;
use std::str::FromStr;
use strum::ParseError;
#[derive(Deserialize)]
pub struct Params {
@ -21,14 +38,11 @@ enum RequestType {
pub fn config(cfg: &mut web::ServiceConfig) {
cfg
.route("/feeds/{type}/{name}.xml", web::get().to(feeds::get_feed))
.route("/feeds/all.xml", web::get().to(feeds::get_all_feed));
.route("/feeds/{type}/{name}.xml", web::get().to(get_feed))
.route("/feeds/all.xml", web::get().to(get_all_feed));
}
async fn get_all_feed(
info: web::Query<Params>,
db: web::Data<Pool<ConnectionManager<PgConnection>>>,
) -> Result<HttpResponse, Error> {
async fn get_all_feed(info: web::Query<Params>, db: DbPoolParam) -> Result<HttpResponse, Error> {
let res = web::block(move || {
let conn = db.get()?;
get_feed_all_data(&conn, &get_sort_type(info)?)
@ -144,8 +158,7 @@ fn get_feed_community(
community_name: String,
) -> Result<ChannelBuilder, failure::Error> {
let site_view = SiteView::read(&conn)?;
let community = Community::read_from_name(&conn, community_name)?;
let community_url = community.get_url();
let community = Community::read_from_name(&conn, &community_name)?;
let posts = PostQueryBuilder::create(&conn)
.listing_type(ListingType::All)
@ -158,7 +171,7 @@ fn get_feed_community(
let mut channel_builder = ChannelBuilder::default();
channel_builder
.title(&format!("{} - {}", site_view.name, community.name))
.link(community_url)
.link(community.actor_id)
.items(items);
if let Some(community_desc) = community.description {

View file

@ -1,4 +1,6 @@
use super::*;
use crate::settings::Settings;
use actix_files::NamedFile;
use actix_web::*;
pub fn config(cfg: &mut web::ServiceConfig) {
cfg

View file

@ -1,31 +1,3 @@
use crate::api::{Oper, Perform};
use crate::db::site_view::SiteView;
use crate::rate_limit::rate_limiter::RateLimiter;
use crate::websocket::{server::ChatServer, WebsocketInfo};
use crate::{get_ip, markdown_to_html, version, Settings};
use actix::prelude::*;
use actix_files::NamedFile;
use actix_web::{body::Body, error::ErrorBadRequest, web::Query, *};
use actix_web_actors::ws;
use chrono::{DateTime, NaiveDateTime, Utc};
use diesel::{
r2d2::{ConnectionManager, Pool},
PgConnection,
};
use log::{error, info};
use regex::Regex;
use rss::{CategoryBuilder, ChannelBuilder, GuidBuilder, Item, ItemBuilder};
use serde::{Deserialize, Serialize};
use serde_json::json;
use std::str::FromStr;
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};
use strum::ParseError;
pub type DbPoolParam = web::Data<Pool<ConnectionManager<PgConnection>>>;
pub type RateLimitParam = web::Data<Arc<Mutex<RateLimiter>>>;
pub type ChatServerParam = web::Data<Addr<ChatServer>>;
pub mod api;
pub mod federation;
pub mod feeds;
@ -33,3 +5,16 @@ pub mod index;
pub mod nodeinfo;
pub mod webfinger;
pub mod websocket;
use crate::{rate_limit::rate_limiter::RateLimiter, websocket::server::ChatServer};
use actix::prelude::*;
use actix_web::*;
use diesel::{
r2d2::{ConnectionManager, Pool},
PgConnection,
};
use std::sync::{Arc, Mutex};
pub type DbPoolParam = web::Data<Pool<ConnectionManager<PgConnection>>>;
pub type RateLimitParam = web::Data<Arc<Mutex<RateLimiter>>>;
pub type ChatServerParam = web::Data<Addr<ChatServer>>;

View file

@ -1,4 +1,13 @@
use super::*;
use crate::{
apub::get_apub_protocol_string,
db::site_view::SiteView,
routes::DbPoolParam,
version,
Settings,
};
use actix_web::{body::Body, error::ErrorBadRequest, *};
use serde::{Deserialize, Serialize};
use url::Url;
pub fn config(cfg: &mut web::ServiceConfig) {
cfg
@ -6,26 +15,32 @@ pub fn config(cfg: &mut web::ServiceConfig) {
.route("/.well-known/nodeinfo", web::get().to(node_info_well_known));
}
async fn node_info_well_known() -> HttpResponse<Body> {
async fn node_info_well_known() -> Result<HttpResponse<Body>, failure::Error> {
let node_info = NodeInfoWellKnown {
links: NodeInfoWellKnownLinks {
rel: "http://nodeinfo.diaspora.software/ns/schema/2.0".to_string(),
href: format!("https://{}/nodeinfo/2.0.json", Settings::get().hostname),
rel: Url::parse("http://nodeinfo.diaspora.software/ns/schema/2.0")?,
href: Url::parse(&format!(
"{}://{}/nodeinfo/2.0.json",
get_apub_protocol_string(),
Settings::get().hostname
))?,
},
};
HttpResponse::Ok().json(node_info)
Ok(HttpResponse::Ok().json(node_info))
}
async fn node_info(
db: web::Data<Pool<ConnectionManager<PgConnection>>>,
) -> Result<HttpResponse, Error> {
async fn node_info(db: DbPoolParam) -> Result<HttpResponse, Error> {
let res = web::block(move || {
let conn = db.get()?;
let site_view = match SiteView::read(&conn) {
Ok(site_view) => site_view,
Err(_) => return Err(format_err!("not_found")),
};
let protocols = vec![];
let protocols = if Settings::get().federation.enabled {
vec!["activitypub".to_string()]
} else {
vec![]
};
Ok(NodeInfo {
version: "2.0".to_string(),
software: NodeInfoSoftware {
@ -49,41 +64,41 @@ async fn node_info(
Ok(res)
}
#[derive(Serialize)]
struct NodeInfoWellKnown {
links: NodeInfoWellKnownLinks,
#[derive(Serialize, Deserialize, Debug)]
pub struct NodeInfoWellKnown {
pub links: NodeInfoWellKnownLinks,
}
#[derive(Serialize)]
struct NodeInfoWellKnownLinks {
rel: String,
href: String,
#[derive(Serialize, Deserialize, Debug)]
pub struct NodeInfoWellKnownLinks {
pub rel: Url,
pub href: Url,
}
#[derive(Serialize)]
struct NodeInfo {
version: String,
software: NodeInfoSoftware,
protocols: Vec<String>,
usage: NodeInfoUsage,
#[derive(Serialize, Deserialize, Debug)]
pub struct NodeInfo {
pub version: String,
pub software: NodeInfoSoftware,
pub protocols: Vec<String>,
pub usage: NodeInfoUsage,
}
#[derive(Serialize)]
struct NodeInfoSoftware {
name: String,
version: String,
#[derive(Serialize, Deserialize, Debug)]
pub struct NodeInfoSoftware {
pub name: String,
pub version: String,
}
#[derive(Serialize)]
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
struct NodeInfoUsage {
users: NodeInfoUsers,
local_posts: i64,
local_comments: i64,
open_registrations: bool,
pub struct NodeInfoUsage {
pub users: NodeInfoUsers,
pub local_posts: i64,
pub local_comments: i64,
pub open_registrations: bool,
}
#[derive(Serialize)]
struct NodeInfoUsers {
total: i64,
#[derive(Serialize, Deserialize, Debug)]
pub struct NodeInfoUsers {
pub total: i64,
}

View file

@ -1,16 +1,41 @@
use super::*;
use crate::db::community::Community;
use crate::{
db::{community::Community, user::User_},
routes::DbPoolParam,
Settings,
};
use actix_web::{error::ErrorBadRequest, web::Query, *};
use regex::Regex;
use serde::{Deserialize, Serialize};
#[derive(Deserialize)]
pub struct Params {
resource: String,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct WebFingerResponse {
pub subject: String,
pub aliases: Vec<String>,
pub links: Vec<WebFingerLink>,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct WebFingerLink {
pub rel: Option<String>,
#[serde(rename(serialize = "type", deserialize = "type"))]
pub type_: Option<String>,
pub href: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub template: Option<String>,
}
pub fn config(cfg: &mut web::ServiceConfig) {
cfg.route(
".well-known/webfinger",
web::get().to(get_webfinger_response),
);
if Settings::get().federation.enabled {
cfg.route(
".well-known/webfinger",
web::get().to(get_webfinger_response),
);
}
}
lazy_static! {
@ -19,6 +44,11 @@ lazy_static! {
Settings::get().hostname
))
.unwrap();
static ref WEBFINGER_USER_REGEX: Regex = Regex::new(&format!(
"^acct:([a-z0-9_]{{3, 20}})@{}$",
Settings::get().hostname
))
.unwrap();
}
/// Responds to webfinger requests of the following format. There isn't any real documentation for
@ -29,56 +59,63 @@ lazy_static! {
/// https://radical.town/.well-known/webfinger?resource=acct:felix@radical.town
async fn get_webfinger_response(
info: Query<Params>,
db: web::Data<Pool<ConnectionManager<PgConnection>>>,
db: DbPoolParam,
) -> Result<HttpResponse, Error> {
let res = web::block(move || {
let conn = db.get()?;
let regex_parsed = WEBFINGER_COMMUNITY_REGEX
let community_regex_parsed = WEBFINGER_COMMUNITY_REGEX
.captures(&info.resource)
.map(|c| c.get(1));
// TODO: replace this with .flatten() once we are running rust 1.40
let regex_parsed_flattened = match regex_parsed {
Some(s) => s,
None => None,
};
let community_name = match regex_parsed_flattened {
Some(c) => c.as_str(),
None => return Err(format_err!("not_found")),
.map(|c| c.get(1))
.flatten();
let user_regex_parsed = WEBFINGER_USER_REGEX
.captures(&info.resource)
.map(|c| c.get(1))
.flatten();
let url = if let Some(community_name) = community_regex_parsed {
// Make sure the requested community exists.
let community = match Community::read_from_name(&conn, &community_name.as_str()) {
Ok(o) => o,
Err(_) => return Err(format_err!("not_found")),
};
community.actor_id
} else if let Some(user_name) = user_regex_parsed {
// Make sure the requested user exists.
let user = match User_::read_from_name(&conn, &user_name.as_str()) {
Ok(o) => o,
Err(_) => return Err(format_err!("not_found")),
};
user.actor_id
} else {
return Err(format_err!("not_found"));
};
// Make sure the requested community exists.
let community = match Community::read_from_name(&conn, community_name.to_string()) {
Ok(o) => o,
Err(_) => return Err(format_err!("not_found")),
let wf_res = WebFingerResponse {
subject: info.resource.to_owned(),
aliases: vec![url.to_owned()],
links: vec![
WebFingerLink {
rel: Some("http://webfinger.net/rel/profile-page".to_string()),
type_: Some("text/html".to_string()),
href: Some(url.to_owned()),
template: None,
},
WebFingerLink {
rel: Some("self".to_string()),
type_: Some("application/activity+json".to_string()),
href: Some(url),
template: None,
}, // TODO: this also needs to return the subscribe link once that's implemented
//{
// "rel": "http://ostatus.org/schema/1.0/subscribe",
// "template": "https://my_instance.com/authorize_interaction?uri={uri}"
//}
],
};
let community_url = community.get_url();
Ok(json!({
"subject": info.resource,
"aliases": [
community_url,
],
"links": [
{
"rel": "http://webfinger.net/rel/profile-page",
"type": "text/html",
"href": community_url
},
{
"rel": "self",
"type": "application/activity+json",
// Yes this is correct, this link doesn't include the `.json` extension
"href": community_url
}
// TODO: this also needs to return the subscribe link once that's implemented
//{
// "rel": "http://ostatus.org/schema/1.0/subscribe",
// "template": "https://my_instance.com/authorize_interaction?uri={uri}"
//}
]
}))
Ok(wf_res)
})
.await
.map(|json| HttpResponse::Ok().json(json))

View file

@ -1,5 +1,12 @@
use super::*;
use crate::websocket::server::*;
use crate::{
get_ip,
websocket::server::{ChatServer, *},
};
use actix::prelude::*;
use actix_web::*;
use actix_web_actors::ws;
use log::{debug, error, info};
use std::time::{Duration, Instant};
/// How often heartbeat pings are sent
const HEARTBEAT_INTERVAL: Duration = Duration::from_secs(5);
@ -32,7 +39,6 @@ struct WSSession {
/// Client must send ping at least once per 10 seconds (CLIENT_TIMEOUT),
/// otherwise we drop connection.
hb: Instant,
// db: Pool<ConnectionManager<PgConnection>>,
}
impl Actor for WSSession {
@ -144,7 +150,7 @@ impl WSSession {
// check client heartbeats
if Instant::now().duration_since(act.hb) > CLIENT_TIMEOUT {
// heartbeat timed out
error!("Websocket Client heartbeat failed, disconnecting!");
debug!("Websocket Client heartbeat failed, disconnecting!");
// notify chat server
act.cs_addr.do_send(Disconnect {

View file

@ -1,3 +1,14 @@
table! {
activity (id) {
id -> Int4,
user_id -> Int4,
data -> Jsonb,
local -> Bool,
published -> Timestamp,
updated -> Nullable<Timestamp>,
}
}
table! {
category (id) {
id -> Int4,
@ -17,6 +28,8 @@ table! {
published -> Timestamp,
updated -> Nullable<Timestamp>,
deleted -> Bool,
ap_id -> Varchar,
local -> Bool,
}
}
@ -53,6 +66,11 @@ table! {
updated -> Nullable<Timestamp>,
deleted -> Bool,
nsfw -> Bool,
actor_id -> Varchar,
local -> Bool,
private_key -> Nullable<Text>,
public_key -> Nullable<Text>,
last_refreshed_at -> Timestamp,
}
}
@ -211,6 +229,8 @@ table! {
embed_description -> Nullable<Text>,
embed_html -> Nullable<Text>,
thumbnail_url -> Nullable<Text>,
ap_id -> Varchar,
local -> Bool,
}
}
@ -252,6 +272,8 @@ table! {
read -> Bool,
published -> Timestamp,
updated -> Nullable<Timestamp>,
ap_id -> Varchar,
local -> Bool,
}
}
@ -273,7 +295,6 @@ table! {
user_ (id) {
id -> Int4,
name -> Varchar,
fedi_name -> Varchar,
preferred_username -> Nullable<Varchar>,
password_encrypted -> Text,
email -> Nullable<Text>,
@ -290,6 +311,12 @@ table! {
show_avatars -> Bool,
send_notifications_to_email -> Bool,
matrix_user_id -> Nullable<Text>,
actor_id -> Varchar,
bio -> Nullable<Text>,
local -> Bool,
private_key -> Nullable<Text>,
public_key -> Nullable<Text>,
last_refreshed_at -> Timestamp,
}
}
@ -311,6 +338,7 @@ table! {
}
}
joinable!(activity -> user_ (user_id));
joinable!(comment -> post (post_id));
joinable!(comment -> user_ (creator_id));
joinable!(comment_like -> comment (comment_id));
@ -353,6 +381,7 @@ joinable!(user_mention -> comment (comment_id));
joinable!(user_mention -> user_ (recipient_id));
allow_tables_to_appear_in_same_query!(
activity,
category,
comment,
comment_like,

View file

@ -1,10 +1,7 @@
use config::{Config, ConfigError, Environment, File};
use failure::Error;
use serde::Deserialize;
use std::env;
use std::fs;
use std::net::IpAddr;
use std::sync::RwLock;
use std::{env, fs, net::IpAddr, sync::RwLock};
static CONFIG_FILE_DEFAULTS: &str = "config/defaults.hjson";
static CONFIG_FILE: &str = "config/config.hjson";
@ -20,6 +17,7 @@ pub struct Settings {
pub front_end_dir: String,
pub rate_limit: RateLimitConfig,
pub email: Option<EmailConfig>,
pub federation: Federation,
}
#[derive(Debug, Deserialize, Clone)]
@ -59,6 +57,13 @@ pub struct Database {
pub pool_size: u32,
}
#[derive(Debug, Deserialize, Clone)]
pub struct Federation {
pub enabled: bool,
pub tls_enabled: bool,
pub allowed_instances: String,
}
lazy_static! {
static ref SETTINGS: RwLock<Settings> = RwLock::new(match Settings::init() {
Ok(c) => c,

View file

@ -2,16 +2,20 @@ pub mod server;
use crate::ConnectionId;
use actix::prelude::*;
use diesel::r2d2::{ConnectionManager, Pool};
use diesel::PgConnection;
use diesel::{
r2d2::{ConnectionManager, Pool},
PgConnection,
};
use failure::Error;
use log::{error, info};
use rand::{rngs::ThreadRng, Rng};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use server::ChatServer;
use std::collections::{HashMap, HashSet};
use std::str::FromStr;
use std::{
collections::{HashMap, HashSet},
str::FromStr,
};
#[derive(EnumString, ToString, Debug, Clone)]
pub enum UserOperation {

View file

@ -3,15 +3,16 @@
//! room through `ChatServer`.
use super::*;
use crate::api::comment::*;
use crate::api::community::*;
use crate::api::post::*;
use crate::api::site::*;
use crate::api::user::*;
use crate::api::*;
use crate::rate_limit::RateLimit;
use crate::websocket::UserOperation;
use crate::{CommunityId, ConnectionId, IPAddr, PostId, UserId};
use crate::{
api::{comment::*, community::*, post::*, site::*, user::*, *},
rate_limit::RateLimit,
websocket::UserOperation,
CommunityId,
ConnectionId,
IPAddr,
PostId,
UserId,
};
/// Chat server sends this messages to session
#[derive(Message)]

1
ui/.eslintignore vendored
View file

@ -1,2 +1,3 @@
fuse.js
translation_report.ts
src/api_tests

3
ui/.gitignore vendored
View file

@ -6,14 +6,11 @@ _site
.git
build
.build
.git
.history
.idea
.jshintrc
.nyc_output
.sass-cache
.vscode
build
coverage
jsconfig.json
Gemfile.lock

10
ui/jest.config.js vendored Normal file
View file

@ -0,0 +1,10 @@
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
testTimeout: 30000,
globals: {
'ts-jest': {
diagnostics: false,
},
},
};

8
ui/package.json vendored
View file

@ -6,6 +6,7 @@
"license": "AGPL-3.0-or-later",
"main": "index.js",
"scripts": {
"api-test": "jest src/api_tests/api.spec.ts",
"build": "node fuse prod",
"lint": "tsc --noEmit && eslint --report-unused-disable-directives --ext .js,.ts,.tsx src",
"prebuild": "node generate_translations.js",
@ -18,7 +19,7 @@
"@types/autosize": "^3.0.6",
"@types/js-cookie": "^2.2.6",
"@types/jwt-decode": "^2.2.1",
"@types/markdown-it": "^10.0.0",
"@types/markdown-it": "^0.0.9",
"@types/markdown-it-container": "^2.0.2",
"@types/node": "^13.11.1",
"autosize": "^4.0.2",
@ -38,6 +39,7 @@
"markdown-it-emoji": "^1.4.0",
"mobius1-selectr": "^2.4.13",
"moment": "^2.24.0",
"node-fetch": "^2.6.0",
"prettier": "^2.0.4",
"reconnecting-websocket": "^4.4.0",
"rxjs": "^6.5.5",
@ -49,12 +51,16 @@
"ws": "^7.2.3"
},
"devDependencies": {
"@types/jest": "^25.2.1",
"@types/node-fetch": "^2.5.6",
"eslint": "^6.5.1",
"eslint-plugin-inferno": "^7.14.3",
"eslint-plugin-jane": "^7.2.1",
"fuse-box": "^3.1.3",
"jest": "^25.4.0",
"lint-staged": "^10.1.3",
"sortpack": "^2.1.4",
"ts-jest": "^25.4.0",
"ts-node": "^8.8.2",
"ts-transform-classcat": "^1.0.0",
"ts-transform-inferno": "^4.0.3",

1487
ui/src/api_tests/api.spec.ts vendored Normal file

File diff suppressed because it is too large Load diff

View file

@ -113,6 +113,9 @@ export class AdminSettings extends Component<any, AdminSettingsState> {
user={{
name: admin.name,
avatar: admin.avatar,
id: admin.id,
local: admin.local,
actor_id: admin.actor_id,
}}
/>
</li>
@ -133,6 +136,9 @@ export class AdminSettings extends Component<any, AdminSettingsState> {
user={{
name: banned.name,
avatar: banned.avatar,
id: banned.id,
local: banned.local,
actor_id: banned.actor_id,
}}
/>
</li>

View file

@ -154,6 +154,9 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
user={{
name: node.comment.creator_name,
avatar: node.comment.creator_avatar,
id: node.comment.creator_id,
local: node.comment.creator_local,
actor_id: node.comment.creator_actor_id,
}}
/>
</span>

View file

@ -14,6 +14,7 @@ import {
} from '../interfaces';
import { WebSocketService } from '../services';
import { wsJsonToRes, toast } from '../utils';
import { CommunityLink } from './community-link';
import { i18n } from '../i18next';
declare const Sortable: any;
@ -104,9 +105,7 @@ export class Communities extends Component<any, CommunitiesState> {
{this.state.communities.map(community => (
<tr>
<td>
<Link to={`/c/${community.name}`}>
{community.name}
</Link>
<CommunityLink community={community} />
</td>
<td class="d-none d-lg-table-cell">{community.title}</td>
<td>{community.category_name}</td>

38
ui/src/components/community-link.tsx vendored Normal file
View file

@ -0,0 +1,38 @@
import { Component } from 'inferno';
import { Link } from 'inferno-router';
import { Community } from '../interfaces';
import { hostname } from '../utils';
interface CommunityOther {
name: string;
id?: number; // Necessary if its federated
local?: boolean;
actor_id?: string;
}
interface CommunityLinkProps {
community: Community | CommunityOther;
realLink?: boolean;
}
export class CommunityLink extends Component<CommunityLinkProps, any> {
constructor(props: any, context: any) {
super(props, context);
}
render() {
let community = this.props.community;
let name_: string, link: string;
let local = community.local == null ? true : community.local;
if (local) {
name_ = community.name;
link = `/c/${community.name}`;
} else {
name_ = `${community.name}@${hostname(community.actor_id)}`;
link = !this.props.realLink
? `/community/${community.id}`
: community.actor_id;
}
return <Link to={link}>{name_}</Link>;
}
}

View file

@ -80,6 +80,11 @@ export class Community extends Component<any, State> {
removed: null,
nsfw: false,
deleted: null,
local: null,
actor_id: null,
last_refreshed_at: null,
creator_actor_id: null,
creator_local: null,
},
moderators: [],
admins: [],

View file

@ -34,6 +34,7 @@ import { ListingTypeSelect } from './listing-type-select';
import { DataTypeSelect } from './data-type-select';
import { SiteForm } from './site-form';
import { UserListing } from './user-listing';
import { CommunityLink } from './community-link';
import {
wsJsonToRes,
repoUrl,
@ -190,9 +191,14 @@ export class Main extends Component<any, MainState> {
<ul class="list-inline">
{this.state.subscribedCommunities.map(community => (
<li class="list-inline-item">
<Link to={`/c/${community.community_name}`}>
{community.community_name}
</Link>
<CommunityLink
community={{
name: community.community_name,
id: community.community_id,
local: community.community_local,
actor_id: community.community_actor_id,
}}
/>
</li>
))}
</ul>
@ -228,7 +234,7 @@ export class Main extends Component<any, MainState> {
<ul class="list-inline">
{this.state.trendingCommunities.map(community => (
<li class="list-inline-item">
<Link to={`/c/${community.name}`}>{community.name}</Link>
<CommunityLink community={community} />
</li>
))}
</ul>
@ -321,6 +327,9 @@ export class Main extends Component<any, MainState> {
user={{
name: admin.name,
avatar: admin.avatar,
local: admin.local,
actor_id: admin.actor_id,
id: admin.id,
}}
/>
</li>

View file

@ -35,6 +35,7 @@ import {
setupTribute,
setupTippy,
emojiPicker,
hostname,
pictrsDeleteToast,
} from '../utils';
import autosize from 'autosize';
@ -333,7 +334,11 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
>
<option>{i18n.t('select_a_community')}</option>
{this.state.communities.map(community => (
<option value={community.id}>{community.name}</option>
<option value={community.id}>
{community.local
? community.name
: `${hostname(community.actor_id)}/${community.name}`}
</option>
))}
</select>
</div>

View file

@ -20,6 +20,7 @@ import { MomentTime } from './moment-time';
import { PostForm } from './post-form';
import { IFramelyCard } from './iframely-card';
import { UserListing } from './user-listing';
import { CommunityLink } from './community-link';
import {
md,
mdToHtml,
@ -30,6 +31,7 @@ import {
getUnixTime,
pictrsImage,
setupTippy,
hostname,
previewLines,
} from '../utils';
import { i18n } from '../i18next';
@ -314,22 +316,21 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
</Link>
)}
</h5>
{post.url &&
!(new URL(post.url).hostname == window.location.hostname) && (
<small class="d-inline-block">
<a
className="ml-2 text-muted font-italic"
href={post.url}
target="_blank"
title={post.url}
>
{new URL(post.url).hostname}
<svg class="ml-1 icon icon-inline">
<use xlinkHref="#icon-external-link"></use>
</svg>
</a>
</small>
)}
{post.url && !(hostname(post.url) == window.location.hostname) && (
<small class="d-inline-block">
<a
className="ml-2 text-muted font-italic"
href={post.url}
target="_blank"
title={post.url}
>
{hostname(post.url)}
<svg class="ml-1 icon icon-inline">
<use xlinkHref="#icon-external-link"></use>
</svg>
</a>
</small>
)}
{(isImage(post.url) || this.props.post.thumbnail_url) && (
<>
{!this.state.imageExpanded ? (
@ -422,6 +423,9 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
user={{
name: post.creator_name,
avatar: post.creator_avatar,
id: post.creator_id,
local: post.creator_local,
actor_id: post.creator_actor_id,
}}
/>
{this.isMod && (
@ -442,9 +446,14 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
{this.props.showCommunity && (
<span>
<span> {i18n.t('to')} </span>
<Link to={`/c/${post.community_name}`}>
{post.community_name}
</Link>
<CommunityLink
community={{
name: post.community_name,
id: post.community_id,
local: post.community_local,
actor_id: post.community_actor_id,
}}
/>
</span>
)}
</li>

View file

@ -135,6 +135,9 @@ export class PrivateMessageForm extends Component<
user={{
name: this.state.recipient.name,
avatar: this.state.recipient.avatar,
id: this.state.recipient.id,
local: this.state.recipient.local,
actor_id: this.state.recipient.actor_id,
}}
/>
</div>

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