Compare commits
148 commits
master
...
federated_
Author | SHA1 | Date | |
---|---|---|---|
15f1920b25 | |||
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 |
100 changed files with 11805 additions and 2051 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.
|
||||
|
|
17
docker/dev/test_deploy.sh
vendored
17
docker/dev/test_deploy.sh
vendored
|
@ -1,11 +1,16 @@
|
|||
#!/bin/sh
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
BRANCH=$1
|
||||
|
||||
git checkout $BRANCH
|
||||
cd ../../
|
||||
|
||||
# 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
|
||||
|
|
22
docker/federation-test/run-tests.sh
vendored
Executable file
22
docker/federation-test/run-tests.sh
vendored
Executable file
|
@ -0,0 +1,22 @@
|
|||
#!/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
|
||||
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.38.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"]
|
92
docker/federation/docker-compose.yml
vendored
Normal file
92
docker/federation/docker-compose.yml
vendored
Normal file
|
@ -0,0 +1,92 @@
|
|||
version: '3.3'
|
||||
|
||||
services:
|
||||
nginx:
|
||||
image: nginx:1.17-alpine
|
||||
ports:
|
||||
- "8540:8540"
|
||||
- "8550:8550"
|
||||
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
|
||||
- pictshare_alpha
|
||||
- lemmy_beta
|
||||
- pictshare_beta
|
||||
- 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__INSTANCE_WHITELIST=lemmy_beta
|
||||
- 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
|
||||
pictshare_alpha:
|
||||
image: shtripok/pictshare:latest
|
||||
volumes:
|
||||
- ./volumes/pictshare_alpha:/usr/share/nginx/html/data
|
||||
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__INSTANCE_WHITELIST=lemmy_alpha
|
||||
- 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
|
||||
pictshare_beta:
|
||||
image: shtripok/pictshare:latest
|
||||
volumes:
|
||||
- ./volumes/pictshare_beta:/usr/share/nginx/html/data
|
||||
restart: always
|
||||
|
||||
iframely:
|
||||
image: dogbin/iframely:latest
|
||||
volumes:
|
||||
- ../iframely.config.local.js:/iframely/config.local.js:ro
|
||||
restart: always
|
75
docker/federation/nginx.conf
vendored
Normal file
75
docker/federation/nginx.conf
vendored
Normal file
|
@ -0,0 +1,75 @@
|
|||
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";
|
||||
}
|
||||
|
||||
location /pictshare/ {
|
||||
proxy_pass http://pictshare_alpha: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;
|
||||
}
|
||||
|
||||
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";
|
||||
}
|
||||
|
||||
location /pictshare/ {
|
||||
proxy_pass http://pictshare_beta: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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
17
docker/federation/run-federation-test.bash
vendored
Executable file
17
docker/federation/run-federation-test.bash
vendored
Executable file
|
@ -0,0 +1,17 @@
|
|||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
if [ "$1" = "-yarn" ]; then
|
||||
pushd ../../ui/ || exit
|
||||
yarn
|
||||
yarn build
|
||||
popd || exit
|
||||
fi
|
||||
|
||||
pushd ../../server/ || exit
|
||||
cargo build
|
||||
popd || exit
|
||||
|
||||
sudo docker build ../../ --file Dockerfile -t lemmy-federation:latest
|
||||
|
||||
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
|
||||
|
|
35
docs/src/contributing_federation_development.md
vendored
35
docs/src/contributing_federation_development.md
vendored
|
@ -5,17 +5,17 @@
|
|||
If you don't have a local clone of the Lemmy repo yet, just run the following command:
|
||||
|
||||
```bash
|
||||
git clone https://yerbamate.dev/nutomic/lemmy.git -b federation
|
||||
git clone https://yerbamate.dev/LemmyNet/lemmy.git -b federation
|
||||
```
|
||||
|
||||
If you already have the Lemmy repo cloned, you need to add a new remote:
|
||||
```bash
|
||||
git remote add federation https://yerbamate.dev/nutomic/lemmy.git
|
||||
git remote add federation https://yerbamate.dev/LemmyNet/lemmy.git
|
||||
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
|
||||
instance_whitelist: 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
|
||||
```
|
||||
|
|
2528
server/Cargo.lock
generated
vendored
2528
server/Cargo.lock
generated
vendored
File diff suppressed because it is too large
Load diff
11
server/Cargo.toml
vendored
11
server/Cargo.toml
vendored
|
@ -5,11 +5,11 @@ authors = ["Dessalines <happydooby@gmail.com>"]
|
|||
edition = "2018"
|
||||
|
||||
[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"
|
||||
activitystreams = "0.6.0"
|
||||
bcrypt = "0.6.2"
|
||||
activitypub = "0.2.0"
|
||||
chrono = { version = "0.4.7", features = ["serde"] }
|
||||
failure = "0.1.5"
|
||||
serde_json = { version = "1.0.48", features = ["preserve_order"]}
|
||||
|
@ -34,8 +34,15 @@ rss = "1.9.0"
|
|||
htmlescape = "0.3.1"
|
||||
config = "0.10.1"
|
||||
hjson = "0.8.2"
|
||||
url = { version = "2.1.1", features = ["serde"] }
|
||||
percent-encoding = "2.1.0"
|
||||
isahc = "0.9"
|
||||
comrak = "0.7"
|
||||
openssl = "0.10"
|
||||
http = "0.2.1"
|
||||
http-signature-normalization = "0.4.1"
|
||||
base64 = "0.12.0"
|
||||
tokio = "0.2.18"
|
||||
futures = "0.3.4"
|
||||
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
|
||||
instance_whitelist: ""
|
||||
}
|
||||
# # 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);
|
|
@ -87,7 +87,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 +102,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,13 +113,20 @@ impl Perform for Oper<CreateComment> {
|
|||
Err(_e) => return Err(APIError::err("couldnt_create_comment").into()),
|
||||
};
|
||||
|
||||
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)?;
|
||||
|
||||
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 {
|
||||
if let Ok(mention_user) = User_::read_from_name(&conn, (*username_mention).to_string()) {
|
||||
if let Ok(mention_user) = User_::read_from_name(&conn, username_mention) {
|
||||
// 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
|
||||
|
@ -224,6 +235,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 +279,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 +305,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,25 +322,44 @@ 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()),
|
||||
};
|
||||
|
||||
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)?;
|
||||
}
|
||||
|
||||
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());
|
||||
let mention_user = User_::read_from_name(&conn, username_mention);
|
||||
|
||||
if mention_user.is_ok() {
|
||||
let mention_user_id = mention_user?.id;
|
||||
|
@ -480,7 +516,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 +554,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
|
||||
|
|
|
@ -3,15 +3,15 @@ use super::*;
|
|||
#[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,
|
||||
}
|
||||
|
||||
|
@ -30,17 +30,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)]
|
||||
|
@ -134,25 +134,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()),
|
||||
};
|
||||
|
@ -165,8 +165,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
|
||||
|
@ -230,6 +232,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(),
|
||||
|
@ -240,6 +244,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) {
|
||||
|
@ -311,7 +321,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());
|
||||
}
|
||||
|
||||
|
@ -328,6 +339,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(),
|
||||
|
@ -338,9 +351,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()),
|
||||
};
|
||||
|
@ -361,6 +380,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 {
|
||||
|
@ -447,23 +480,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))?;
|
||||
|
@ -675,11 +726,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) {
|
||||
|
|
|
@ -22,19 +22,23 @@ use crate::{
|
|||
naive_now, remove_slurs, send_email, slur_check, slurs_vec_to_str,
|
||||
};
|
||||
|
||||
use crate::apub::{
|
||||
fetcher::search_by_apub_id,
|
||||
signatures::generate_actor_keypair,
|
||||
{make_apub_endpoint, ActorType, ApubLikeableType, ApubObjectType, EndpointType},
|
||||
};
|
||||
use crate::settings::Settings;
|
||||
use crate::websocket::UserOperation;
|
||||
use crate::websocket::{
|
||||
server::{
|
||||
JoinCommunityRoom, JoinPostRoom, JoinUserRoom, SendAllMessage, SendComment,
|
||||
SendCommunityRoomMessage, SendPost, SendUserRoomMessage,
|
||||
},
|
||||
WebsocketInfo,
|
||||
UserOperation, WebsocketInfo,
|
||||
};
|
||||
use diesel::r2d2::{ConnectionManager, Pool};
|
||||
use diesel::PgConnection;
|
||||
use failure::Error;
|
||||
use log::{error, info};
|
||||
use log::{debug, error, info};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::str::FromStr;
|
||||
|
||||
|
@ -62,8 +66,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,6 @@
|
|||
use super::*;
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct CreatePost {
|
||||
name: String,
|
||||
url: Option<String>,
|
||||
|
@ -31,7 +31,7 @@ pub struct GetPostResponse {
|
|||
pub online: usize,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct GetPosts {
|
||||
type_: String,
|
||||
sort: String,
|
||||
|
@ -41,9 +41,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 +112,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 +137,9 @@ impl Perform for Oper<CreatePost> {
|
|||
embed_description: iframely_description,
|
||||
embed_html: iframely_html,
|
||||
thumbnail_url: pictshare_thumbnail,
|
||||
ap_id: "changeme".into(),
|
||||
local: true,
|
||||
published: None,
|
||||
};
|
||||
|
||||
let inserted_post = match Post::create(&conn, &post_form) {
|
||||
|
@ -151,6 +155,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 +169,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 +369,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 +390,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 +467,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 +476,8 @@ impl Perform for Oper<EditPost> {
|
|||
let (iframely_title, iframely_description, iframely_html, pictshare_thumbnail) =
|
||||
fetch_iframely_and_pictshare_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 +494,12 @@ impl Perform for Oper<EditPost> {
|
|||
embed_description: iframely_description,
|
||||
embed_html: iframely_html,
|
||||
thumbnail_url: pictshare_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 +541,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 };
|
||||
|
|
|
@ -9,7 +9,7 @@ pub struct ListCategoriesResponse {
|
|||
categories: Vec<Category>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct Search {
|
||||
q: String,
|
||||
type_: String,
|
||||
|
@ -20,13 +20,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 +342,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 +359,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 +372,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 +416,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 +446,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)
|
||||
|
|
|
@ -186,7 +186,7 @@ pub struct PrivateMessagesResponse {
|
|||
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
pub struct PrivateMessageResponse {
|
||||
message: PrivateMessageView,
|
||||
pub message: PrivateMessageView,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
|
@ -261,10 +261,11 @@ impl Perform for Oper<Register> {
|
|||
return Err(APIError::err("admin_already_created").into());
|
||||
}
|
||||
|
||||
let user_keypair = generate_actor_keypair()?;
|
||||
|
||||
// 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,
|
||||
|
@ -280,6 +281,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
|
||||
|
@ -298,12 +305,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,
|
||||
|
@ -312,6 +322,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()
|
||||
}
|
||||
|
@ -406,7 +422,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(),
|
||||
|
@ -422,6 +437,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) {
|
||||
|
@ -483,7 +504,7 @@ impl Perform for Oper<GetUserDetails> {
|
|||
None => {
|
||||
match User_::read_from_name(
|
||||
&conn,
|
||||
data
|
||||
&data
|
||||
.username
|
||||
.to_owned()
|
||||
.unwrap_or_else(|| "admin".to_string()),
|
||||
|
@ -575,30 +596,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()),
|
||||
};
|
||||
|
@ -656,30 +654,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()),
|
||||
};
|
||||
|
@ -850,18 +825,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()),
|
||||
};
|
||||
|
@ -897,12 +861,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)
|
||||
|
@ -950,18 +917,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()),
|
||||
};
|
||||
|
@ -975,25 +931,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()),
|
||||
};
|
||||
|
@ -1099,19 +1037,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) {
|
||||
|
@ -1121,6 +1063,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 {
|
||||
|
@ -1164,7 +1114,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;
|
||||
|
||||
|
@ -1180,7 +1130,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());
|
||||
}
|
||||
|
||||
|
@ -1192,8 +1143,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 {
|
||||
|
@ -1207,17 +1158,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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
49
server/src/apub/activities.rs
Normal file
49
server/src/apub/activities.rs
Normal file
|
@ -0,0 +1,49 @@
|
|||
use super::*;
|
||||
|
||||
pub fn populate_object_props(
|
||||
props: &mut ObjectProperties,
|
||||
addressed_to: &str,
|
||||
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_cc_xsd_any_uri(addressed_to)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Send an activity to a list of recipients, using the correct headers etc.
|
||||
pub fn send_activity<A>(
|
||||
activity: &A,
|
||||
private_key: &str,
|
||||
sender_id: &str,
|
||||
to: Vec<String>,
|
||||
) -> Result<(), Error>
|
||||
where
|
||||
A: Serialize + Debug,
|
||||
{
|
||||
let json = serde_json::to_string(&activity)?;
|
||||
debug!("Sending activitypub activity {} to {:?}", json, to);
|
||||
// TODO it needs to expand, the to field needs to expand and dedup the followers urls
|
||||
// The inbox is determined by first retrieving the target actor's JSON-LD representation and then looking up the inbox property. If a recipient is a Collection or OrderedCollection, then the server MUST dereference the collection (with the user's credentials) and discover inboxes for each item in the collection. Servers MUST limit the number of layers of indirections through collections which will be performed, which MAY be one.
|
||||
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 request = Request::post(t).header("Host", to_url.domain().unwrap());
|
||||
let signature = sign(&request, private_key, sender_id)?;
|
||||
let res = request
|
||||
.header("Signature", signature)
|
||||
.header("Content-Type", "application/json")
|
||||
.body(json.to_owned())?
|
||||
.send()?;
|
||||
debug!("Result for activity send: {:?}", res);
|
||||
}
|
||||
Ok(())
|
||||
}
|
463
server/src/apub/comment.rs
Normal file
463
server/src/apub/comment.rs
Normal file
|
@ -0,0 +1,463 @@
|
|||
use super::*;
|
||||
|
||||
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();
|
||||
|
||||
// The 2nd item, if it exists, is the parent comment apub_id
|
||||
let parent_id: Option<i32> = match in_reply_tos.next() {
|
||||
Some(parent_comment_uri) => {
|
||||
let parent_comment_uri_str = &parent_comment_uri.to_string();
|
||||
let parent_comment = Comment::read_from_apub_id(&conn, &parent_comment_uri_str)?;
|
||||
|
||||
Some(parent_comment.id)
|
||||
}
|
||||
None => None,
|
||||
};
|
||||
|
||||
let post = Post::read_from_apub_id(&conn, &post_ap_id)?;
|
||||
|
||||
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 mut create = Create::new();
|
||||
populate_object_props(
|
||||
&mut create.object_props,
|
||||
&community.get_followers_url(),
|
||||
&id,
|
||||
)?;
|
||||
create
|
||||
.create_props
|
||||
.set_actor_xsd_any_uri(creator.actor_id.to_owned())?
|
||||
.set_object_base_box(note)?;
|
||||
|
||||
// Insert the sent activity into the activity table
|
||||
let activity_form = activity::ActivityForm {
|
||||
user_id: creator.id,
|
||||
data: serde_json::to_value(&create)?,
|
||||
local: true,
|
||||
updated: None,
|
||||
};
|
||||
activity::Activity::create(&conn, &activity_form)?;
|
||||
|
||||
send_activity(
|
||||
&create,
|
||||
&creator.private_key.as_ref().unwrap(),
|
||||
&creator.actor_id,
|
||||
community.get_follower_inboxes(&conn)?,
|
||||
)?;
|
||||
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 mut update = Update::new();
|
||||
populate_object_props(
|
||||
&mut update.object_props,
|
||||
&community.get_followers_url(),
|
||||
&id,
|
||||
)?;
|
||||
update
|
||||
.update_props
|
||||
.set_actor_xsd_any_uri(creator.actor_id.to_owned())?
|
||||
.set_object_base_box(note)?;
|
||||
|
||||
// Insert the sent activity into the activity table
|
||||
let activity_form = activity::ActivityForm {
|
||||
user_id: creator.id,
|
||||
data: serde_json::to_value(&update)?,
|
||||
local: true,
|
||||
updated: None,
|
||||
};
|
||||
activity::Activity::create(&conn, &activity_form)?;
|
||||
|
||||
send_activity(
|
||||
&update,
|
||||
&creator.private_key.as_ref().unwrap(),
|
||||
&creator.actor_id,
|
||||
community.get_follower_inboxes(&conn)?,
|
||||
)?;
|
||||
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,
|
||||
&community.get_followers_url(),
|
||||
&id,
|
||||
)?;
|
||||
|
||||
delete
|
||||
.delete_props
|
||||
.set_actor_xsd_any_uri(creator.actor_id.to_owned())?
|
||||
.set_object_base_box(note)?;
|
||||
|
||||
// Insert the sent activity into the activity table
|
||||
let activity_form = activity::ActivityForm {
|
||||
user_id: creator.id,
|
||||
data: serde_json::to_value(&delete)?,
|
||||
local: true,
|
||||
updated: None,
|
||||
};
|
||||
activity::Activity::create(&conn, &activity_form)?;
|
||||
|
||||
send_activity(
|
||||
&delete,
|
||||
&creator.private_key.as_ref().unwrap(),
|
||||
&creator.actor_id,
|
||||
community.get_follower_inboxes(&conn)?,
|
||||
)?;
|
||||
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,
|
||||
&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,
|
||||
&community.get_followers_url(),
|
||||
&undo_id,
|
||||
)?;
|
||||
|
||||
undo
|
||||
.undo_props
|
||||
.set_actor_xsd_any_uri(creator.actor_id.to_owned())?
|
||||
.set_object_base_box(delete)?;
|
||||
|
||||
// Insert the sent activity into the activity table
|
||||
let activity_form = activity::ActivityForm {
|
||||
user_id: creator.id,
|
||||
data: serde_json::to_value(&undo)?,
|
||||
local: true,
|
||||
updated: None,
|
||||
};
|
||||
activity::Activity::create(&conn, &activity_form)?;
|
||||
|
||||
send_activity(
|
||||
&undo,
|
||||
&creator.private_key.as_ref().unwrap(),
|
||||
&creator.actor_id,
|
||||
community.get_follower_inboxes(&conn)?,
|
||||
)?;
|
||||
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,
|
||||
&community.get_followers_url(),
|
||||
&id,
|
||||
)?;
|
||||
|
||||
remove
|
||||
.remove_props
|
||||
.set_actor_xsd_any_uri(mod_.actor_id.to_owned())?
|
||||
.set_object_base_box(note)?;
|
||||
|
||||
// Insert the sent activity into the activity table
|
||||
let activity_form = activity::ActivityForm {
|
||||
user_id: mod_.id,
|
||||
data: serde_json::to_value(&remove)?,
|
||||
local: true,
|
||||
updated: None,
|
||||
};
|
||||
activity::Activity::create(&conn, &activity_form)?;
|
||||
|
||||
send_activity(
|
||||
&remove,
|
||||
&mod_.private_key.as_ref().unwrap(),
|
||||
&mod_.actor_id,
|
||||
community.get_follower_inboxes(&conn)?,
|
||||
)?;
|
||||
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,
|
||||
&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,
|
||||
&community.get_followers_url(),
|
||||
&undo_id,
|
||||
)?;
|
||||
|
||||
undo
|
||||
.undo_props
|
||||
.set_actor_xsd_any_uri(mod_.actor_id.to_owned())?
|
||||
.set_object_base_box(remove)?;
|
||||
|
||||
// Insert the sent activity into the activity table
|
||||
let activity_form = activity::ActivityForm {
|
||||
user_id: mod_.id,
|
||||
data: serde_json::to_value(&undo)?,
|
||||
local: true,
|
||||
updated: None,
|
||||
};
|
||||
activity::Activity::create(&conn, &activity_form)?;
|
||||
|
||||
send_activity(
|
||||
&undo,
|
||||
&mod_.private_key.as_ref().unwrap(),
|
||||
&mod_.actor_id,
|
||||
community.get_follower_inboxes(&conn)?,
|
||||
)?;
|
||||
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, &community.get_followers_url(), &id)?;
|
||||
like
|
||||
.like_props
|
||||
.set_actor_xsd_any_uri(creator.actor_id.to_owned())?
|
||||
.set_object_base_box(note)?;
|
||||
|
||||
// Insert the sent activity into the activity table
|
||||
let activity_form = activity::ActivityForm {
|
||||
user_id: creator.id,
|
||||
data: serde_json::to_value(&like)?,
|
||||
local: true,
|
||||
updated: None,
|
||||
};
|
||||
activity::Activity::create(&conn, &activity_form)?;
|
||||
|
||||
send_activity(
|
||||
&like,
|
||||
&creator.private_key.as_ref().unwrap(),
|
||||
&creator.actor_id,
|
||||
community.get_follower_inboxes(&conn)?,
|
||||
)?;
|
||||
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,
|
||||
&community.get_followers_url(),
|
||||
&id,
|
||||
)?;
|
||||
dislike
|
||||
.dislike_props
|
||||
.set_actor_xsd_any_uri(creator.actor_id.to_owned())?
|
||||
.set_object_base_box(note)?;
|
||||
|
||||
// Insert the sent activity into the activity table
|
||||
let activity_form = activity::ActivityForm {
|
||||
user_id: creator.id,
|
||||
data: serde_json::to_value(&dislike)?,
|
||||
local: true,
|
||||
updated: None,
|
||||
};
|
||||
activity::Activity::create(&conn, &activity_form)?;
|
||||
|
||||
send_activity(
|
||||
&dislike,
|
||||
&creator.private_key.as_ref().unwrap(),
|
||||
&creator.actor_id,
|
||||
community.get_follower_inboxes(&conn)?,
|
||||
)?;
|
||||
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, &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,
|
||||
&community.get_followers_url(),
|
||||
&undo_id,
|
||||
)?;
|
||||
|
||||
undo
|
||||
.undo_props
|
||||
.set_actor_xsd_any_uri(creator.actor_id.to_owned())?
|
||||
.set_object_base_box(like)?;
|
||||
|
||||
// Insert the sent activity into the activity table
|
||||
let activity_form = activity::ActivityForm {
|
||||
user_id: creator.id,
|
||||
data: serde_json::to_value(&undo)?,
|
||||
local: true,
|
||||
updated: None,
|
||||
};
|
||||
activity::Activity::create(&conn, &activity_form)?;
|
||||
|
||||
send_activity(
|
||||
&undo,
|
||||
&creator.private_key.as_ref().unwrap(),
|
||||
&creator.actor_id,
|
||||
community.get_follower_inboxes(&conn)?,
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
|
@ -1,109 +1,416 @@
|
|||
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 super::*;
|
||||
|
||||
#[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_summary_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())?;
|
||||
|
||||
Ok(group.extend(actor_props).extend(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()
|
||||
}
|
||||
|
||||
/// 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
|
||||
.follow_props
|
||||
.get_actor_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 the sent activity into the activity table
|
||||
let activity_form = activity::ActivityForm {
|
||||
user_id: self.creator_id,
|
||||
data: serde_json::to_value(&accept)?,
|
||||
local: true,
|
||||
updated: None,
|
||||
};
|
||||
activity::Activity::create(&conn, &activity_form)?;
|
||||
|
||||
send_activity(
|
||||
&accept,
|
||||
&self.private_key.to_owned().unwrap(),
|
||||
&self.actor_id,
|
||||
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, &self.get_followers_url(), &id)?;
|
||||
|
||||
delete
|
||||
.delete_props
|
||||
.set_actor_xsd_any_uri(creator.actor_id.to_owned())?
|
||||
.set_object_base_box(group)?;
|
||||
|
||||
// Insert the sent activity into the activity table
|
||||
let activity_form = activity::ActivityForm {
|
||||
user_id: self.creator_id,
|
||||
data: serde_json::to_value(&delete)?,
|
||||
local: true,
|
||||
updated: None,
|
||||
};
|
||||
activity::Activity::create(&conn, &activity_form)?;
|
||||
|
||||
// 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.private_key.as_ref().unwrap(),
|
||||
&creator.actor_id,
|
||||
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, &self.get_followers_url(), &id)?;
|
||||
|
||||
delete
|
||||
.delete_props
|
||||
.set_actor_xsd_any_uri(creator.actor_id.to_owned())?
|
||||
.set_object_base_box(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, &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 the sent activity into the activity table
|
||||
let activity_form = activity::ActivityForm {
|
||||
user_id: self.creator_id,
|
||||
data: serde_json::to_value(&undo)?,
|
||||
local: true,
|
||||
updated: None,
|
||||
};
|
||||
activity::Activity::create(&conn, &activity_form)?;
|
||||
|
||||
// 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.private_key.as_ref().unwrap(),
|
||||
&creator.actor_id,
|
||||
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, &self.get_followers_url(), &id)?;
|
||||
|
||||
remove
|
||||
.remove_props
|
||||
.set_actor_xsd_any_uri(mod_.actor_id.to_owned())?
|
||||
.set_object_base_box(group)?;
|
||||
|
||||
// Insert the sent activity into the activity table
|
||||
let activity_form = activity::ActivityForm {
|
||||
user_id: mod_.id,
|
||||
data: serde_json::to_value(&remove)?,
|
||||
local: true,
|
||||
updated: None,
|
||||
};
|
||||
activity::Activity::create(&conn, &activity_form)?;
|
||||
|
||||
// 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_.private_key.as_ref().unwrap(),
|
||||
&mod_.actor_id,
|
||||
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, &self.get_followers_url(), &id)?;
|
||||
|
||||
remove
|
||||
.remove_props
|
||||
.set_actor_xsd_any_uri(mod_.actor_id.to_owned())?
|
||||
.set_object_base_box(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, &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 the sent activity into the activity table
|
||||
let activity_form = activity::ActivityForm {
|
||||
user_id: mod_.id,
|
||||
data: serde_json::to_value(&undo)?,
|
||||
local: true,
|
||||
updated: None,
|
||||
};
|
||||
activity::Activity::create(&conn, &activity_form)?;
|
||||
|
||||
// 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_.private_key.as_ref().unwrap(),
|
||||
&mod_.actor_id,
|
||||
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()
|
||||
// TODO eventually this will have to use the inbox or shared_inbox column, meaning that view
|
||||
// will have to change
|
||||
.map(|c| {
|
||||
// If the user is local, but the community isn't, get the community shared inbox
|
||||
// and vice versa
|
||||
if c.user_local && !c.community_local {
|
||||
get_shared_inbox(&c.community_actor_id)
|
||||
} else if !c.user_local && c.community_local {
|
||||
get_shared_inbox(&c.user_actor_id)
|
||||
} else {
|
||||
"".to_string()
|
||||
}
|
||||
})
|
||||
.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 oprops = &group.base.base.object_props;
|
||||
let aprops = &group.base.extension;
|
||||
let public_key: &PublicKey = &group.extension.public_key;
|
||||
|
||||
let _followers_uri = Url::parse(&aprops.get_followers().unwrap().to_string())?;
|
||||
let _outbox_uri = Url::parse(&aprops.get_outbox().to_string())?;
|
||||
// TODO don't do extra fetching here
|
||||
// let _outbox = fetch_remote_object::<OrderedCollection>(&outbox_uri)?;
|
||||
// let _followers = fetch_remote_object::<UnorderedCollection>(&followers_uri)?;
|
||||
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: 1, // -> peertube uses `"category": {"identifier": "9","name": "Comedy"},`
|
||||
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: false,
|
||||
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 siz (for privacy).
|
||||
// TODO this needs to return the actual followers, and the to: field needs this
|
||||
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))
|
||||
}
|
||||
|
||||
// TODO should not be doing this
|
||||
// Returns an UnorderedCollection with the latest posts from the community.
|
||||
//pub async fn get_apub_community_outbox(
|
||||
// info: Path<CommunityQuery>,
|
||||
// db: DbPoolParam,
|
||||
// chat_server: ChatServerParam,
|
||||
//) -> Result<HttpResponse<Body>, Error> {
|
||||
// let community = Community::read_from_name(&&db.get()?, &info.community_name)?;
|
||||
|
||||
// let conn = establish_unpooled_connection();
|
||||
// //As we are an object, we validated that the community id was valid
|
||||
// let community_posts: Vec<Post> = Post::list_for_community(&conn, community.id)?;
|
||||
|
||||
// let mut collection = OrderedCollection::default();
|
||||
// let oprops: &mut ObjectProperties = collection.as_mut();
|
||||
// oprops
|
||||
// .set_context_xsd_any_uri(context())?
|
||||
// .set_id(community.actor_id)?;
|
||||
// collection
|
||||
// .collection_props
|
||||
// .set_many_items_base_boxes(
|
||||
// community_posts
|
||||
// .iter()
|
||||
// .map(|c| c.as_page(&conn).unwrap())
|
||||
// .collect(),
|
||||
// )?
|
||||
// .set_total_items(community_posts.len() as u64)?;
|
||||
|
||||
// Ok(create_apub_response(&collection))
|
||||
//}
|
||||
|
|
135
server/src/apub/community_inbox.rs
Normal file
135
server/src/apub/community_inbox.rs
Normal file
|
@ -0,0 +1,135 @@
|
|||
use super::*;
|
||||
|
||||
#[serde(untagged)]
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub enum CommunityAcceptedObjects {
|
||||
Follow(Follow),
|
||||
Undo(Undo),
|
||||
}
|
||||
|
||||
// TODO Consolidate community and user inboxes into a single shared one
|
||||
/// 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 community_name = path.into_inner();
|
||||
debug!(
|
||||
"Community {} received activity {:?}",
|
||||
&community_name, &input
|
||||
);
|
||||
match input {
|
||||
CommunityAcceptedObjects::Follow(f) => {
|
||||
handle_follow(&f, &request, &community_name, db, chat_server)
|
||||
}
|
||||
CommunityAcceptedObjects::Undo(u) => {
|
||||
handle_undo_follow(&u, &request, &community_name, db, chat_server)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle a follow request from a remote user, adding it to the local database and returning an
|
||||
/// Accept activity.
|
||||
fn handle_follow(
|
||||
follow: &Follow,
|
||||
request: &HttpRequest,
|
||||
community_name: &str,
|
||||
db: DbPoolParam,
|
||||
_chat_server: ChatServerParam,
|
||||
) -> Result<HttpResponse, Error> {
|
||||
let user_uri = follow
|
||||
.follow_props
|
||||
.get_actor_xsd_any_uri()
|
||||
.unwrap()
|
||||
.to_string();
|
||||
let _community_uri = follow
|
||||
.follow_props
|
||||
.get_object_xsd_any_uri()
|
||||
.unwrap()
|
||||
.to_string();
|
||||
|
||||
let conn = db.get()?;
|
||||
|
||||
let user = get_or_fetch_and_upsert_remote_user(&user_uri, &conn)?;
|
||||
let community = Community::read_from_name(&conn, &community_name)?;
|
||||
|
||||
verify(&request, &user.public_key.unwrap())?;
|
||||
|
||||
// Insert the received activity into the activity table
|
||||
let activity_form = activity::ActivityForm {
|
||||
user_id: user.id,
|
||||
data: serde_json::to_value(&follow)?,
|
||||
local: false,
|
||||
updated: None,
|
||||
};
|
||||
activity::Activity::create(&conn, &activity_form)?;
|
||||
|
||||
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,
|
||||
request: &HttpRequest,
|
||||
community_name: &str,
|
||||
db: DbPoolParam,
|
||||
_chat_server: ChatServerParam,
|
||||
) -> Result<HttpResponse, Error> {
|
||||
let follow = undo
|
||||
.undo_props
|
||||
.get_object_base_box()
|
||||
.to_owned()
|
||||
.unwrap()
|
||||
.to_owned()
|
||||
.into_concrete::<Follow>()?;
|
||||
|
||||
let user_uri = follow
|
||||
.follow_props
|
||||
.get_actor_xsd_any_uri()
|
||||
.unwrap()
|
||||
.to_string();
|
||||
|
||||
let _community_uri = follow
|
||||
.follow_props
|
||||
.get_object_xsd_any_uri()
|
||||
.unwrap()
|
||||
.to_string();
|
||||
|
||||
let conn = db.get()?;
|
||||
|
||||
let user = get_or_fetch_and_upsert_remote_user(&user_uri, &conn)?;
|
||||
let community = Community::read_from_name(&conn, &community_name)?;
|
||||
|
||||
verify(&request, &user.public_key.unwrap())?;
|
||||
|
||||
// Insert the received activity into the activity table
|
||||
let activity_form = activity::ActivityForm {
|
||||
user_id: user.id,
|
||||
data: serde_json::to_value(&follow)?,
|
||||
local: false,
|
||||
updated: None,
|
||||
};
|
||||
activity::Activity::create(&conn, &activity_form)?;
|
||||
|
||||
let community_follower_form = CommunityFollowerForm {
|
||||
community_id: community.id,
|
||||
user_id: user.id,
|
||||
};
|
||||
|
||||
CommunityFollower::unfollow(&conn, &community_follower_form).ok();
|
||||
|
||||
Ok(HttpResponse::Ok().finish())
|
||||
}
|
221
server/src/apub/fetcher.rs
Normal file
221
server/src/apub/fetcher.rs
Normal file
|
@ -0,0 +1,221 @@
|
|||
use super::*;
|
||||
|
||||
// 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 = Request::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>),
|
||||
}
|
||||
|
||||
/// 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/p/3
|
||||
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.base.base.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.base.base.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)?];
|
||||
}
|
||||
}
|
||||
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
|
||||
&& u
|
||||
.last_refreshed_at
|
||||
.lt(&(naive_now() - chrono::Duration::days(1)))
|
||||
{
|
||||
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)),
|
||||
}
|
||||
}
|
||||
|
||||
/// 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 its older than a day, re-fetch it
|
||||
if !c.local
|
||||
&& c
|
||||
.last_refreshed_at
|
||||
.lt(&(naive_now() - chrono::Duration::days(1)))
|
||||
{
|
||||
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
|
||||
.base
|
||||
.base
|
||||
.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)),
|
||||
}
|
||||
}
|
||||
|
||||
// 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,294 @@
|
|||
pub mod activities;
|
||||
pub mod comment;
|
||||
pub mod community;
|
||||
pub mod community_inbox;
|
||||
pub mod fetcher;
|
||||
pub mod page_extension;
|
||||
pub mod post;
|
||||
pub mod private_message;
|
||||
pub mod shared_inbox;
|
||||
pub mod signatures;
|
||||
pub mod user;
|
||||
use crate::Settings;
|
||||
pub mod user_inbox;
|
||||
|
||||
use std::fmt::Display;
|
||||
use crate::api::community::CommunityResponse;
|
||||
use crate::websocket::server::SendCommunityRoomMessage;
|
||||
use activitystreams::object::kind::{NoteType, PageType};
|
||||
use activitystreams::{
|
||||
activity::{Accept, Create, Delete, Dislike, Follow, Like, Remove, Undo, Update},
|
||||
actor::{kind::GroupType, properties::ApActorProperties, Actor, Group, Person},
|
||||
collection::UnorderedCollection,
|
||||
context,
|
||||
endpoint::EndpointProperties,
|
||||
ext::{Ext, Extensible, Extension},
|
||||
object::{properties::ObjectProperties, Note, Page, Tombstone},
|
||||
public, BaseBox,
|
||||
};
|
||||
use actix_web::body::Body;
|
||||
use actix_web::web::Path;
|
||||
use actix_web::{web, HttpRequest, HttpResponse, Result};
|
||||
use diesel::result::Error::NotFound;
|
||||
use diesel::PgConnection;
|
||||
use failure::Error;
|
||||
use failure::_core::fmt::Debug;
|
||||
use http::request::Builder;
|
||||
use http_signature_normalization::Config;
|
||||
use isahc::prelude::*;
|
||||
use itertools::Itertools;
|
||||
use log::debug;
|
||||
use openssl::hash::MessageDigest;
|
||||
use openssl::sign::{Signer, Verifier};
|
||||
use openssl::{pkey::PKey, rsa::Rsa};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::BTreeMap;
|
||||
use std::time::Duration;
|
||||
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};
|
||||
use crate::api::comment::CommentResponse;
|
||||
use crate::api::post::PostResponse;
|
||||
use crate::api::site::SearchResponse;
|
||||
use crate::api::user::PrivateMessageResponse;
|
||||
use crate::db::comment::{Comment, CommentForm, CommentLike, CommentLikeForm};
|
||||
use crate::db::comment_view::CommentView;
|
||||
use crate::db::community::{
|
||||
Community, CommunityFollower, CommunityFollowerForm, CommunityForm, CommunityModerator,
|
||||
CommunityModeratorForm,
|
||||
};
|
||||
use crate::db::community_view::{CommunityFollowerView, CommunityModeratorView, CommunityView};
|
||||
use crate::db::post::{Post, PostForm, PostLike, PostLikeForm};
|
||||
use crate::db::post_view::PostView;
|
||||
use crate::db::private_message::{PrivateMessage, PrivateMessageForm};
|
||||
use crate::db::private_message_view::PrivateMessageView;
|
||||
use crate::db::user::{UserForm, User_};
|
||||
use crate::db::user_view::UserView;
|
||||
use crate::db::{activity, Crud, Followable, Joinable, Likeable, SearchType};
|
||||
use crate::routes::nodeinfo::{NodeInfo, NodeInfoWellKnown};
|
||||
use crate::routes::{ChatServerParam, DbPoolParam};
|
||||
use crate::websocket::{
|
||||
server::{SendComment, SendPost, SendUserRoomMessage},
|
||||
UserOperation,
|
||||
};
|
||||
use crate::{convert_datetime, naive_now, Settings};
|
||||
|
||||
#[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,
|
||||
};
|
||||
use crate::apub::page_extension::PageExtension;
|
||||
use activities::{populate_object_props, send_activity};
|
||||
use activitystreams::Base;
|
||||
use chrono::NaiveDateTime;
|
||||
use fetcher::{get_or_fetch_and_upsert_remote_community, get_or_fetch_and_upsert_remote_user};
|
||||
use signatures::verify;
|
||||
use signatures::{sign, PublicKey, PublicKeyExtension};
|
||||
|
||||
let person = user.as_person();
|
||||
assert_eq!(
|
||||
format!("https://{}/federation/u/thom", Settings::get().hostname),
|
||||
person.object_props.id_string().unwrap()
|
||||
);
|
||||
}
|
||||
type GroupExt = Ext<Ext<Group, ApActorProperties>, PublicKeyExtension>;
|
||||
type PersonExt = Ext<Ext<Person, ApActorProperties>, PublicKeyExtension>;
|
||||
type PageExt = Ext<Page, PageExtension>;
|
||||
|
||||
#[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,
|
||||
};
|
||||
pub static APUB_JSON_CONTENT_TYPE: &str = "application/activity+json";
|
||||
|
||||
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 name.
|
||||
///
|
||||
/// TODO: we will probably need to change apub endpoint urls so that html and activity+json content
|
||||
/// types are handled at the same endpoint, so that you can copy the url into mastodon search
|
||||
/// and have it fetch the object.
|
||||
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",
|
||||
// TODO I have to change this else my update advanced_migrations crashes the
|
||||
// server if a comment exists.
|
||||
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 whitelist.
|
||||
fn is_apub_id_valid(apub_id: &Url) -> bool {
|
||||
if apub_id.scheme() != get_apub_protocol_string() {
|
||||
return false;
|
||||
}
|
||||
|
||||
let whitelist: Vec<String> = Settings::get()
|
||||
.federation
|
||||
.instance_whitelist
|
||||
.split(',')
|
||||
.map(|d| d.to_string())
|
||||
.collect();
|
||||
match apub_id.domain() {
|
||||
Some(d) => whitelist.contains(&d.to_owned()),
|
||||
None => false,
|
||||
}
|
||||
}
|
||||
|
||||
// TODO Not sure good names for these
|
||||
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::default();
|
||||
tombstone.object_props.set_id(object_id)?;
|
||||
tombstone
|
||||
.tombstone_props
|
||||
.set_former_type_xsd_string(former_type)?
|
||||
.set_deleted(convert_datetime(updated))?;
|
||||
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;
|
||||
|
||||
// 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()
|
||||
}
|
||||
}
|
||||
|
|
10
server/src/apub/page_extension.rs
Normal file
10
server/src/apub/page_extension.rs
Normal file
|
@ -0,0 +1,10 @@
|
|||
use super::*;
|
||||
|
||||
#[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 {}
|
|
@ -1,38 +1,473 @@
|
|||
use crate::apub::make_apub_endpoint;
|
||||
use crate::db::post::Post;
|
||||
use crate::to_datetime_utc;
|
||||
use activitypub::{context, object::Page};
|
||||
use super::*;
|
||||
|
||||
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())?;
|
||||
}
|
||||
|
||||
if let Some(u) = self.updated {
|
||||
oprops.set_updated(convert_datetime(u))?;
|
||||
}
|
||||
|
||||
let ext = PageExtension {
|
||||
comments_enabled: !self.locked,
|
||||
sensitive: self.nsfw,
|
||||
};
|
||||
Ok(page.extend(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.extension;
|
||||
let page = &page.base;
|
||||
let oprops = &page.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)?;
|
||||
|
||||
Ok(PostForm {
|
||||
name: oprops.get_summary_xsd_string().unwrap().to_string(),
|
||||
url: oprops.get_url_xsd_any_uri().map(|u| u.to_string()),
|
||||
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: None, // -> attachment? or fetch the embed locally
|
||||
embed_description: None,
|
||||
embed_html: None,
|
||||
thumbnail_url: None,
|
||||
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,
|
||||
&community.get_followers_url(),
|
||||
&id,
|
||||
)?;
|
||||
create
|
||||
.create_props
|
||||
.set_actor_xsd_any_uri(creator.actor_id.to_owned())?
|
||||
.set_object_base_box(page)?;
|
||||
|
||||
// Insert the sent activity into the activity table
|
||||
let activity_form = activity::ActivityForm {
|
||||
user_id: creator.id,
|
||||
data: serde_json::to_value(&create)?,
|
||||
local: true,
|
||||
updated: None,
|
||||
};
|
||||
activity::Activity::create(&conn, &activity_form)?;
|
||||
|
||||
send_activity(
|
||||
&create,
|
||||
&creator.private_key.as_ref().unwrap(),
|
||||
&creator.actor_id,
|
||||
community.get_follower_inboxes(&conn)?,
|
||||
)?;
|
||||
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,
|
||||
&community.get_followers_url(),
|
||||
&id,
|
||||
)?;
|
||||
update
|
||||
.update_props
|
||||
.set_actor_xsd_any_uri(creator.actor_id.to_owned())?
|
||||
.set_object_base_box(page)?;
|
||||
|
||||
// Insert the sent activity into the activity table
|
||||
let activity_form = activity::ActivityForm {
|
||||
user_id: creator.id,
|
||||
data: serde_json::to_value(&update)?,
|
||||
local: true,
|
||||
updated: None,
|
||||
};
|
||||
activity::Activity::create(&conn, &activity_form)?;
|
||||
|
||||
send_activity(
|
||||
&update,
|
||||
&creator.private_key.as_ref().unwrap(),
|
||||
&creator.actor_id,
|
||||
community.get_follower_inboxes(&conn)?,
|
||||
)?;
|
||||
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,
|
||||
&community.get_followers_url(),
|
||||
&id,
|
||||
)?;
|
||||
|
||||
delete
|
||||
.delete_props
|
||||
.set_actor_xsd_any_uri(creator.actor_id.to_owned())?
|
||||
.set_object_base_box(page)?;
|
||||
|
||||
// Insert the sent activity into the activity table
|
||||
let activity_form = activity::ActivityForm {
|
||||
user_id: self.creator_id,
|
||||
data: serde_json::to_value(&delete)?,
|
||||
local: true,
|
||||
updated: None,
|
||||
};
|
||||
activity::Activity::create(&conn, &activity_form)?;
|
||||
|
||||
let community = Community::read(conn, self.community_id)?;
|
||||
send_activity(
|
||||
&delete,
|
||||
&creator.private_key.as_ref().unwrap(),
|
||||
&creator.actor_id,
|
||||
community.get_follower_inboxes(&conn)?,
|
||||
)?;
|
||||
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,
|
||||
&community.get_followers_url(),
|
||||
&id,
|
||||
)?;
|
||||
|
||||
delete
|
||||
.delete_props
|
||||
.set_actor_xsd_any_uri(creator.actor_id.to_owned())?
|
||||
.set_object_base_box(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,
|
||||
&community.get_followers_url(),
|
||||
&undo_id,
|
||||
)?;
|
||||
|
||||
undo
|
||||
.undo_props
|
||||
.set_actor_xsd_any_uri(creator.actor_id.to_owned())?
|
||||
.set_object_base_box(delete)?;
|
||||
|
||||
// Insert the sent activity into the activity table
|
||||
let activity_form = activity::ActivityForm {
|
||||
user_id: self.creator_id,
|
||||
data: serde_json::to_value(&undo)?,
|
||||
local: true,
|
||||
updated: None,
|
||||
};
|
||||
activity::Activity::create(&conn, &activity_form)?;
|
||||
|
||||
let community = Community::read(conn, self.community_id)?;
|
||||
send_activity(
|
||||
&undo,
|
||||
&creator.private_key.as_ref().unwrap(),
|
||||
&creator.actor_id,
|
||||
community.get_follower_inboxes(&conn)?,
|
||||
)?;
|
||||
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,
|
||||
&community.get_followers_url(),
|
||||
&id,
|
||||
)?;
|
||||
|
||||
remove
|
||||
.remove_props
|
||||
.set_actor_xsd_any_uri(mod_.actor_id.to_owned())?
|
||||
.set_object_base_box(page)?;
|
||||
|
||||
// Insert the sent activity into the activity table
|
||||
let activity_form = activity::ActivityForm {
|
||||
user_id: mod_.id,
|
||||
data: serde_json::to_value(&remove)?,
|
||||
local: true,
|
||||
updated: None,
|
||||
};
|
||||
activity::Activity::create(&conn, &activity_form)?;
|
||||
|
||||
let community = Community::read(conn, self.community_id)?;
|
||||
send_activity(
|
||||
&remove,
|
||||
&mod_.private_key.as_ref().unwrap(),
|
||||
&mod_.actor_id,
|
||||
community.get_follower_inboxes(&conn)?,
|
||||
)?;
|
||||
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,
|
||||
&community.get_followers_url(),
|
||||
&id,
|
||||
)?;
|
||||
|
||||
remove
|
||||
.remove_props
|
||||
.set_actor_xsd_any_uri(mod_.actor_id.to_owned())?
|
||||
.set_object_base_box(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,
|
||||
&community.get_followers_url(),
|
||||
&undo_id,
|
||||
)?;
|
||||
|
||||
undo
|
||||
.undo_props
|
||||
.set_actor_xsd_any_uri(mod_.actor_id.to_owned())?
|
||||
.set_object_base_box(remove)?;
|
||||
|
||||
// Insert the sent activity into the activity table
|
||||
let activity_form = activity::ActivityForm {
|
||||
user_id: mod_.id,
|
||||
data: serde_json::to_value(&undo)?,
|
||||
local: true,
|
||||
updated: None,
|
||||
};
|
||||
activity::Activity::create(&conn, &activity_form)?;
|
||||
|
||||
let community = Community::read(conn, self.community_id)?;
|
||||
send_activity(
|
||||
&undo,
|
||||
&mod_.private_key.as_ref().unwrap(),
|
||||
&mod_.actor_id,
|
||||
community.get_follower_inboxes(&conn)?,
|
||||
)?;
|
||||
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, &community.get_followers_url(), &id)?;
|
||||
like
|
||||
.like_props
|
||||
.set_actor_xsd_any_uri(creator.actor_id.to_owned())?
|
||||
.set_object_base_box(page)?;
|
||||
|
||||
// Insert the sent activity into the activity table
|
||||
let activity_form = activity::ActivityForm {
|
||||
user_id: creator.id,
|
||||
data: serde_json::to_value(&like)?,
|
||||
local: true,
|
||||
updated: None,
|
||||
};
|
||||
activity::Activity::create(&conn, &activity_form)?;
|
||||
|
||||
send_activity(
|
||||
&like,
|
||||
&creator.private_key.as_ref().unwrap(),
|
||||
&creator.actor_id,
|
||||
community.get_follower_inboxes(&conn)?,
|
||||
)?;
|
||||
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,
|
||||
&community.get_followers_url(),
|
||||
&id,
|
||||
)?;
|
||||
dislike
|
||||
.dislike_props
|
||||
.set_actor_xsd_any_uri(creator.actor_id.to_owned())?
|
||||
.set_object_base_box(page)?;
|
||||
|
||||
// Insert the sent activity into the activity table
|
||||
let activity_form = activity::ActivityForm {
|
||||
user_id: creator.id,
|
||||
data: serde_json::to_value(&dislike)?,
|
||||
local: true,
|
||||
updated: None,
|
||||
};
|
||||
activity::Activity::create(&conn, &activity_form)?;
|
||||
|
||||
send_activity(
|
||||
&dislike,
|
||||
&creator.private_key.as_ref().unwrap(),
|
||||
&creator.actor_id,
|
||||
community.get_follower_inboxes(&conn)?,
|
||||
)?;
|
||||
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, &community.get_followers_url(), &id)?;
|
||||
like
|
||||
.like_props
|
||||
.set_actor_xsd_any_uri(creator.actor_id.to_owned())?
|
||||
.set_object_base_box(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,
|
||||
&community.get_followers_url(),
|
||||
&undo_id,
|
||||
)?;
|
||||
|
||||
undo
|
||||
.undo_props
|
||||
.set_actor_xsd_any_uri(creator.actor_id.to_owned())?
|
||||
.set_object_base_box(like)?;
|
||||
|
||||
// Insert the sent activity into the activity table
|
||||
let activity_form = activity::ActivityForm {
|
||||
user_id: creator.id,
|
||||
data: serde_json::to_value(&undo)?,
|
||||
local: true,
|
||||
updated: None,
|
||||
};
|
||||
activity::Activity::create(&conn, &activity_form)?;
|
||||
|
||||
send_activity(
|
||||
&undo,
|
||||
&creator.private_key.as_ref().unwrap(),
|
||||
&creator.actor_id,
|
||||
community.get_follower_inboxes(&conn)?,
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
234
server/src/apub/private_message.rs
Normal file
234
server/src/apub/private_message.rs
Normal file
|
@ -0,0 +1,234 @@
|
|||
use super::*;
|
||||
|
||||
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 the sent activity into the activity table
|
||||
let activity_form = activity::ActivityForm {
|
||||
user_id: creator.id,
|
||||
data: serde_json::to_value(&create)?,
|
||||
local: true,
|
||||
updated: None,
|
||||
};
|
||||
activity::Activity::create(&conn, &activity_form)?;
|
||||
|
||||
send_activity(
|
||||
&create,
|
||||
&creator.private_key.as_ref().unwrap(),
|
||||
&creator.actor_id,
|
||||
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 the sent activity into the activity table
|
||||
let activity_form = activity::ActivityForm {
|
||||
user_id: creator.id,
|
||||
data: serde_json::to_value(&update)?,
|
||||
local: true,
|
||||
updated: None,
|
||||
};
|
||||
activity::Activity::create(&conn, &activity_form)?;
|
||||
|
||||
send_activity(
|
||||
&update,
|
||||
&creator.private_key.as_ref().unwrap(),
|
||||
&creator.actor_id,
|
||||
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 the sent activity into the activity table
|
||||
let activity_form = activity::ActivityForm {
|
||||
user_id: creator.id,
|
||||
data: serde_json::to_value(&delete)?,
|
||||
local: true,
|
||||
updated: None,
|
||||
};
|
||||
activity::Activity::create(&conn, &activity_form)?;
|
||||
|
||||
send_activity(
|
||||
&delete,
|
||||
&creator.private_key.as_ref().unwrap(),
|
||||
&creator.actor_id,
|
||||
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 the sent activity into the activity table
|
||||
let activity_form = activity::ActivityForm {
|
||||
user_id: creator.id,
|
||||
data: serde_json::to_value(&undo)?,
|
||||
local: true,
|
||||
updated: None,
|
||||
};
|
||||
activity::Activity::create(&conn, &activity_form)?;
|
||||
|
||||
send_activity(
|
||||
&undo,
|
||||
&creator.private_key.as_ref().unwrap(),
|
||||
&creator.actor_id,
|
||||
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!()
|
||||
}
|
||||
}
|
1567
server/src/apub/shared_inbox.rs
Normal file
1567
server/src/apub/shared_inbox.rs
Normal file
File diff suppressed because it is too large
Load diff
122
server/src/apub/signatures.rs
Normal file
122
server/src/apub/signatures.rs
Normal file
|
@ -0,0 +1,122 @@
|
|||
use super::*;
|
||||
|
||||
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)?,
|
||||
})
|
||||
}
|
||||
|
||||
/// Signs request headers with the given keypair.
|
||||
/// TODO: would be nice to pass the sending actor in, instead of raw privatekey/id strings
|
||||
pub fn sign(request: &Builder, private_key: &str, sender_id: &str) -> Result<String, Error> {
|
||||
let signing_key_id = format!("{}#main-key", sender_id);
|
||||
|
||||
let headers = request
|
||||
.headers_ref()
|
||||
.unwrap()
|
||||
.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 signature_header_value = HTTP_SIG_CONFIG
|
||||
.begin_sign(
|
||||
request.method_ref().unwrap().as_str(),
|
||||
request
|
||||
.uri_ref()
|
||||
.unwrap()
|
||||
.path_and_query()
|
||||
.unwrap()
|
||||
.as_str(),
|
||||
headers,
|
||||
)
|
||||
.sign(signing_key_id, |signing_string| {
|
||||
let private_key = PKey::private_key_from_pem(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, public_key: &str) -> 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 {}",
|
||||
&public_key, &signing_string
|
||||
);
|
||||
let public_key = PKey::public_key_from_pem(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: Actor {}
|
|
@ -1,74 +1,212 @@
|
|||
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 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
|
||||
}
|
||||
}
|
||||
use super::*;
|
||||
|
||||
#[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())?;
|
||||
}
|
||||
|
||||
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(person.extend(actor_props).extend(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()
|
||||
}
|
||||
|
||||
/// 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 mut follow = Follow::new();
|
||||
|
||||
let id = format!("{}/follow/{}", self.actor_id, uuid::Uuid::new_v4());
|
||||
|
||||
follow
|
||||
.object_props
|
||||
.set_context_xsd_any_uri(context())?
|
||||
.set_id(id)?;
|
||||
follow
|
||||
.follow_props
|
||||
.set_actor_xsd_any_uri(self.actor_id.to_owned())?
|
||||
.set_object_xsd_any_uri(follow_actor_id)?;
|
||||
let to = format!("{}/inbox", follow_actor_id);
|
||||
|
||||
// Insert the sent activity into the activity table
|
||||
let activity_form = activity::ActivityForm {
|
||||
user_id: self.id,
|
||||
data: serde_json::to_value(&follow)?,
|
||||
local: true,
|
||||
updated: None,
|
||||
};
|
||||
activity::Activity::create(&conn, &activity_form)?;
|
||||
|
||||
send_activity(
|
||||
&follow,
|
||||
&self.private_key.as_ref().unwrap(),
|
||||
&follow_actor_id,
|
||||
vec![to],
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn send_unfollow(&self, follow_actor_id: &str, conn: &PgConnection) -> Result<(), Error> {
|
||||
let mut follow = Follow::new();
|
||||
|
||||
let id = format!("{}/follow/{}", self.actor_id, uuid::Uuid::new_v4());
|
||||
|
||||
follow
|
||||
.object_props
|
||||
.set_context_xsd_any_uri(context())?
|
||||
.set_id(id)?;
|
||||
follow
|
||||
.follow_props
|
||||
.set_actor_xsd_any_uri(self.actor_id.to_owned())?
|
||||
.set_object_xsd_any_uri(follow_actor_id)?;
|
||||
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::default();
|
||||
|
||||
undo
|
||||
.object_props
|
||||
.set_context_xsd_any_uri(context())?
|
||||
.set_id(undo_id)?;
|
||||
|
||||
undo
|
||||
.undo_props
|
||||
.set_actor_xsd_any_uri(self.actor_id.to_owned())?
|
||||
.set_object_base_box(follow)?;
|
||||
|
||||
// Insert the sent activity into the activity table
|
||||
let activity_form = activity::ActivityForm {
|
||||
user_id: self.id,
|
||||
data: serde_json::to_value(&undo)?,
|
||||
local: true,
|
||||
updated: None,
|
||||
};
|
||||
activity::Activity::create(&conn, &activity_form)?;
|
||||
|
||||
send_activity(
|
||||
&undo,
|
||||
&self.private_key.as_ref().unwrap(),
|
||||
&follow_actor_id,
|
||||
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.base.base.object_props;
|
||||
let aprops = &person.base.extension;
|
||||
let public_key: &PublicKey = &person.extension.public_key;
|
||||
|
||||
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: None, // -> icon, image
|
||||
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))
|
||||
}
|
||||
|
|
320
server/src/apub/user_inbox.rs
Normal file
320
server/src/apub/user_inbox.rs
Normal file
|
@ -0,0 +1,320 @@
|
|||
use super::*;
|
||||
|
||||
#[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.public_key.unwrap())?;
|
||||
|
||||
let user = User_::read_from_name(&conn, username)?;
|
||||
|
||||
// Insert the received activity into the activity table
|
||||
let activity_form = activity::ActivityForm {
|
||||
user_id: community.creator_id,
|
||||
data: serde_json::to_value(&accept)?,
|
||||
local: false,
|
||||
updated: None,
|
||||
};
|
||||
activity::Activity::create(&conn, &activity_form)?;
|
||||
|
||||
// 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
|
||||
// TODO: at this point, indicate to the user that they are following the community
|
||||
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.public_key.unwrap())?;
|
||||
|
||||
// Insert the received activity into the activity table
|
||||
let activity_form = activity::ActivityForm {
|
||||
user_id: user.id,
|
||||
data: serde_json::to_value(&create)?,
|
||||
local: false,
|
||||
updated: None,
|
||||
};
|
||||
activity::Activity::create(&conn, &activity_form)?;
|
||||
|
||||
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.public_key.unwrap())?;
|
||||
|
||||
// Insert the received activity into the activity table
|
||||
let activity_form = activity::ActivityForm {
|
||||
user_id: user.id,
|
||||
data: serde_json::to_value(&update)?,
|
||||
local: false,
|
||||
updated: None,
|
||||
};
|
||||
activity::Activity::create(&conn, &activity_form)?;
|
||||
|
||||
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.public_key.unwrap())?;
|
||||
|
||||
// Insert the received activity into the activity table
|
||||
let activity_form = activity::ActivityForm {
|
||||
user_id: user.id,
|
||||
data: serde_json::to_value(&delete)?,
|
||||
local: false,
|
||||
updated: None,
|
||||
};
|
||||
activity::Activity::create(&conn, &activity_form)?;
|
||||
|
||||
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.public_key.unwrap())?;
|
||||
|
||||
// Insert the received activity into the activity table
|
||||
let activity_form = activity::ActivityForm {
|
||||
user_id: user.id,
|
||||
data: serde_json::to_value(&delete)?,
|
||||
local: false,
|
||||
updated: None,
|
||||
};
|
||||
activity::Activity::create(&conn, &activity_form)?;
|
||||
|
||||
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())
|
||||
}
|
123
server/src/db/activity.rs
Normal file
123
server/src/db/activity.rs
Normal file
|
@ -0,0 +1,123 @@
|
|||
use super::*;
|
||||
use crate::schema::activity;
|
||||
use crate::schema::activity::dsl::*;
|
||||
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> {
|
||||
activity.find(activity_id).first::<Self>(conn)
|
||||
}
|
||||
|
||||
fn delete(conn: &PgConnection, activity_id: i32) -> Result<usize, Error> {
|
||||
diesel::delete(activity.find(activity_id)).execute(conn)
|
||||
}
|
||||
|
||||
fn create(conn: &PgConnection, new_activity: &ActivityForm) -> Result<Self, Error> {
|
||||
insert_into(activity)
|
||||
.values(new_activity)
|
||||
.get_result::<Self>(conn)
|
||||
}
|
||||
|
||||
fn update(
|
||||
conn: &PgConnection,
|
||||
activity_id: i32,
|
||||
new_activity: &ActivityForm,
|
||||
) -> Result<Self, Error> {
|
||||
diesel::update(activity.find(activity_id))
|
||||
.set(new_activity)
|
||||
.get_result::<Self>(conn)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::super::user::*;
|
||||
use super::*;
|
||||
|
||||
#[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);
|
||||
}
|
||||
}
|
169
server/src/db/code_migrations.rs
Normal file
169
server/src/db/code_migrations.rs
Normal file
|
@ -0,0 +1,169 @@
|
|||
// This is for db migrations that require code
|
||||
use super::comment::Comment;
|
||||
use super::community::{Community, CommunityForm};
|
||||
use super::post::Post;
|
||||
use super::private_message::PrivateMessage;
|
||||
use super::user::{UserForm, User_};
|
||||
use super::*;
|
||||
use crate::apub::signatures::generate_actor_keypair;
|
||||
use crate::apub::{make_apub_endpoint, EndpointType};
|
||||
use crate::naive_now;
|
||||
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,5 +1,7 @@
|
|||
use super::post::Post;
|
||||
use super::*;
|
||||
use crate::apub::{make_apub_endpoint, EndpointType};
|
||||
use crate::naive_now;
|
||||
use crate::schema::{comment, comment_like, comment_saved};
|
||||
|
||||
// WITH RECURSIVE MyTree AS (
|
||||
|
@ -19,10 +21,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 +38,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 +75,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"]
|
||||
|
@ -170,7 +213,6 @@ mod tests {
|
|||
|
||||
let new_user = UserForm {
|
||||
name: "terry".into(),
|
||||
fedi_name: "rrf".into(),
|
||||
preferred_username: None,
|
||||
password_encrypted: "nope".into(),
|
||||
email: None,
|
||||
|
@ -186,6 +228,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 +248,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 +274,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 +289,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 +308,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 +320,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();
|
||||
|
|
|
@ -14,10 +14,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 +49,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 +87,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 +300,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 +339,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,
|
||||
|
@ -434,7 +464,6 @@ mod tests {
|
|||
|
||||
let new_user = UserForm {
|
||||
name: "timmy".into(),
|
||||
fedi_name: "rrf".into(),
|
||||
preferred_username: None,
|
||||
password_encrypted: "nope".into(),
|
||||
email: None,
|
||||
|
@ -450,6 +479,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 +499,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 +525,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 +540,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 +582,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 +615,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)
|
||||
|
|
|
@ -15,9 +15,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 +32,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,13 +74,25 @@ 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 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)
|
||||
}
|
||||
|
||||
pub fn get_url(&self) -> String {
|
||||
format!("https://{}/c/{}", Settings::get().hostname, self.name)
|
||||
}
|
||||
|
@ -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> {
|
||||
|
@ -216,7 +240,6 @@ mod tests {
|
|||
|
||||
let new_user = UserForm {
|
||||
name: "bobbee".into(),
|
||||
fedi_name: "rrf".into(),
|
||||
preferred_username: None,
|
||||
password_encrypted: "nope".into(),
|
||||
email: None,
|
||||
|
@ -232,6 +255,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 +275,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 +297,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 +351,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();
|
||||
|
|
|
@ -15,6 +15,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 +45,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 +68,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 +84,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 +100,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 +126,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 +284,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 +318,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 +352,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,
|
||||
}
|
||||
|
||||
|
|
|
@ -4,7 +4,9 @@ use diesel::result::Error;
|
|||
use diesel::*;
|
||||
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 +44,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;
|
||||
}
|
||||
|
|
|
@ -438,7 +438,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 +453,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 +480,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 +500,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 +526,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 +541,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();
|
||||
|
|
|
@ -88,7 +88,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 +103,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,4 +1,6 @@
|
|||
use super::*;
|
||||
use crate::apub::{make_apub_endpoint, EndpointType};
|
||||
use crate::naive_now;
|
||||
use crate::schema::{post, post_like, post_read, post_saved};
|
||||
|
||||
#[derive(Queryable, Identifiable, PartialEq, Debug, Serialize, Deserialize)]
|
||||
|
@ -21,9 +23,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 +37,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 +46,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 {
|
||||
|
@ -191,7 +246,6 @@ mod tests {
|
|||
|
||||
let new_user = UserForm {
|
||||
name: "jim".into(),
|
||||
fedi_name: "rrf".into(),
|
||||
preferred_username: None,
|
||||
password_encrypted: "nope".into(),
|
||||
email: None,
|
||||
|
@ -207,6 +261,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 +281,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 +307,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 +332,8 @@ mod tests {
|
|||
embed_description: None,
|
||||
embed_html: None,
|
||||
thumbnail_url: None,
|
||||
ap_id: "changeme".into(),
|
||||
local: true,
|
||||
};
|
||||
|
||||
// Post Like
|
||||
|
|
|
@ -22,10 +22,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 +69,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 +119,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,
|
||||
|
@ -359,7 +377,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 +392,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 +412,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 +438,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 +505,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 +550,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,4 +1,5 @@
|
|||
use super::*;
|
||||
use crate::apub::{make_apub_endpoint, EndpointType};
|
||||
use crate::schema::private_message;
|
||||
|
||||
#[derive(Queryable, Identifiable, PartialEq, Debug, Serialize, Deserialize)]
|
||||
|
@ -12,6 +13,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 +22,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,6 +61,28 @@ 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::*;
|
||||
|
@ -65,7 +93,6 @@ mod tests {
|
|||
|
||||
let creator_form = UserForm {
|
||||
name: "creator_pm".into(),
|
||||
fedi_name: "rrf".into(),
|
||||
preferred_username: None,
|
||||
password_encrypted: "nope".into(),
|
||||
email: None,
|
||||
|
@ -81,13 +108,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 +135,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 +168,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();
|
||||
|
|
|
@ -12,10 +12,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 +35,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 +61,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,7 +1,7 @@
|
|||
use super::*;
|
||||
use crate::schema::user_;
|
||||
use crate::schema::user_::dsl::*;
|
||||
use crate::{is_email_regex, Settings};
|
||||
use crate::{is_email_regex, naive_now, Settings};
|
||||
use bcrypt::{hash, DEFAULT_COST};
|
||||
use jsonwebtoken::{decode, encode, DecodingKey, EncodingKey, Header, TokenData, Validation};
|
||||
|
||||
|
@ -10,7 +10,6 @@ use jsonwebtoken::{decode, encode, DecodingKey, EncodingKey, Header, TokenData,
|
|||
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 +26,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 +53,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 +88,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 +97,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 +160,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,
|
||||
|
@ -186,7 +217,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 +232,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 +245,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 +261,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();
|
||||
|
|
|
@ -64,7 +64,6 @@ mod tests {
|
|||
|
||||
let new_user = UserForm {
|
||||
name: "terrylake".into(),
|
||||
fedi_name: "rrf".into(),
|
||||
preferred_username: None,
|
||||
password_encrypted: "nope".into(),
|
||||
email: None,
|
||||
|
@ -80,13 +79,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 +106,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 +126,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 +152,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 +167,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();
|
||||
|
|
|
@ -7,6 +7,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 +18,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 +33,8 @@ table! {
|
|||
my_vote -> Nullable<Int4>,
|
||||
saved -> Nullable<Bool>,
|
||||
recipient_id -> Int4,
|
||||
recipient_actor_id -> Text,
|
||||
recipient_local -> Bool,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -37,6 +43,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 +54,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 +69,8 @@ table! {
|
|||
my_vote -> Nullable<Int4>,
|
||||
saved -> Nullable<Bool>,
|
||||
recipient_id -> Int4,
|
||||
recipient_actor_id -> Text,
|
||||
recipient_local -> Bool,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -70,6 +82,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 +93,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 +108,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> {
|
||||
|
|
|
@ -5,11 +5,13 @@ use diesel::pg::Pg;
|
|||
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 +27,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 +52,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,8 +36,9 @@ pub mod settings;
|
|||
pub mod version;
|
||||
pub mod websocket;
|
||||
|
||||
use crate::settings::Settings;
|
||||
use actix_web::dev::ConnectionInfo;
|
||||
use chrono::{DateTime, NaiveDateTime, Utc};
|
||||
use chrono::{DateTime, FixedOffset, Local, NaiveDateTime, Utc};
|
||||
use isahc::prelude::*;
|
||||
use lettre::smtp::authentication::{Credentials, Mechanism};
|
||||
use lettre::smtp::extension::ClientId;
|
||||
|
@ -49,8 +52,6 @@ use rand::{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;
|
||||
|
@ -69,6 +70,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)
|
||||
}
|
||||
|
|
|
@ -6,19 +6,21 @@ use actix::prelude::*;
|
|||
use actix_web::*;
|
||||
use diesel::r2d2::{ConnectionManager, Pool};
|
||||
use diesel::PgConnection;
|
||||
use failure::Error;
|
||||
use lemmy_server::{
|
||||
db::code_migrations::run_advanced_migrations,
|
||||
rate_limit::{rate_limiter::RateLimiter, RateLimit},
|
||||
routes::{api, federation, feeds, index, nodeinfo, webfinger},
|
||||
settings::Settings,
|
||||
websocket::server::*,
|
||||
};
|
||||
use std::{io, sync::Arc};
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
embed_migrations!();
|
||||
|
||||
#[actix_rt::main]
|
||||
async fn main() -> io::Result<()> {
|
||||
async fn main() -> Result<(), Error> {
|
||||
env_logger::init();
|
||||
let settings = Settings::get();
|
||||
|
||||
|
@ -32,6 +34,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 {
|
||||
|
@ -47,31 +50,33 @@ async fn main() -> io::Result<()> {
|
|||
);
|
||||
|
||||
// Create Http server with websocket support
|
||||
HttpServer::new(move || {
|
||||
let settings = Settings::get();
|
||||
let rate_limiter = rate_limiter.clone();
|
||||
App::new()
|
||||
.wrap(middleware::Logger::default())
|
||||
.data(pool.clone())
|
||||
.data(server.clone())
|
||||
// The routes
|
||||
.configure(move |cfg| api::config(cfg, &rate_limiter))
|
||||
.configure(federation::config)
|
||||
.configure(feeds::config)
|
||||
.configure(index::config)
|
||||
.configure(nodeinfo::config)
|
||||
.configure(webfinger::config)
|
||||
// static files
|
||||
.service(actix_files::Files::new(
|
||||
"/static",
|
||||
settings.front_end_dir.to_owned(),
|
||||
))
|
||||
.service(actix_files::Files::new(
|
||||
"/docs",
|
||||
settings.front_end_dir + "/documentation",
|
||||
))
|
||||
})
|
||||
.bind((settings.bind, settings.port))?
|
||||
.run()
|
||||
.await
|
||||
Ok(
|
||||
HttpServer::new(move || {
|
||||
let settings = Settings::get();
|
||||
let rate_limiter = rate_limiter.clone();
|
||||
App::new()
|
||||
.wrap(middleware::Logger::default())
|
||||
.data(pool.clone())
|
||||
.data(server.clone())
|
||||
// The routes
|
||||
.configure(move |cfg| api::config(cfg, &rate_limiter))
|
||||
.configure(federation::config)
|
||||
.configure(feeds::config)
|
||||
.configure(index::config)
|
||||
.configure(nodeinfo::config)
|
||||
.configure(webfinger::config)
|
||||
// static files
|
||||
.service(actix_files::Files::new(
|
||||
"/static",
|
||||
settings.front_end_dir.to_owned(),
|
||||
))
|
||||
.service(actix_files::Files::new(
|
||||
"/docs",
|
||||
settings.front_end_dir + "/documentation",
|
||||
))
|
||||
})
|
||||
.bind((settings.bind, settings.port))?
|
||||
.run()
|
||||
.await?,
|
||||
)
|
||||
}
|
||||
|
|
|
@ -83,6 +83,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,38 @@
|
|||
use super::*;
|
||||
use crate::apub;
|
||||
use crate::apub::community::*;
|
||||
use crate::apub::community_inbox::community_inbox;
|
||||
use crate::apub::post::get_apub_post;
|
||||
use crate::apub::shared_inbox::shared_inbox;
|
||||
use crate::apub::user::*;
|
||||
use crate::apub::user_inbox::user_inbox;
|
||||
use crate::apub::APUB_JSON_CONTENT_TYPE;
|
||||
|
||||
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)),
|
||||
)
|
||||
// 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));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -25,10 +25,7 @@ pub fn config(cfg: &mut web::ServiceConfig) {
|
|||
.route("/feeds/all.xml", web::get().to(feeds::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,7 +141,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 = Community::read_from_name(&conn, &community_name)?;
|
||||
let community_url = community.get_url();
|
||||
|
||||
let posts = PostQueryBuilder::create(&conn)
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
use crate::api::{Oper, Perform};
|
||||
use crate::apub::get_apub_protocol_string;
|
||||
use crate::db::site_view::SiteView;
|
||||
use crate::rate_limit::rate_limiter::RateLimiter;
|
||||
use crate::websocket::{server::ChatServer, WebsocketInfo};
|
||||
|
@ -21,6 +22,7 @@ use std::str::FromStr;
|
|||
use std::sync::{Arc, Mutex};
|
||||
use std::time::{Duration, Instant};
|
||||
use strum::ParseError;
|
||||
use url::Url;
|
||||
|
||||
pub type DbPoolParam = web::Data<Pool<ConnectionManager<PgConnection>>>;
|
||||
pub type RateLimitParam = web::Data<Arc<Mutex<RateLimiter>>>;
|
||||
|
|
|
@ -6,26 +6,28 @@ 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 = if Settings::get().federation_enabled {
|
||||
let protocols = if Settings::get().federation.enabled {
|
||||
vec!["activitypub".to_string()]
|
||||
} else {
|
||||
vec![]
|
||||
|
@ -53,41 +55,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,
|
||||
}
|
||||
|
|
|
@ -7,7 +7,7 @@ pub struct Params {
|
|||
}
|
||||
|
||||
pub fn config(cfg: &mut web::ServiceConfig) {
|
||||
if Settings::get().federation_enabled {
|
||||
if Settings::get().federation.enabled {
|
||||
cfg.route(
|
||||
".well-known/webfinger",
|
||||
web::get().to(get_webfinger_response),
|
||||
|
@ -31,26 +31,22 @@ 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
|
||||
.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 {
|
||||
.map(|c| c.get(1))
|
||||
.flatten();
|
||||
let community_name = match regex_parsed {
|
||||
Some(c) => c.as_str(),
|
||||
None => return Err(format_err!("not_found")),
|
||||
};
|
||||
|
||||
// Make sure the requested community exists.
|
||||
let community = match Community::read_from_name(&conn, community_name.to_string()) {
|
||||
let community = match Community::read_from_name(&conn, &community_name) {
|
||||
Ok(o) => o,
|
||||
Err(_) => return Err(format_err!("not_found")),
|
||||
};
|
||||
|
@ -63,22 +59,21 @@ async fn get_webfinger_response(
|
|||
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}"
|
||||
//}
|
||||
{
|
||||
"rel": "http://webfinger.net/rel/profile-page",
|
||||
"type": "text/html",
|
||||
"href": community.get_url(),
|
||||
},
|
||||
{
|
||||
"rel": "self",
|
||||
"type": "application/activity+json",
|
||||
"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}"
|
||||
//}
|
||||
]
|
||||
}))
|
||||
})
|
||||
|
|
|
@ -32,7 +32,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 {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -20,7 +20,7 @@ pub struct Settings {
|
|||
pub front_end_dir: String,
|
||||
pub rate_limit: RateLimitConfig,
|
||||
pub email: Option<EmailConfig>,
|
||||
pub federation_enabled: bool,
|
||||
pub federation: Federation,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
|
@ -60,6 +60,13 @@ pub struct Database {
|
|||
pub pool_size: u32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
pub struct Federation {
|
||||
pub enabled: bool,
|
||||
pub tls_enabled: bool,
|
||||
pub instance_whitelist: String,
|
||||
}
|
||||
|
||||
lazy_static! {
|
||||
static ref SETTINGS: RwLock<Settings> = RwLock::new(match Settings::init() {
|
||||
Ok(c) => c,
|
||||
|
|
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",
|
||||
|
|
1114
ui/src/api_tests/api.spec.ts
vendored
Normal file
1114
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>
|
||||
|
|
5
ui/src/components/comment-form.tsx
vendored
5
ui/src/components/comment-form.tsx
vendored
|
@ -162,8 +162,9 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
|
|||
</button>
|
||||
{this.state.commentForm.content && (
|
||||
<button
|
||||
className={`btn btn-sm mr-2 btn-secondary ${this.state
|
||||
.previewMode && 'active'}`}
|
||||
className={`btn btn-sm mr-2 btn-secondary ${
|
||||
this.state.previewMode && 'active'
|
||||
}`}
|
||||
onClick={linkEvent(this, this.handlePreviewToggle)}
|
||||
>
|
||||
{i18n.t('preview')}
|
||||
|
|
29
ui/src/components/comment-node.tsx
vendored
29
ui/src/components/comment-node.tsx
vendored
|
@ -142,9 +142,11 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
|
|||
}
|
||||
>
|
||||
<div
|
||||
class={`${!this.props.noIndent &&
|
||||
class={`${
|
||||
!this.props.noIndent &&
|
||||
this.props.node.comment.parent_id &&
|
||||
'ml-2'}`}
|
||||
'ml-2'
|
||||
}`}
|
||||
>
|
||||
<div class="d-flex flex-wrap align-items-center mb-1 mt-1 text-muted small">
|
||||
<span class="mr-2">
|
||||
|
@ -152,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>
|
||||
|
@ -249,8 +254,9 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
|
|||
this.loadingIcon
|
||||
) : (
|
||||
<svg
|
||||
class={`icon icon-inline ${node.comment.read &&
|
||||
'text-success'}`}
|
||||
class={`icon icon-inline ${
|
||||
node.comment.read && 'text-success'
|
||||
}`}
|
||||
>
|
||||
<use xlinkHref="#icon-check"></use>
|
||||
</svg>
|
||||
|
@ -302,8 +308,9 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
|
|||
this.loadingIcon
|
||||
) : (
|
||||
<svg
|
||||
class={`icon icon-inline ${node.comment.saved &&
|
||||
'text-warning'}`}
|
||||
class={`icon icon-inline ${
|
||||
node.comment.saved && 'text-warning'
|
||||
}`}
|
||||
>
|
||||
<use xlinkHref="#icon-star"></use>
|
||||
</svg>
|
||||
|
@ -350,8 +357,9 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
|
|||
data-tippy-content={i18n.t('view_source')}
|
||||
>
|
||||
<svg
|
||||
class={`icon icon-inline ${this.state
|
||||
.viewSource && 'text-success'}`}
|
||||
class={`icon icon-inline ${
|
||||
this.state.viewSource && 'text-success'
|
||||
}`}
|
||||
>
|
||||
<use xlinkHref="#icon-file-text"></use>
|
||||
</svg>
|
||||
|
@ -380,8 +388,9 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
|
|||
}
|
||||
>
|
||||
<svg
|
||||
class={`icon icon-inline ${node.comment
|
||||
.deleted && 'text-danger'}`}
|
||||
class={`icon icon-inline ${
|
||||
node.comment.deleted && 'text-danger'
|
||||
}`}
|
||||
>
|
||||
<use xlinkHref="#icon-trash"></use>
|
||||
</svg>
|
||||
|
|
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>
|
||||
|
@ -319,6 +325,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>
|
||||
|
|
2
ui/src/components/navbar.tsx
vendored
2
ui/src/components/navbar.tsx
vendored
|
@ -381,7 +381,7 @@ export class Navbar extends Component<any, NavbarState> {
|
|||
|
||||
requestNotificationPermission() {
|
||||
if (UserService.Instance.user) {
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
if (!Notification) {
|
||||
toast(i18n.t('notifications_error'), 'danger');
|
||||
return;
|
||||
|
|
17
ui/src/components/post-form.tsx
vendored
17
ui/src/components/post-form.tsx
vendored
|
@ -35,6 +35,7 @@ import {
|
|||
setupTribute,
|
||||
setupTippy,
|
||||
emojiPicker,
|
||||
hostname,
|
||||
} from '../utils';
|
||||
import autosize from 'autosize';
|
||||
import Tribute from 'tributejs/src/Tribute.js';
|
||||
|
@ -194,8 +195,9 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
|
|||
<form>
|
||||
<label
|
||||
htmlFor="file-upload"
|
||||
className={`${UserService.Instance.user &&
|
||||
'pointer'} d-inline-block float-right text-muted font-weight-bold`}
|
||||
className={`${
|
||||
UserService.Instance.user && 'pointer'
|
||||
} d-inline-block float-right text-muted font-weight-bold`}
|
||||
data-tippy-content={i18n.t('upload_image')}
|
||||
>
|
||||
<svg class="icon icon-inline">
|
||||
|
@ -288,8 +290,9 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
|
|||
)}
|
||||
{this.state.postForm.body && (
|
||||
<button
|
||||
className={`mt-1 mr-2 btn btn-sm btn-secondary ${this.state
|
||||
.previewMode && 'active'}`}
|
||||
className={`mt-1 mr-2 btn btn-sm btn-secondary ${
|
||||
this.state.previewMode && 'active'
|
||||
}`}
|
||||
onClick={linkEvent(this, this.handlePreviewToggle)}
|
||||
>
|
||||
{i18n.t('preview')}
|
||||
|
@ -329,7 +332,11 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
|
|||
onInput={linkEvent(this, this.handlePostCommunityChange)}
|
||||
>
|
||||
{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>
|
||||
|
|
78
ui/src/components/post-listing.tsx
vendored
78
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,
|
||||
pictshareImage,
|
||||
setupTippy,
|
||||
hostname,
|
||||
previewLines,
|
||||
} from '../utils';
|
||||
import { i18n } from '../i18next';
|
||||
|
@ -150,9 +152,9 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
|||
let post = this.props.post;
|
||||
return (
|
||||
<img
|
||||
className={`img-fluid thumbnail rounded ${(post.nsfw ||
|
||||
post.community_nsfw) &&
|
||||
'img-blur'}`}
|
||||
className={`img-fluid thumbnail rounded ${
|
||||
(post.nsfw || post.community_nsfw) && 'img-blur'
|
||||
}`}
|
||||
src={src}
|
||||
/>
|
||||
);
|
||||
|
@ -312,22 +314,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 ? (
|
||||
|
@ -420,6 +421,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 && (
|
||||
|
@ -440,9 +444,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>
|
||||
|
@ -542,8 +551,9 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
|||
}
|
||||
>
|
||||
<svg
|
||||
class={`icon icon-inline ${post.saved &&
|
||||
'text-warning'}`}
|
||||
class={`icon icon-inline ${
|
||||
post.saved && 'text-warning'
|
||||
}`}
|
||||
>
|
||||
<use xlinkHref="#icon-star"></use>
|
||||
</svg>
|
||||
|
@ -586,8 +596,9 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
|||
}
|
||||
>
|
||||
<svg
|
||||
class={`icon icon-inline ${post.deleted &&
|
||||
'text-danger'}`}
|
||||
class={`icon icon-inline ${
|
||||
post.deleted && 'text-danger'
|
||||
}`}
|
||||
>
|
||||
<use xlinkHref="#icon-trash"></use>
|
||||
</svg>
|
||||
|
@ -618,8 +629,9 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
|||
data-tippy-content={i18n.t('view_source')}
|
||||
>
|
||||
<svg
|
||||
class={`icon icon-inline ${this.state
|
||||
.viewSource && 'text-success'}`}
|
||||
class={`icon icon-inline ${
|
||||
this.state.viewSource && 'text-success'
|
||||
}`}
|
||||
>
|
||||
<use xlinkHref="#icon-file-text"></use>
|
||||
</svg>
|
||||
|
@ -639,8 +651,9 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
|||
}
|
||||
>
|
||||
<svg
|
||||
class={`icon icon-inline ${post.locked &&
|
||||
'text-danger'}`}
|
||||
class={`icon icon-inline ${
|
||||
post.locked && 'text-danger'
|
||||
}`}
|
||||
>
|
||||
<use xlinkHref="#icon-lock"></use>
|
||||
</svg>
|
||||
|
@ -657,8 +670,9 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
|||
}
|
||||
>
|
||||
<svg
|
||||
class={`icon icon-inline ${post.stickied &&
|
||||
'text-success'}`}
|
||||
class={`icon icon-inline ${
|
||||
post.stickied && 'text-success'
|
||||
}`}
|
||||
>
|
||||
<use xlinkHref="#icon-pin"></use>
|
||||
</svg>
|
||||
|
|
20
ui/src/components/post.tsx
vendored
20
ui/src/components/post.tsx
vendored
|
@ -213,8 +213,9 @@ export class Post extends Component<any, PostState> {
|
|||
return (
|
||||
<div class="btn-group btn-group-toggle mb-2">
|
||||
<label
|
||||
className={`btn btn-sm btn-secondary pointer ${this.state
|
||||
.commentSort === CommentSortType.Hot && 'active'}`}
|
||||
className={`btn btn-sm btn-secondary pointer ${
|
||||
this.state.commentSort === CommentSortType.Hot && 'active'
|
||||
}`}
|
||||
>
|
||||
{i18n.t('hot')}
|
||||
<input
|
||||
|
@ -225,8 +226,9 @@ export class Post extends Component<any, PostState> {
|
|||
/>
|
||||
</label>
|
||||
<label
|
||||
className={`btn btn-sm btn-secondary pointer ${this.state
|
||||
.commentSort === CommentSortType.Top && 'active'}`}
|
||||
className={`btn btn-sm btn-secondary pointer ${
|
||||
this.state.commentSort === CommentSortType.Top && 'active'
|
||||
}`}
|
||||
>
|
||||
{i18n.t('top')}
|
||||
<input
|
||||
|
@ -237,8 +239,9 @@ export class Post extends Component<any, PostState> {
|
|||
/>
|
||||
</label>
|
||||
<label
|
||||
className={`btn btn-sm btn-secondary pointer ${this.state
|
||||
.commentSort === CommentSortType.New && 'active'}`}
|
||||
className={`btn btn-sm btn-secondary pointer ${
|
||||
this.state.commentSort === CommentSortType.New && 'active'
|
||||
}`}
|
||||
>
|
||||
{i18n.t('new')}
|
||||
<input
|
||||
|
@ -249,8 +252,9 @@ export class Post extends Component<any, PostState> {
|
|||
/>
|
||||
</label>
|
||||
<label
|
||||
className={`btn btn-sm btn-secondary pointer ${this.state
|
||||
.commentSort === CommentSortType.Old && 'active'}`}
|
||||
className={`btn btn-sm btn-secondary pointer ${
|
||||
this.state.commentSort === CommentSortType.Old && 'active'
|
||||
}`}
|
||||
>
|
||||
{i18n.t('old')}
|
||||
<input
|
||||
|
|
8
ui/src/components/private-message-form.tsx
vendored
8
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>
|
||||
|
@ -222,8 +225,9 @@ export class PrivateMessageForm extends Component<
|
|||
</button>
|
||||
{this.state.privateMessageForm.content && (
|
||||
<button
|
||||
className={`btn btn-secondary mr-2 ${this.state.previewMode &&
|
||||
'active'}`}
|
||||
className={`btn btn-secondary mr-2 ${
|
||||
this.state.previewMode && 'active'
|
||||
}`}
|
||||
onClick={linkEvent(this, this.handlePreviewToggle)}
|
||||
>
|
||||
{i18n.t('preview')}
|
||||
|
|
15
ui/src/components/private-message.tsx
vendored
15
ui/src/components/private-message.tsx
vendored
|
@ -144,8 +144,9 @@ export class PrivateMessage extends Component<
|
|||
}
|
||||
>
|
||||
<svg
|
||||
class={`icon icon-inline ${message.read &&
|
||||
'text-success'}`}
|
||||
class={`icon icon-inline ${
|
||||
message.read && 'text-success'
|
||||
}`}
|
||||
>
|
||||
<use xlinkHref="#icon-check"></use>
|
||||
</svg>
|
||||
|
@ -188,8 +189,9 @@ export class PrivateMessage extends Component<
|
|||
}
|
||||
>
|
||||
<svg
|
||||
class={`icon icon-inline ${message.deleted &&
|
||||
'text-danger'}`}
|
||||
class={`icon icon-inline ${
|
||||
message.deleted && 'text-danger'
|
||||
}`}
|
||||
>
|
||||
<use xlinkHref="#icon-trash"></use>
|
||||
</svg>
|
||||
|
@ -204,8 +206,9 @@ export class PrivateMessage extends Component<
|
|||
data-tippy-content={i18n.t('view_source')}
|
||||
>
|
||||
<svg
|
||||
class={`icon icon-inline ${this.state.viewSource &&
|
||||
'text-success'}`}
|
||||
class={`icon icon-inline ${
|
||||
this.state.viewSource && 'text-success'
|
||||
}`}
|
||||
>
|
||||
<use xlinkHref="#icon-file-text"></use>
|
||||
</svg>
|
||||
|
|
39
ui/src/components/search.tsx
vendored
39
ui/src/components/search.tsx
vendored
|
@ -22,8 +22,6 @@ import {
|
|||
fetchLimit,
|
||||
routeSearchTypeToEnum,
|
||||
routeSortTypeToEnum,
|
||||
pictshareAvatarThumbnail,
|
||||
showAvatars,
|
||||
toast,
|
||||
createCommentLikeRes,
|
||||
createPostLikeFindRes,
|
||||
|
@ -31,6 +29,7 @@ import {
|
|||
} from '../utils';
|
||||
import { PostListing } from './post-listing';
|
||||
import { UserListing } from './user-listing';
|
||||
import { CommunityLink } from './community-link';
|
||||
import { SortSelect } from './sort-select';
|
||||
import { CommentNodes } from './comment-nodes';
|
||||
import { i18n } from '../i18next';
|
||||
|
@ -110,7 +109,6 @@ export class Search extends Component<any, SearchState> {
|
|||
nextProps.history.action == 'POP' ||
|
||||
nextProps.history.action == 'PUSH'
|
||||
) {
|
||||
this.state = this.emptyState;
|
||||
this.state.q = this.getSearchQueryFromProps(nextProps);
|
||||
this.state.type_ = this.getSearchTypeFromProps(nextProps);
|
||||
this.state.sort = this.getSortTypeFromProps(nextProps);
|
||||
|
@ -253,16 +251,7 @@ export class Search extends Component<any, SearchState> {
|
|||
/>
|
||||
)}
|
||||
{i.type_ == 'communities' && (
|
||||
<div>
|
||||
<span>
|
||||
<Link to={`/c/${(i.data as Community).name}`}>{`/c/${
|
||||
(i.data as Community).name
|
||||
}`}</Link>
|
||||
</span>
|
||||
<span>{` - ${(i.data as Community).title} - ${
|
||||
(i.data as Community).number_of_subscribers
|
||||
} subscribers`}</span>
|
||||
</div>
|
||||
<div>{this.communityListing(i.data as Community)}</div>
|
||||
)}
|
||||
{i.type_ == 'users' && (
|
||||
<div>
|
||||
|
@ -316,20 +305,28 @@ export class Search extends Component<any, SearchState> {
|
|||
<>
|
||||
{this.state.searchResponse.communities.map(community => (
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<span>
|
||||
<Link
|
||||
to={`/c/${community.name}`}
|
||||
>{`/c/${community.name}`}</Link>
|
||||
</span>
|
||||
<span>{` - ${community.title} - ${community.number_of_subscribers} subscribers`}</span>
|
||||
</div>
|
||||
<div class="col-12">{this.communityListing(community)}</div>
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
communityListing(community: Community) {
|
||||
return (
|
||||
<>
|
||||
<span>
|
||||
<CommunityLink community={community} />
|
||||
</span>
|
||||
<span>{` - ${community.title} -
|
||||
${i18n.t('number_of_subscribers', {
|
||||
count: community.number_of_subscribers,
|
||||
})}
|
||||
`}</span>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
users() {
|
||||
return (
|
||||
<>
|
||||
|
|
36
ui/src/components/sidebar.tsx
vendored
36
ui/src/components/sidebar.tsx
vendored
|
@ -8,14 +8,10 @@ import {
|
|||
UserView,
|
||||
} from '../interfaces';
|
||||
import { WebSocketService, UserService } from '../services';
|
||||
import {
|
||||
mdToHtml,
|
||||
getUnixTime,
|
||||
pictshareAvatarThumbnail,
|
||||
showAvatars,
|
||||
} from '../utils';
|
||||
import { mdToHtml, getUnixTime, hostname } from '../utils';
|
||||
import { CommunityForm } from './community-form';
|
||||
import { UserListing } from './user-listing';
|
||||
import { CommunityLink } from './community-link';
|
||||
import { i18n } from '../i18next';
|
||||
|
||||
interface SidebarProps {
|
||||
|
@ -65,6 +61,15 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
|
|||
|
||||
sidebar() {
|
||||
let community = this.props.community;
|
||||
let name_: string, link: string;
|
||||
|
||||
if (community.local) {
|
||||
name_ = community.name;
|
||||
link = `/c/${community.name}`;
|
||||
} else {
|
||||
name_ = `${community.name}@${hostname(community.actor_id)}`;
|
||||
link = community.actor_id;
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
<div class="card border-secondary mb-3">
|
||||
|
@ -82,9 +87,7 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
|
|||
</small>
|
||||
)}
|
||||
</h5>
|
||||
<Link className="text-muted" to={`/c/${community.name}`}>
|
||||
/c/{community.name}
|
||||
</Link>
|
||||
<CommunityLink community={community} realLink />
|
||||
<ul class="list-inline mb-1 text-muted font-weight-bold">
|
||||
{this.canMod && (
|
||||
<>
|
||||
|
@ -111,8 +114,9 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
|
|||
}
|
||||
>
|
||||
<svg
|
||||
class={`icon icon-inline ${community.deleted &&
|
||||
'text-danger'}`}
|
||||
class={`icon icon-inline ${
|
||||
community.deleted && 'text-danger'
|
||||
}`}
|
||||
>
|
||||
<use xlinkHref="#icon-trash"></use>
|
||||
</svg>
|
||||
|
@ -209,15 +213,19 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
|
|||
user={{
|
||||
name: mod.user_name,
|
||||
avatar: mod.avatar,
|
||||
id: mod.user_id,
|
||||
local: mod.user_local,
|
||||
actor_id: mod.user_actor_id,
|
||||
}}
|
||||
/>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
{/* TODO the to= needs to be able to handle community_ids as well, since they're federated */}
|
||||
<Link
|
||||
class={`btn btn-sm btn-secondary btn-block mb-3 ${(community.deleted ||
|
||||
community.removed) &&
|
||||
'no-click'}`}
|
||||
class={`btn btn-sm btn-secondary btn-block mb-3 ${
|
||||
(community.deleted || community.removed) && 'no-click'
|
||||
}`}
|
||||
to={`/create_post?community=${community.name}`}
|
||||
>
|
||||
{i18n.t('create_a_post')}
|
||||
|
|
21
ui/src/components/user-listing.tsx
vendored
21
ui/src/components/user-listing.tsx
vendored
|
@ -1,15 +1,19 @@
|
|||
import { Component } from 'inferno';
|
||||
import { Link } from 'inferno-router';
|
||||
import { UserView } from '../interfaces';
|
||||
import { pictshareAvatarThumbnail, showAvatars } from '../utils';
|
||||
import { pictshareAvatarThumbnail, showAvatars, hostname } from '../utils';
|
||||
|
||||
interface UserOther {
|
||||
name: string;
|
||||
id?: number; // Necessary if its federated
|
||||
avatar?: string;
|
||||
local?: boolean;
|
||||
actor_id?: string;
|
||||
}
|
||||
|
||||
interface UserListingProps {
|
||||
user: UserView | UserOther;
|
||||
realLink?: boolean;
|
||||
}
|
||||
|
||||
export class UserListing extends Component<UserListingProps, any> {
|
||||
|
@ -19,8 +23,19 @@ export class UserListing extends Component<UserListingProps, any> {
|
|||
|
||||
render() {
|
||||
let user = this.props.user;
|
||||
let local = user.local == null ? true : user.local;
|
||||
let name_: string, link: string;
|
||||
|
||||
if (local) {
|
||||
name_ = user.name;
|
||||
link = `/u/${user.name}`;
|
||||
} else {
|
||||
name_ = `${user.name}@${hostname(user.actor_id)}`;
|
||||
link = !this.props.realLink ? `/user/${user.id}` : user.actor_id;
|
||||
}
|
||||
|
||||
return (
|
||||
<Link className="text-body font-weight-bold" to={`/u/${user.name}`}>
|
||||
<Link className="text-body font-weight-bold" to={link}>
|
||||
{user.avatar && showAvatars() && (
|
||||
<img
|
||||
height="32"
|
||||
|
@ -29,7 +44,7 @@ export class UserListing extends Component<UserListingProps, any> {
|
|||
class="rounded-circle mr-2"
|
||||
/>
|
||||
)}
|
||||
<span>{user.name}</span>
|
||||
<span>{name_}</span>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
|
13
ui/src/components/user.tsx
vendored
13
ui/src/components/user.tsx
vendored
|
@ -40,6 +40,7 @@ import {
|
|||
setupTippy,
|
||||
} from '../utils';
|
||||
import { PostListing } from './post-listing';
|
||||
import { UserListing } from './user-listing';
|
||||
import { SortSelect } from './sort-select';
|
||||
import { ListingTypeSelect } from './listing-type-select';
|
||||
import { CommentNodes } from './comment-nodes';
|
||||
|
@ -81,7 +82,6 @@ export class User extends Component<any, UserState> {
|
|||
user: {
|
||||
id: null,
|
||||
name: null,
|
||||
fedi_name: null,
|
||||
published: null,
|
||||
number_of_posts: null,
|
||||
post_score: null,
|
||||
|
@ -91,6 +91,8 @@ export class User extends Component<any, UserState> {
|
|||
avatar: null,
|
||||
show_avatars: null,
|
||||
send_notifications_to_email: null,
|
||||
actor_id: null,
|
||||
local: null,
|
||||
},
|
||||
user_id: null,
|
||||
username: null,
|
||||
|
@ -399,7 +401,9 @@ export class User extends Component<any, UserState> {
|
|||
<div class="card-body">
|
||||
<h5>
|
||||
<ul class="list-inline mb-0">
|
||||
<li className="list-inline-item">{user.name}</li>
|
||||
<li className="list-inline-item">
|
||||
<UserListing user={user} realLink />
|
||||
</li>
|
||||
{user.banned && (
|
||||
<li className="list-inline-item badge badge-danger">
|
||||
{i18n.t('banned')}
|
||||
|
@ -455,8 +459,9 @@ export class User extends Component<any, UserState> {
|
|||
) : (
|
||||
<>
|
||||
<a
|
||||
className={`btn btn-block btn-secondary mt-3 ${!this.state
|
||||
.user.matrix_user_id && 'disabled'}`}
|
||||
className={`btn btn-block btn-secondary mt-3 ${
|
||||
!this.state.user.matrix_user_id && 'disabled'
|
||||
}`}
|
||||
target="_blank"
|
||||
href={`https://matrix.to/#/${this.state.user.matrix_user_id}`}
|
||||
>
|
||||
|
|
2
ui/src/env.ts
vendored
2
ui/src/env.ts
vendored
|
@ -1,6 +1,6 @@
|
|||
const host = `${window.location.hostname}`;
|
||||
const port = `${
|
||||
window.location.port == '4444' ? '8536' : window.location.port
|
||||
window.location.port == '4444' ? '8540' : window.location.port
|
||||
}`;
|
||||
const endpoint = `${host}:${port}`;
|
||||
|
||||
|
|
34
ui/src/interfaces.ts
vendored
34
ui/src/interfaces.ts
vendored
|
@ -100,10 +100,13 @@ export interface User {
|
|||
|
||||
export interface UserView {
|
||||
id: number;
|
||||
actor_id: string;
|
||||
name: string;
|
||||
avatar?: string;
|
||||
email?: string;
|
||||
matrix_user_id?: string;
|
||||
bio?: string;
|
||||
local: boolean;
|
||||
published: string;
|
||||
number_of_posts: number;
|
||||
post_score: number;
|
||||
|
@ -117,15 +120,21 @@ export interface UserView {
|
|||
export interface CommunityUser {
|
||||
id: number;
|
||||
user_id: number;
|
||||
user_actor_id: string;
|
||||
user_local: boolean;
|
||||
user_name: string;
|
||||
avatar?: string;
|
||||
community_id: number;
|
||||
community_actor_id: string;
|
||||
community_local: boolean;
|
||||
community_name: string;
|
||||
published: string;
|
||||
}
|
||||
|
||||
export interface Community {
|
||||
id: number;
|
||||
actor_id: string;
|
||||
local: boolean;
|
||||
name: string;
|
||||
title: string;
|
||||
description?: string;
|
||||
|
@ -136,6 +145,9 @@ export interface Community {
|
|||
nsfw: boolean;
|
||||
published: string;
|
||||
updated?: string;
|
||||
creator_actor_id: string;
|
||||
creator_local: boolean;
|
||||
last_refreshed_at: string;
|
||||
creator_name: string;
|
||||
creator_avatar?: string;
|
||||
category_name: string;
|
||||
|
@ -161,13 +173,19 @@ export interface Post {
|
|||
embed_description?: string;
|
||||
embed_html?: string;
|
||||
thumbnail_url?: string;
|
||||
ap_id: string;
|
||||
local: boolean;
|
||||
nsfw: boolean;
|
||||
banned: boolean;
|
||||
banned_from_community: boolean;
|
||||
published: string;
|
||||
updated?: string;
|
||||
creator_actor_id: string;
|
||||
creator_local: boolean;
|
||||
creator_name: string;
|
||||
creator_avatar?: string;
|
||||
community_actor_id: string;
|
||||
community_local: boolean;
|
||||
community_name: string;
|
||||
community_removed: boolean;
|
||||
community_deleted: boolean;
|
||||
|
@ -188,6 +206,8 @@ export interface Post {
|
|||
|
||||
export interface Comment {
|
||||
id: number;
|
||||
ap_id: string;
|
||||
local: boolean;
|
||||
creator_id: number;
|
||||
post_id: number;
|
||||
parent_id?: number;
|
||||
|
@ -198,9 +218,13 @@ export interface Comment {
|
|||
published: string;
|
||||
updated?: string;
|
||||
community_id: number;
|
||||
community_actor_id: string;
|
||||
community_local: boolean;
|
||||
community_name: string;
|
||||
banned: boolean;
|
||||
banned_from_community: boolean;
|
||||
creator_actor_id: string;
|
||||
creator_local: boolean;
|
||||
creator_name: string;
|
||||
creator_avatar?: string;
|
||||
score: number;
|
||||
|
@ -213,6 +237,8 @@ export interface Comment {
|
|||
saved?: boolean;
|
||||
user_mention_id?: number; // For mention type
|
||||
recipient_id?: number;
|
||||
recipient_actor_id?: string;
|
||||
recipient_local?: boolean;
|
||||
depth?: number;
|
||||
}
|
||||
|
||||
|
@ -247,10 +273,16 @@ export interface PrivateMessage {
|
|||
read: boolean;
|
||||
published: string;
|
||||
updated?: string;
|
||||
ap_id: string;
|
||||
local: boolean;
|
||||
creator_name: string;
|
||||
creator_avatar?: string;
|
||||
creator_actor_id: string;
|
||||
creator_local: boolean;
|
||||
recipient_name: string;
|
||||
recipient_avatar?: string;
|
||||
recipient_actor_id: string;
|
||||
recipient_local: boolean;
|
||||
}
|
||||
|
||||
export enum BanType {
|
||||
|
@ -628,7 +660,7 @@ export interface CommentForm {
|
|||
post_id: number;
|
||||
parent_id?: number;
|
||||
edit_id?: number;
|
||||
creator_id: number;
|
||||
creator_id?: number;
|
||||
removed?: boolean;
|
||||
deleted?: boolean;
|
||||
reason?: string;
|
||||
|
|
42
ui/src/utils.ts
vendored
42
ui/src/utils.ts
vendored
|
@ -118,11 +118,11 @@ export const md = new markdown_it({
|
|||
typographer: true,
|
||||
})
|
||||
.use(markdown_it_container, 'spoiler', {
|
||||
validate: function(params: any) {
|
||||
validate: function (params: any) {
|
||||
return params.trim().match(/^spoiler\s+(.*)$/);
|
||||
},
|
||||
|
||||
render: function(tokens: any, idx: any) {
|
||||
render: function (tokens: any, idx: any) {
|
||||
var m = tokens[idx].info.trim().match(/^spoiler\s+(.*)$/);
|
||||
|
||||
if (tokens[idx].nesting === 1) {
|
||||
|
@ -138,7 +138,7 @@ export const md = new markdown_it({
|
|||
defs: objectFlip(emojiShortName),
|
||||
});
|
||||
|
||||
md.renderer.rules.emoji = function(token, idx) {
|
||||
md.renderer.rules.emoji = function (token, idx) {
|
||||
return twemoji.parse(token[idx].content);
|
||||
};
|
||||
|
||||
|
@ -284,7 +284,7 @@ export function debounce(
|
|||
let timeout: any;
|
||||
|
||||
// Calling debounce returns a new anonymous function
|
||||
return function() {
|
||||
return function () {
|
||||
// reference the context and args for the setTimeout function
|
||||
var context = this,
|
||||
args = arguments;
|
||||
|
@ -300,7 +300,7 @@ export function debounce(
|
|||
clearTimeout(timeout);
|
||||
|
||||
// Set the new timeout
|
||||
timeout = setTimeout(function() {
|
||||
timeout = setTimeout(function () {
|
||||
// Inside the timeout function, clear the timeout variable
|
||||
// which will let the next execution run when in 'immediate' mode
|
||||
timeout = null;
|
||||
|
@ -501,7 +501,10 @@ export function setupTribute(): Tribute {
|
|||
{
|
||||
trigger: '@',
|
||||
selectTemplate: (item: any) => {
|
||||
return `[/u/${item.original.key}](/u/${item.original.key})`;
|
||||
let link = item.original.local
|
||||
? `[@${item.original.key}](/u/${item.original.key})`
|
||||
: `[@${item.original.key}](/user/${item.original.id})`;
|
||||
return link;
|
||||
},
|
||||
values: (text: string, cb: any) => {
|
||||
userSearch(text, (users: any) => cb(users));
|
||||
|
@ -514,9 +517,12 @@ export function setupTribute(): Tribute {
|
|||
|
||||
// Communities
|
||||
{
|
||||
trigger: '#',
|
||||
trigger: '!',
|
||||
selectTemplate: (item: any) => {
|
||||
return `[/c/${item.original.key}](/c/${item.original.key})`;
|
||||
let link = item.original.local
|
||||
? `[!${item.original.key}](/c/${item.original.key})`
|
||||
: `[!${item.original.key}](/community/${item.original.id})`;
|
||||
return link;
|
||||
},
|
||||
values: (text: string, cb: any) => {
|
||||
communitySearch(text, (communities: any) => cb(communities));
|
||||
|
@ -559,7 +565,12 @@ function userSearch(text: string, cb: any) {
|
|||
if (res.op == UserOperation.Search) {
|
||||
let data = res.data as SearchResponse;
|
||||
let users = data.users.map(u => {
|
||||
return { key: u.name };
|
||||
let name_ = u.local ? u.name : `${u.name}@${hostname(u.actor_id)}`;
|
||||
return {
|
||||
key: name_,
|
||||
local: u.local,
|
||||
id: u.id,
|
||||
};
|
||||
});
|
||||
cb(users);
|
||||
this.userSub.unsubscribe();
|
||||
|
@ -590,8 +601,13 @@ function communitySearch(text: string, cb: any) {
|
|||
let res = wsJsonToRes(msg);
|
||||
if (res.op == UserOperation.Search) {
|
||||
let data = res.data as SearchResponse;
|
||||
let communities = data.communities.map(u => {
|
||||
return { key: u.name };
|
||||
let communities = data.communities.map(c => {
|
||||
let name_ = c.local ? c.name : `${c.name}@${hostname(c.actor_id)}`;
|
||||
return {
|
||||
key: name_,
|
||||
local: c.local,
|
||||
id: c.id,
|
||||
};
|
||||
});
|
||||
cb(communities);
|
||||
this.communitySub.unsubscribe();
|
||||
|
@ -841,3 +857,7 @@ export function previewLines(text: string, lines: number = 3): string {
|
|||
.slice(0, lines * 2)
|
||||
.join('\n');
|
||||
}
|
||||
|
||||
export function hostname(url: string): string {
|
||||
return new URL(url).hostname;
|
||||
}
|
||||
|
|
1892
ui/yarn.lock
vendored
1892
ui/yarn.lock
vendored
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue