Compare commits
184 commits
master
...
federation
Author | SHA1 | Date | |
---|---|---|---|
dc94e58cbf | |||
fd6a040568 | |||
68bcc26ff6 | |||
325ed2ec3b | |||
|
cfa40e482a | ||
0f1a8ec928 | |||
5c6601cb2a | |||
f40f74b20d | |||
c1ef766125 | |||
3999e0485e | |||
1aa30d855e | |||
f3aba6da92 | |||
c34cc46c2d | |||
52206998aa | |||
d6e2119277 | |||
8f9bd1fef7 | |||
ce0a37cdf1 | |||
cac7011d53 | |||
5753c4feaa | |||
|
b08574fd57 | ||
|
baa402f82f | ||
a9af247f1e | |||
d1aca27126 | |||
f15c3b4e1e | |||
9e61c3be94 | |||
f88180650d | |||
020b9b8cdd | |||
|
940dc73f28 | ||
3a4973ad68 | |||
0fb8450e56 | |||
13ca47a3b4 | |||
11acc7225e | |||
a1ad21ec56 | |||
bb1b4ee33e | |||
66142c546b | |||
15f1920b25 | |||
dfd6629a6f | |||
21407260a4 | |||
f1692a07fc | |||
7485f1a5b4 | |||
b177cbce1d | |||
b8b2398d32 | |||
fab22e3d8a | |||
dfc9637230 | |||
2c22e413eb | |||
|
67d4daa7a1 | ||
211ef795e9 | |||
a09c818746 | |||
5366797a4b | |||
75c6c8521b | |||
2f1cd9976d | |||
461114c143 | |||
38cdfdf7e0 | |||
770dcbdc49 | |||
ee4f923f60 | |||
8cd68f56aa | |||
c43f06124a | |||
0c0c683986 | |||
36d0e34668 | |||
59bba148ff | |||
b60c7bbae7 | |||
07a9d84ed0 | |||
3b62f58dd2 | |||
6eaa06ab02 | |||
9721b77317 | |||
4b741c3759 | |||
70060c27b2 | |||
9c30b37d57 | |||
|
10877fd45f | ||
22abbebd41 | |||
3ce0618362 | |||
079ac091eb | |||
b5a5b307a0 | |||
df9135f410 | |||
8a25f0f816 | |||
33c5c21a57 | |||
d846740839 | |||
c3ac1649f2 | |||
66a2c4a2c3 | |||
e5497edd5c | |||
ce800f75ad | |||
b8aaf5c1f1 | |||
70816a4779 | |||
92e30311ce | |||
0425e8b114 | |||
18e570b021 | |||
957e4a2611 | |||
4e80543edb | |||
|
a90b16a72c | ||
f0026065f5 | |||
2f4b3a4f83 | |||
697c62fb64 | |||
1e7c3841b2 | |||
7117b5ce32 | |||
5284dc0c52 | |||
8daf72278d | |||
0199b5f169 | |||
a49bd1d42a | |||
b1b97db11a | |||
c5ced6fa5e | |||
8908c8b184 | |||
9c974fbe50 | |||
86f172076b | |||
9a85f1b25f | |||
a941c20024 | |||
7ba6ee8714 | |||
fcf1c65fc1 | |||
1336b4ed60 | |||
f040dac647 | |||
26ad37a8c0 | |||
5c83cbc1ac | |||
9878a58452 | |||
9d2046d5a2 | |||
19c8461397 | |||
13e6c98e47 | |||
fdaf0b3364 | |||
fac1cc7e1d | |||
fc951d9295 | |||
17d3d2492c | |||
5e3902a3bc | |||
509005fa0c | |||
492625f6d6 | |||
483d11e772 | |||
0b617377df | |||
f5b58bcdaf | |||
5706b533fd | |||
6962b9c433 | |||
edd0ef5991 | |||
d2bad5f79e | |||
61c560c12c | |||
d3bd7771d2 | |||
b7103a7e14 | |||
1b0da74b57 | |||
095ccae616 | |||
17bf6baa25 | |||
56947e7710 | |||
4fadc4d072 | |||
85ea1046f0 | |||
cb7059f832 | |||
c16458b728 | |||
6a7a262912 | |||
96c3621a80 | |||
9197b39ed6 | |||
32b0275257 | |||
31f835db86 | |||
5ca466117d | |||
0d369e6019 | |||
945fd8331b | |||
4354f868fd | |||
bf52bc22e4 | |||
875545f7e1 | |||
672798e711 | |||
|
875ed79f3f | ||
20a06ce3f2 | |||
cfe0d9c9c2 | |||
33cce05300 | |||
390b204272 | |||
bd030470b1 | |||
5043a52b88 | |||
05735b31c0 | |||
8ebcc7ac02 | |||
5896a9d251 | |||
b01f4f75d6 | |||
8f67a3c634 | |||
063811cb60 | |||
27c07f1f84 | |||
54172bd322 | |||
8867fa1d52 | |||
18be8b10f5 | |||
34a827a270 | |||
91ae9a9d49 | |||
1f29e91796 | |||
7cdf167e4b | |||
b854d8f3a0 | |||
f9443dfbd3 | |||
1d824ee293 | |||
8130535af4 | |||
f247b28262 | |||
ac4a62636b | |||
d932acad16 | |||
eaf548b5db | |||
35489a706b | |||
e09a035373 | |||
581f36d6ef |
108 changed files with 13215 additions and 2386 deletions
8
.dockerignore
vendored
8
.dockerignore
vendored
|
@ -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
16
.gitignore
vendored
|
@ -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
2
README.md
vendored
|
@ -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.
|
||||
|
|
15
docker/dev/test_deploy.sh
vendored
15
docker/dev/test_deploy.sh
vendored
|
@ -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
23
docker/federation-test/run-tests.sh
vendored
Executable 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
17
docker/federation/Dockerfile
vendored
Normal 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
132
docker/federation/docker-compose.yml
vendored
Normal 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
125
docker/federation/nginx.conf
vendored
Normal 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
28
docker/federation/run-federation-test.bash
vendored
Executable 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
|
2
docs/src/about_guide.md
vendored
2
docs/src/about_guide.md
vendored
|
@ -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
|
||||
|
|
31
docs/src/contributing_federation_development.md
vendored
31
docs/src/contributing_federation_development.md
vendored
|
@ -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
|
||||
```
|
||||
|
|
3
server/.rustfmt.toml
vendored
3
server/.rustfmt.toml
vendored
|
@ -1,2 +1,5 @@
|
|||
tab_spaces = 2
|
||||
edition="2018"
|
||||
imports_layout="HorizontalVertical"
|
||||
merge_imports=true
|
||||
reorder_imports=true
|
||||
|
|
2618
server/Cargo.lock
generated
vendored
2618
server/Cargo.lock
generated
vendored
File diff suppressed because it is too large
Load diff
25
server/Cargo.toml
vendored
25
server/Cargo.toml
vendored
|
@ -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
4
server/config/config.hjson
vendored
Normal file
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
hostname: "localhost:8536"
|
||||
federation_enabled: true
|
||||
}
|
14
server/config/defaults.hjson
vendored
14
server/config/defaults.hjson
vendored
|
@ -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
|
||||
|
|
16
server/migrations/2020-03-26-192410_add_activitypub_tables/down.sql
vendored
Normal file
16
server/migrations/2020-03-26-192410_add_activitypub_tables/down.sql
vendored
Normal 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;
|
36
server/migrations/2020-03-26-192410_add_activitypub_tables/up.sql
vendored
Normal file
36
server/migrations/2020-03-26-192410_add_activitypub_tables/up.sql
vendored
Normal 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.
|
||||
|
7
server/migrations/2020-04-03-194936_add_activitypub_for_posts_and_comments/down.sql
vendored
Normal file
7
server/migrations/2020-04-03-194936_add_activitypub_for_posts_and_comments/down.sql
vendored
Normal 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;
|
14
server/migrations/2020-04-03-194936_add_activitypub_for_posts_and_comments/up.sql
vendored
Normal file
14
server/migrations/2020-04-03-194936_add_activitypub_for_posts_and_comments/up.sql
vendored
Normal 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
|
||||
;
|
||||
|
36
server/migrations/2020-04-07-135912_add_user_community_apub_constraints/down.sql
vendored
Normal file
36
server/migrations/2020-04-07-135912_add_user_community_apub_constraints/down.sql
vendored
Normal 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);
|
38
server/migrations/2020-04-07-135912_add_user_community_apub_constraints/up.sql
vendored
Normal file
38
server/migrations/2020-04-07-135912_add_user_community_apub_constraints/up.sql
vendored
Normal 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);
|
||||
|
440
server/migrations/2020-04-14-163701_update_views_for_activitypub/down.sql
vendored
Normal file
440
server/migrations/2020-04-14-163701_update_views_for_activitypub/down.sql
vendored
Normal 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
|
||||
;
|
||||
|
497
server/migrations/2020-04-14-163701_update_views_for_activitypub/up.sql
vendored
Normal file
497
server/migrations/2020-04-14-163701_update_views_for_activitypub/up.sql
vendored
Normal 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
|
||||
;
|
||||
|
4
server/migrations/2020-04-21-123957_remove_unique_user_constraints/down.sql
vendored
Normal file
4
server/migrations/2020-04-21-123957_remove_unique_user_constraints/down.sql
vendored
Normal 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));
|
||||
|
2
server/migrations/2020-04-21-123957_remove_unique_user_constraints/up.sql
vendored
Normal file
2
server/migrations/2020-04-21-123957_remove_unique_user_constraints/up.sql
vendored
Normal 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));
|
21
server/migrations/2020-05-05-210233_add_activitypub_for_private_messages/down.sql
vendored
Normal file
21
server/migrations/2020-05-05-210233_add_activitypub_for_private_messages/down.sql
vendored
Normal 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);
|
25
server/migrations/2020-05-05-210233_add_activitypub_for_private_messages/up.sql
vendored
Normal file
25
server/migrations/2020-05-05-210233_add_activitypub_for_private_messages/up.sql
vendored
Normal 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);
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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 }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 };
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
74
server/src/apub/activities.rs
Normal file
74
server/src/apub/activities.rs
Normal 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
510
server/src/apub/comment.rs
Normal 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 = ¬e.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,
|
||||
})
|
||||
}
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
|
123
server/src/apub/community_inbox.rs
Normal file
123
server/src/apub/community_inbox.rs
Normal 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())
|
||||
}
|
40
server/src/apub/extensions/group_extensions.rs
Normal file
40
server/src/apub/extensions/group_extensions.rs
Normal 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 {}
|
3
server/src/apub/extensions/mod.rs
Normal file
3
server/src/apub/extensions/mod.rs
Normal file
|
@ -0,0 +1,3 @@
|
|||
pub mod group_extensions;
|
||||
pub mod page_extension;
|
||||
pub mod signatures;
|
11
server/src/apub/extensions/page_extension.rs
Normal file
11
server/src/apub/extensions/page_extension.rs
Normal 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 {}
|
137
server/src/apub/extensions/signatures.rs
Normal file
137
server/src/apub/extensions/signatures.rs
Normal 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
326
server/src/apub/fetcher.rs
Normal 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>>()?,
|
||||
// )
|
||||
// }
|
|
@ -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."))
|
||||
}
|
||||
|
|
|
@ -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(())
|
||||
}
|
||||
}
|
||||
|
|
211
server/src/apub/private_message.rs
Normal file
211
server/src/apub/private_message.rs
Normal 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 = ¬e.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!()
|
||||
}
|
||||
}
|
1615
server/src/apub/shared_inbox.rs
Normal file
1615
server/src/apub/shared_inbox.rs
Normal file
File diff suppressed because it is too large
Load diff
|
@ -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))
|
||||
}
|
||||
|
|
312
server/src/apub/user_inbox.rs
Normal file
312
server/src/apub/user_inbox.rs
Normal 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(¬e, &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(¬e, &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(¬e, &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(¬e, &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
149
server/src/db/activity.rs
Normal 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);
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
|
|
173
server/src/db/code_migrations.rs
Normal file
173
server/src/db/code_migrations.rs
Normal 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(())
|
||||
}
|
|
@ -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();
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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> {
|
||||
|
|
|
@ -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"]
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
use super::*;
|
||||
use diesel::{result::Error, *};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
table! {
|
||||
site_view (id) {
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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> {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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>>,
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>>;
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
1
ui/.eslintignore
vendored
|
@ -1,2 +1,3 @@
|
|||
fuse.js
|
||||
translation_report.ts
|
||||
src/api_tests
|
||||
|
|
3
ui/.gitignore
vendored
3
ui/.gitignore
vendored
|
@ -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
10
ui/jest.config.js
vendored
Normal file
|
@ -0,0 +1,10 @@
|
|||
module.exports = {
|
||||
preset: 'ts-jest',
|
||||
testEnvironment: 'node',
|
||||
testTimeout: 30000,
|
||||
globals: {
|
||||
'ts-jest': {
|
||||
diagnostics: false,
|
||||
},
|
||||
},
|
||||
};
|
8
ui/package.json
vendored
8
ui/package.json
vendored
|
@ -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
1487
ui/src/api_tests/api.spec.ts
vendored
Normal file
File diff suppressed because it is too large
Load diff
6
ui/src/components/admin-settings.tsx
vendored
6
ui/src/components/admin-settings.tsx
vendored
|
@ -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>
|
||||
|
|
3
ui/src/components/comment-node.tsx
vendored
3
ui/src/components/comment-node.tsx
vendored
|
@ -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>
|
||||
|
|
5
ui/src/components/communities.tsx
vendored
5
ui/src/components/communities.tsx
vendored
|
@ -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
38
ui/src/components/community-link.tsx
vendored
Normal 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>;
|
||||
}
|
||||
}
|
5
ui/src/components/community.tsx
vendored
5
ui/src/components/community.tsx
vendored
|
@ -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: [],
|
||||
|
|
17
ui/src/components/main.tsx
vendored
17
ui/src/components/main.tsx
vendored
|
@ -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>
|
||||
|
|
7
ui/src/components/post-form.tsx
vendored
7
ui/src/components/post-form.tsx
vendored
|
@ -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>
|
||||
|
|
47
ui/src/components/post-listing.tsx
vendored
47
ui/src/components/post-listing.tsx
vendored
|
@ -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>
|
||||
|
|
3
ui/src/components/private-message-form.tsx
vendored
3
ui/src/components/private-message-form.tsx
vendored
|
@ -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
Loading…
Reference in a new issue