Compare commits
119 commits
master
...
shared_inb
Author | SHA1 | Date | |
---|---|---|---|
07a9d84ed0 | |||
4b741c3759 | |||
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 |
90 changed files with 7781 additions and 2199 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/
|
||||
|
|
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
|
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
|
||||
```
|
||||
|
|
2454
server/Cargo.lock
generated
vendored
2454
server/Cargo.lock
generated
vendored
File diff suppressed because it is too large
Load diff
8
server/Cargo.toml
vendored
8
server/Cargo.toml
vendored
|
@ -8,8 +8,8 @@ edition = "2018"
|
|||
diesel = { version = "1.4.2", features = ["postgres","chrono", "r2d2", "64-column-tables"] }
|
||||
diesel_migrations = "1.4.0"
|
||||
dotenv = "0.15.0"
|
||||
activitystreams = "0.5.0-alpha.16"
|
||||
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,14 @@ 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"
|
||||
|
|
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));
|
|
@ -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
|
||||
|
@ -266,6 +277,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 +303,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 +320,30 @@ 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()),
|
||||
};
|
||||
|
||||
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;
|
||||
|
|
|
@ -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) {
|
||||
|
@ -328,6 +338,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,6 +350,12 @@ 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) {
|
||||
|
@ -447,23 +465,31 @@ impl Perform for Oper<FollowCommunity> {
|
|||
|
||||
let user_id = claims.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()),
|
||||
let community = Community::read(&conn, data.community_id)?;
|
||||
if community.local {
|
||||
let community_follower_form = CommunityFollowerForm {
|
||||
community_id: data.community_id,
|
||||
user_id,
|
||||
};
|
||||
|
||||
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::ignore(&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()),
|
||||
};
|
||||
// TODO: still have to implement unfollow
|
||||
let user = User_::read(&conn, user_id)?;
|
||||
user.send_follow(&community.actor_id)?;
|
||||
// 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))?;
|
||||
|
@ -680,6 +706,12 @@ impl Perform for Oper<TransferCommunity> {
|
|||
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, 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,
|
||||
|
@ -446,7 +457,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 +466,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 +484,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 +531,8 @@ impl Perform for Oper<EditPost> {
|
|||
ModStickyPost::create(&conn, &form)?;
|
||||
}
|
||||
|
||||
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 {
|
||||
|
@ -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)
|
||||
|
|
|
@ -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()),
|
||||
};
|
||||
|
@ -950,18 +914,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 +928,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()),
|
||||
};
|
||||
|
|
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(())
|
||||
}
|
136
server/src/apub/comment.rs
Normal file
136
server/src/apub/comment.rs
Normal file
|
@ -0,0 +1,136 @@
|
|||
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)
|
||||
}
|
||||
}
|
||||
|
||||
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 mut create = Create::new();
|
||||
populate_object_props(
|
||||
&mut create.object_props,
|
||||
&community.get_followers_url(),
|
||||
&self.ap_id,
|
||||
)?;
|
||||
create
|
||||
.create_props
|
||||
.set_actor_xsd_any_uri(creator.actor_id.to_owned())?
|
||||
.set_object_base_box(note)?;
|
||||
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 mut update = Update::new();
|
||||
populate_object_props(
|
||||
&mut update.object_props,
|
||||
&community.get_followers_url(),
|
||||
&self.ap_id,
|
||||
)?;
|
||||
update
|
||||
.update_props
|
||||
.set_actor_xsd_any_uri(creator.actor_id.to_owned())?
|
||||
.set_object_base_box(note)?;
|
||||
send_activity(
|
||||
&update,
|
||||
&creator.private_key.as_ref().unwrap(),
|
||||
&creator.actor_id,
|
||||
community.get_follower_inboxes(&conn)?,
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
|
@ -1,109 +1,226 @@
|
|||
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();
|
||||
|
||||
let creator = User_::read(conn, self.creator_id)?;
|
||||
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_attributed_to_xsd_any_uri(creator.actor_id)?;
|
||||
|
||||
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()))
|
||||
}
|
||||
}
|
||||
|
||||
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) -> Result<(), Error> {
|
||||
let actor_uri = follow
|
||||
.follow_props
|
||||
.get_actor_xsd_any_uri()
|
||||
.unwrap()
|
||||
.to_string();
|
||||
|
||||
let mut accept = Accept::new();
|
||||
accept
|
||||
.object_props
|
||||
.set_context_xsd_any_uri(context())?
|
||||
// TODO: needs proper id
|
||||
.set_id(
|
||||
follow
|
||||
.follow_props
|
||||
.get_actor_xsd_any_uri()
|
||||
.unwrap()
|
||||
.to_string(),
|
||||
)?;
|
||||
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);
|
||||
send_activity(
|
||||
&accept,
|
||||
&self.private_key.to_owned().unwrap(),
|
||||
&self.actor_id,
|
||||
vec![to],
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// For a given community, returns the inboxes of all followers.
|
||||
fn get_follower_inboxes(&self, conn: &PgConnection) -> Result<Vec<String>, Error> {
|
||||
debug!("got here.");
|
||||
|
||||
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(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
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 apub_id = &oprops.get_attributed_to_xsd_any_uri().unwrap().to_string();
|
||||
let creator = get_or_fetch_and_upsert_remote_user(&apub_id, conn)?;
|
||||
|
||||
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)?;
|
||||
let c = community.to_apub(&db.get().unwrap())?;
|
||||
Ok(create_apub_response(&c))
|
||||
}
|
||||
|
||||
/// 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))
|
||||
//}
|
||||
|
|
69
server/src/apub/community_inbox.rs
Normal file
69
server/src/apub/community_inbox.rs
Normal file
|
@ -0,0 +1,69 @@
|
|||
use super::*;
|
||||
|
||||
#[serde(untagged)]
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub enum CommunityAcceptedObjects {
|
||||
Follow(Follow),
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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())?;
|
||||
|
||||
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)?;
|
||||
|
||||
Ok(HttpResponse::Ok().finish())
|
||||
}
|
175
server/src/apub/fetcher.rs
Normal file
175
server/src/apub/fetcher.rs
Normal file
|
@ -0,0 +1,175 @@
|
|||
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<Page>),
|
||||
}
|
||||
|
||||
/// 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
|
||||
/// http://lemmy_alpha:8540/u/lemmy_alpha
|
||||
/// http://lemmy_alpha:8540/p/3
|
||||
pub fn search_by_apub_id(query: &str, conn: &PgConnection) -> Result<SearchResponse, Error> {
|
||||
let query_url = 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
|
||||
// TODO the less than needs to be tested
|
||||
if 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
|
||||
// TODO the less than needs to be tested
|
||||
if 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)?;
|
||||
Ok(Community::create(conn, &cf)?)
|
||||
}
|
||||
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,230 @@
|
|||
pub mod activities;
|
||||
pub mod comment;
|
||||
pub mod community;
|
||||
pub mod community_inbox;
|
||||
pub mod fetcher;
|
||||
pub mod post;
|
||||
pub mod shared_inbox;
|
||||
pub mod signatures;
|
||||
pub mod user;
|
||||
use crate::Settings;
|
||||
pub mod user_inbox;
|
||||
|
||||
use std::fmt::Display;
|
||||
use activitystreams::{
|
||||
activity::{Accept, Create, Follow, Update},
|
||||
actor::{properties::ApActorProperties, Actor, Group, Person},
|
||||
collection::UnorderedCollection,
|
||||
context,
|
||||
endpoint::EndpointProperties,
|
||||
ext::{Ext, Extensible, Extension},
|
||||
object::{properties::ObjectProperties, Note, Page},
|
||||
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::db::comment::{Comment, CommentForm};
|
||||
use crate::db::comment_view::CommentView;
|
||||
use crate::db::community::{Community, CommunityFollower, CommunityFollowerForm, CommunityForm};
|
||||
use crate::db::community_view::{CommunityFollowerView, CommunityView};
|
||||
use crate::db::post::{Post, PostForm};
|
||||
use crate::db::post_view::PostView;
|
||||
use crate::db::user::{UserForm, User_};
|
||||
use crate::db::user_view::UserView;
|
||||
use crate::db::{Crud, Followable, SearchType};
|
||||
use crate::routes::nodeinfo::{NodeInfo, NodeInfoWellKnown};
|
||||
use crate::routes::{ChatServerParam, DbPoolParam};
|
||||
use crate::websocket::{
|
||||
server::{SendComment, SendPost},
|
||||
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 activities::{populate_object_props, send_activity};
|
||||
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>;
|
||||
|
||||
#[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,
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
/// 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",
|
||||
};
|
||||
|
||||
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>;
|
||||
}
|
||||
|
||||
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>;
|
||||
}
|
||||
|
||||
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) -> Result<(), Error> {
|
||||
Err(format_err!("Follow not implemented."))
|
||||
}
|
||||
|
||||
#[allow(unused_variables)]
|
||||
fn send_accept_follow(&self, follow: &Follow) -> Result<(), Error> {
|
||||
Err(format_err!("Accept not implemented."))
|
||||
}
|
||||
|
||||
// TODO default because there is no user following yet.
|
||||
#[allow(unused_variables)]
|
||||
/// For a given community, returns the inboxes of all followers.
|
||||
fn get_follower_inboxes(&self, conn: &PgConnection) -> Result<Vec<String>, Error> {
|
||||
Ok(vec![])
|
||||
}
|
||||
|
||||
// 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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,38 +1,142 @@
|
|||
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);
|
||||
#[derive(Deserialize)]
|
||||
pub struct PostQuery {
|
||||
post_id: String,
|
||||
}
|
||||
|
||||
/// 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)?;
|
||||
Ok(create_apub_response(&post.to_apub(&db.get().unwrap())?))
|
||||
}
|
||||
|
||||
impl ToApub for Post {
|
||||
type Response = Page;
|
||||
|
||||
// Turn a Lemmy post into an ActivityPub page that can be sent out over the network.
|
||||
fn to_apub(&self, conn: &PgConnection) -> Result<Page, 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)?;
|
||||
|
||||
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();
|
||||
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())?
|
||||
// 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 {
|
||||
page.object_props.set_content_string(body.to_owned()).ok();
|
||||
oprops.set_content_xsd_string(body.to_owned())?;
|
||||
}
|
||||
|
||||
if let Some(url) = &self.url {
|
||||
page.object_props.set_url_string(url.to_owned()).ok();
|
||||
// TODO: hacky code because we get self.url == Some("")
|
||||
// https://github.com/dessalines/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())?;
|
||||
}
|
||||
|
||||
//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();
|
||||
if let Some(u) = self.updated {
|
||||
oprops.set_updated(convert_datetime(u))?;
|
||||
}
|
||||
|
||||
page
|
||||
Ok(page)
|
||||
}
|
||||
}
|
||||
|
||||
impl FromApub for PostForm {
|
||||
type ApubType = Page;
|
||||
|
||||
/// Parse an ActivityPub page received from another instance into a Lemmy post.
|
||||
fn from_apub(page: &Page, conn: &PgConnection) -> Result<PostForm, Error> {
|
||||
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, // -> Delete activity / tombstone
|
||||
locked: None, // -> commentsEnabled
|
||||
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, // -> Delete activity / tombstone
|
||||
nsfw: false, // -> sensitive
|
||||
stickied: None, // -> put it in "featured" collection of the community
|
||||
embed_title: None, // -> attachment?
|
||||
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 mut create = Create::new();
|
||||
populate_object_props(
|
||||
&mut create.object_props,
|
||||
&community.get_followers_url(),
|
||||
&self.ap_id,
|
||||
)?;
|
||||
create
|
||||
.create_props
|
||||
.set_actor_xsd_any_uri(creator.actor_id.to_owned())?
|
||||
.set_object_base_box(page)?;
|
||||
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 mut update = Update::new();
|
||||
populate_object_props(
|
||||
&mut update.object_props,
|
||||
&community.get_followers_url(),
|
||||
&self.ap_id,
|
||||
)?;
|
||||
update
|
||||
.update_props
|
||||
.set_actor_xsd_any_uri(creator.actor_id.to_owned())?
|
||||
.set_object_base_box(page)?;
|
||||
send_activity(
|
||||
&update,
|
||||
&creator.private_key.as_ref().unwrap(),
|
||||
&creator.actor_id,
|
||||
community.get_follower_inboxes(&conn)?,
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
222
server/src/apub/shared_inbox.rs
Normal file
222
server/src/apub/shared_inbox.rs
Normal file
|
@ -0,0 +1,222 @@
|
|||
use super::*;
|
||||
|
||||
#[serde(untagged)]
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub enum SharedAcceptedObjects {
|
||||
Create(Create),
|
||||
Update(Update),
|
||||
}
|
||||
|
||||
impl SharedAcceptedObjects {
|
||||
fn object(&self) -> Option<&BaseBox> {
|
||||
match self {
|
||||
SharedAcceptedObjects::Create(c) => c.create_props.get_object_base_box(),
|
||||
SharedAcceptedObjects::Update(u) => u.update_props.get_object_base_box(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Handler for all incoming activities to user inboxes.
|
||||
pub async fn shared_inbox(
|
||||
request: HttpRequest,
|
||||
input: web::Json<SharedAcceptedObjects>,
|
||||
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 activity = input.into_inner();
|
||||
let conn = &db.get().unwrap();
|
||||
|
||||
let json = serde_json::to_string(&activity)?;
|
||||
debug!("Shared inbox received activity: {}", json);
|
||||
|
||||
let object = activity.object().cloned().unwrap();
|
||||
|
||||
match (activity, object.kind()) {
|
||||
(SharedAcceptedObjects::Create(c), Some("Note")) => {
|
||||
receive_create_comment(&c, &request, &conn, chat_server)
|
||||
}
|
||||
(SharedAcceptedObjects::Create(c), Some("Page")) => {
|
||||
receive_create_post(&c, &request, &conn, chat_server)
|
||||
}
|
||||
(SharedAcceptedObjects::Update(u), Some("Note")) => {
|
||||
receive_update_comment(&u, &request, &conn, chat_server)
|
||||
}
|
||||
(SharedAcceptedObjects::Update(u), Some("Page")) => {
|
||||
receive_update_post(&u, &request, &conn, chat_server)
|
||||
}
|
||||
_ => Err(format_err!("Unknown incoming activity type.")),
|
||||
}
|
||||
}
|
||||
|
||||
fn receive_create_post(
|
||||
create: &Create,
|
||||
request: &HttpRequest,
|
||||
conn: &PgConnection,
|
||||
chat_server: ChatServerParam,
|
||||
) -> Result<HttpResponse, Error> {
|
||||
let page = create
|
||||
.create_props
|
||||
.get_object_base_box()
|
||||
.to_owned()
|
||||
.unwrap()
|
||||
.to_owned()
|
||||
.to_concrete::<Page>()?;
|
||||
|
||||
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())?;
|
||||
|
||||
let post = PostForm::from_apub(&page, &conn)?;
|
||||
let inserted_post = Post::create(conn, &post)?;
|
||||
|
||||
// Refetch the view
|
||||
let post_view = PostView::read(&conn, inserted_post.id, None)?;
|
||||
|
||||
let res = PostResponse { post: post_view };
|
||||
|
||||
chat_server.do_send(SendPost {
|
||||
op: UserOperation::CreatePost,
|
||||
post: res,
|
||||
my_id: None,
|
||||
});
|
||||
|
||||
Ok(HttpResponse::Ok().finish())
|
||||
}
|
||||
|
||||
fn receive_create_comment(
|
||||
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()
|
||||
.to_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())?;
|
||||
|
||||
let comment = CommentForm::from_apub(¬e, &conn)?;
|
||||
let inserted_comment = Comment::create(conn, &comment)?;
|
||||
|
||||
// Refetch the view
|
||||
let comment_view = CommentView::read(&conn, inserted_comment.id, None)?;
|
||||
|
||||
// TODO get those recipient actor ids from somewhere
|
||||
let recipient_ids = vec![];
|
||||
let res = CommentResponse {
|
||||
comment: comment_view,
|
||||
recipient_ids,
|
||||
};
|
||||
|
||||
chat_server.do_send(SendComment {
|
||||
op: UserOperation::CreateComment,
|
||||
comment: res,
|
||||
my_id: None,
|
||||
});
|
||||
|
||||
Ok(HttpResponse::Ok().finish())
|
||||
}
|
||||
|
||||
fn receive_update_post(
|
||||
update: &Update,
|
||||
request: &HttpRequest,
|
||||
conn: &PgConnection,
|
||||
chat_server: ChatServerParam,
|
||||
) -> Result<HttpResponse, Error> {
|
||||
let page = update
|
||||
.update_props
|
||||
.get_object_base_box()
|
||||
.to_owned()
|
||||
.unwrap()
|
||||
.to_owned()
|
||||
.to_concrete::<Page>()?;
|
||||
|
||||
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())?;
|
||||
|
||||
let post = PostForm::from_apub(&page, conn)?;
|
||||
let post_id = Post::read_from_apub_id(conn, &post.ap_id)?.id;
|
||||
Post::update(conn, post_id, &post)?;
|
||||
|
||||
// Refetch the view
|
||||
let post_view = PostView::read(&conn, post_id, None)?;
|
||||
|
||||
let res = PostResponse { post: post_view };
|
||||
|
||||
chat_server.do_send(SendPost {
|
||||
op: UserOperation::EditPost,
|
||||
post: res,
|
||||
my_id: None,
|
||||
});
|
||||
|
||||
Ok(HttpResponse::Ok().finish())
|
||||
}
|
||||
|
||||
fn receive_update_comment(
|
||||
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()
|
||||
.to_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())?;
|
||||
|
||||
let comment = CommentForm::from_apub(¬e, &conn)?;
|
||||
let comment_id = Comment::read_from_apub_id(conn, &comment.ap_id)?.id;
|
||||
Comment::update(conn, comment_id, &comment)?;
|
||||
|
||||
// Refetch the view
|
||||
let comment_view = CommentView::read(&conn, comment_id, None)?;
|
||||
|
||||
// TODO get those recipient actor ids from somewhere
|
||||
let recipient_ids = vec![];
|
||||
let res = CommentResponse {
|
||||
comment: comment_view,
|
||||
recipient_ids,
|
||||
};
|
||||
|
||||
chat_server.do_send(SendComment {
|
||||
op: UserOperation::EditComment,
|
||||
comment: res,
|
||||
my_id: None,
|
||||
});
|
||||
|
||||
Ok(HttpResponse::Ok().finish())
|
||||
}
|
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,126 @@
|
|||
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()))
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
// TODO might be able to move this to a default trait fn
|
||||
/// As a given local user, send out a follow request to a remote community.
|
||||
fn send_follow(&self, follow_actor_id: &str) -> Result<(), Error> {
|
||||
let mut follow = Follow::new();
|
||||
follow
|
||||
.object_props
|
||||
.set_context_xsd_any_uri(context())?
|
||||
// TODO: needs proper id
|
||||
.set_id(self.actor_id.to_owned())?;
|
||||
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);
|
||||
send_activity(
|
||||
&follow,
|
||||
&self.private_key.as_ref().unwrap(),
|
||||
&follow_actor_id,
|
||||
vec![to],
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
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))
|
||||
}
|
||||
|
|
58
server/src/apub/user_inbox.rs
Normal file
58
server/src/apub/user_inbox.rs
Normal file
|
@ -0,0 +1,58 @@
|
|||
use super::*;
|
||||
|
||||
#[serde(untagged)]
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub enum UserAcceptedObjects {
|
||||
Accept(Accept),
|
||||
}
|
||||
|
||||
/// 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) => handle_accept(&a, &request, &username, &conn),
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle accepted follows.
|
||||
fn handle_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)?;
|
||||
|
||||
// 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())
|
||||
}
|
147
server/src/db/code_migrations.rs
Normal file
147
server/src/db/code_migrations.rs
Normal file
|
@ -0,0 +1,147 @@
|
|||
// This is for db migrations that require code
|
||||
use super::comment::Comment;
|
||||
use super::community::{Community, CommunityForm};
|
||||
use super::post::Post;
|
||||
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)?;
|
||||
|
||||
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(())
|
||||
}
|
|
@ -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 (
|
||||
|
@ -23,6 +25,8 @@ pub struct Comment {
|
|||
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,14 @@ 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)]
|
||||
#[derive(Insertable, AsChangeset, Clone, Serialize, Deserialize, Debug)]
|
||||
#[table_name = "community"]
|
||||
pub struct CommunityForm {
|
||||
pub name: String,
|
||||
|
@ -26,9 +31,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 +73,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)
|
||||
}
|
||||
|
@ -216,7 +239,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 +254,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 +274,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 +296,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 {
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
||||
|
|
|
@ -5,6 +5,7 @@ use diesel::*;
|
|||
use serde::{Deserialize, Serialize};
|
||||
|
||||
pub mod category;
|
||||
pub mod code_migrations;
|
||||
pub mod comment;
|
||||
pub mod comment_view;
|
||||
pub mod community;
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -65,7 +65,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 +80,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,6 +107,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();
|
||||
|
|
|
@ -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?,
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -27,7 +27,7 @@ pub fn config(cfg: &mut web::ServiceConfig) {
|
|||
|
||||
async fn get_all_feed(
|
||||
info: web::Query<Params>,
|
||||
db: web::Data<Pool<ConnectionManager<PgConnection>>>,
|
||||
db: DbPoolParam,
|
||||
) -> Result<HttpResponse, Error> {
|
||||
let res = web::block(move || {
|
||||
let conn = db.get()?;
|
||||
|
@ -144,7 +144,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,18 +6,22 @@ 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>>>,
|
||||
db: DbPoolParam,
|
||||
) -> Result<HttpResponse, Error> {
|
||||
let res = web::block(move || {
|
||||
let conn = db.get()?;
|
||||
|
@ -25,7 +29,7 @@ async fn node_info(
|
|||
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 +57,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,316 +1,342 @@
|
|||
table! {
|
||||
category (id) {
|
||||
id -> Int4,
|
||||
name -> Varchar,
|
||||
}
|
||||
activity (id) {
|
||||
id -> Int4,
|
||||
user_id -> Int4,
|
||||
data -> Jsonb,
|
||||
local -> Bool,
|
||||
published -> Timestamp,
|
||||
updated -> Nullable<Timestamp>,
|
||||
}
|
||||
}
|
||||
|
||||
table! {
|
||||
comment (id) {
|
||||
id -> Int4,
|
||||
creator_id -> Int4,
|
||||
post_id -> Int4,
|
||||
parent_id -> Nullable<Int4>,
|
||||
content -> Text,
|
||||
removed -> Bool,
|
||||
read -> Bool,
|
||||
published -> Timestamp,
|
||||
updated -> Nullable<Timestamp>,
|
||||
deleted -> Bool,
|
||||
}
|
||||
category (id) {
|
||||
id -> Int4,
|
||||
name -> Varchar,
|
||||
}
|
||||
}
|
||||
|
||||
table! {
|
||||
comment_like (id) {
|
||||
id -> Int4,
|
||||
user_id -> Int4,
|
||||
comment_id -> Int4,
|
||||
post_id -> Int4,
|
||||
score -> Int2,
|
||||
published -> Timestamp,
|
||||
}
|
||||
comment (id) {
|
||||
id -> Int4,
|
||||
creator_id -> Int4,
|
||||
post_id -> Int4,
|
||||
parent_id -> Nullable<Int4>,
|
||||
content -> Text,
|
||||
removed -> Bool,
|
||||
read -> Bool,
|
||||
published -> Timestamp,
|
||||
updated -> Nullable<Timestamp>,
|
||||
deleted -> Bool,
|
||||
ap_id -> Varchar,
|
||||
local -> Bool,
|
||||
}
|
||||
}
|
||||
|
||||
table! {
|
||||
comment_saved (id) {
|
||||
id -> Int4,
|
||||
comment_id -> Int4,
|
||||
user_id -> Int4,
|
||||
published -> Timestamp,
|
||||
}
|
||||
comment_like (id) {
|
||||
id -> Int4,
|
||||
user_id -> Int4,
|
||||
comment_id -> Int4,
|
||||
post_id -> Int4,
|
||||
score -> Int2,
|
||||
published -> Timestamp,
|
||||
}
|
||||
}
|
||||
|
||||
table! {
|
||||
community (id) {
|
||||
id -> Int4,
|
||||
name -> Varchar,
|
||||
title -> Varchar,
|
||||
description -> Nullable<Text>,
|
||||
category_id -> Int4,
|
||||
creator_id -> Int4,
|
||||
removed -> Bool,
|
||||
published -> Timestamp,
|
||||
updated -> Nullable<Timestamp>,
|
||||
deleted -> Bool,
|
||||
nsfw -> Bool,
|
||||
}
|
||||
comment_saved (id) {
|
||||
id -> Int4,
|
||||
comment_id -> Int4,
|
||||
user_id -> Int4,
|
||||
published -> Timestamp,
|
||||
}
|
||||
}
|
||||
|
||||
table! {
|
||||
community_follower (id) {
|
||||
id -> Int4,
|
||||
community_id -> Int4,
|
||||
user_id -> Int4,
|
||||
published -> Timestamp,
|
||||
}
|
||||
community (id) {
|
||||
id -> Int4,
|
||||
name -> Varchar,
|
||||
title -> Varchar,
|
||||
description -> Nullable<Text>,
|
||||
category_id -> Int4,
|
||||
creator_id -> Int4,
|
||||
removed -> Bool,
|
||||
published -> Timestamp,
|
||||
updated -> Nullable<Timestamp>,
|
||||
deleted -> Bool,
|
||||
nsfw -> Bool,
|
||||
actor_id -> Varchar,
|
||||
local -> Bool,
|
||||
private_key -> Nullable<Text>,
|
||||
public_key -> Nullable<Text>,
|
||||
last_refreshed_at -> Timestamp,
|
||||
}
|
||||
}
|
||||
|
||||
table! {
|
||||
community_moderator (id) {
|
||||
id -> Int4,
|
||||
community_id -> Int4,
|
||||
user_id -> Int4,
|
||||
published -> Timestamp,
|
||||
}
|
||||
community_follower (id) {
|
||||
id -> Int4,
|
||||
community_id -> Int4,
|
||||
user_id -> Int4,
|
||||
published -> Timestamp,
|
||||
}
|
||||
}
|
||||
|
||||
table! {
|
||||
community_user_ban (id) {
|
||||
id -> Int4,
|
||||
community_id -> Int4,
|
||||
user_id -> Int4,
|
||||
published -> Timestamp,
|
||||
}
|
||||
community_moderator (id) {
|
||||
id -> Int4,
|
||||
community_id -> Int4,
|
||||
user_id -> Int4,
|
||||
published -> Timestamp,
|
||||
}
|
||||
}
|
||||
|
||||
table! {
|
||||
mod_add (id) {
|
||||
id -> Int4,
|
||||
mod_user_id -> Int4,
|
||||
other_user_id -> Int4,
|
||||
removed -> Nullable<Bool>,
|
||||
when_ -> Timestamp,
|
||||
}
|
||||
community_user_ban (id) {
|
||||
id -> Int4,
|
||||
community_id -> Int4,
|
||||
user_id -> Int4,
|
||||
published -> Timestamp,
|
||||
}
|
||||
}
|
||||
|
||||
table! {
|
||||
mod_add_community (id) {
|
||||
id -> Int4,
|
||||
mod_user_id -> Int4,
|
||||
other_user_id -> Int4,
|
||||
community_id -> Int4,
|
||||
removed -> Nullable<Bool>,
|
||||
when_ -> Timestamp,
|
||||
}
|
||||
mod_add (id) {
|
||||
id -> Int4,
|
||||
mod_user_id -> Int4,
|
||||
other_user_id -> Int4,
|
||||
removed -> Nullable<Bool>,
|
||||
when_ -> Timestamp,
|
||||
}
|
||||
}
|
||||
|
||||
table! {
|
||||
mod_ban (id) {
|
||||
id -> Int4,
|
||||
mod_user_id -> Int4,
|
||||
other_user_id -> Int4,
|
||||
reason -> Nullable<Text>,
|
||||
banned -> Nullable<Bool>,
|
||||
expires -> Nullable<Timestamp>,
|
||||
when_ -> Timestamp,
|
||||
}
|
||||
mod_add_community (id) {
|
||||
id -> Int4,
|
||||
mod_user_id -> Int4,
|
||||
other_user_id -> Int4,
|
||||
community_id -> Int4,
|
||||
removed -> Nullable<Bool>,
|
||||
when_ -> Timestamp,
|
||||
}
|
||||
}
|
||||
|
||||
table! {
|
||||
mod_ban_from_community (id) {
|
||||
id -> Int4,
|
||||
mod_user_id -> Int4,
|
||||
other_user_id -> Int4,
|
||||
community_id -> Int4,
|
||||
reason -> Nullable<Text>,
|
||||
banned -> Nullable<Bool>,
|
||||
expires -> Nullable<Timestamp>,
|
||||
when_ -> Timestamp,
|
||||
}
|
||||
mod_ban (id) {
|
||||
id -> Int4,
|
||||
mod_user_id -> Int4,
|
||||
other_user_id -> Int4,
|
||||
reason -> Nullable<Text>,
|
||||
banned -> Nullable<Bool>,
|
||||
expires -> Nullable<Timestamp>,
|
||||
when_ -> Timestamp,
|
||||
}
|
||||
}
|
||||
|
||||
table! {
|
||||
mod_lock_post (id) {
|
||||
id -> Int4,
|
||||
mod_user_id -> Int4,
|
||||
post_id -> Int4,
|
||||
locked -> Nullable<Bool>,
|
||||
when_ -> Timestamp,
|
||||
}
|
||||
mod_ban_from_community (id) {
|
||||
id -> Int4,
|
||||
mod_user_id -> Int4,
|
||||
other_user_id -> Int4,
|
||||
community_id -> Int4,
|
||||
reason -> Nullable<Text>,
|
||||
banned -> Nullable<Bool>,
|
||||
expires -> Nullable<Timestamp>,
|
||||
when_ -> Timestamp,
|
||||
}
|
||||
}
|
||||
|
||||
table! {
|
||||
mod_remove_comment (id) {
|
||||
id -> Int4,
|
||||
mod_user_id -> Int4,
|
||||
comment_id -> Int4,
|
||||
reason -> Nullable<Text>,
|
||||
removed -> Nullable<Bool>,
|
||||
when_ -> Timestamp,
|
||||
}
|
||||
mod_lock_post (id) {
|
||||
id -> Int4,
|
||||
mod_user_id -> Int4,
|
||||
post_id -> Int4,
|
||||
locked -> Nullable<Bool>,
|
||||
when_ -> Timestamp,
|
||||
}
|
||||
}
|
||||
|
||||
table! {
|
||||
mod_remove_community (id) {
|
||||
id -> Int4,
|
||||
mod_user_id -> Int4,
|
||||
community_id -> Int4,
|
||||
reason -> Nullable<Text>,
|
||||
removed -> Nullable<Bool>,
|
||||
expires -> Nullable<Timestamp>,
|
||||
when_ -> Timestamp,
|
||||
}
|
||||
mod_remove_comment (id) {
|
||||
id -> Int4,
|
||||
mod_user_id -> Int4,
|
||||
comment_id -> Int4,
|
||||
reason -> Nullable<Text>,
|
||||
removed -> Nullable<Bool>,
|
||||
when_ -> Timestamp,
|
||||
}
|
||||
}
|
||||
|
||||
table! {
|
||||
mod_remove_post (id) {
|
||||
id -> Int4,
|
||||
mod_user_id -> Int4,
|
||||
post_id -> Int4,
|
||||
reason -> Nullable<Text>,
|
||||
removed -> Nullable<Bool>,
|
||||
when_ -> Timestamp,
|
||||
}
|
||||
mod_remove_community (id) {
|
||||
id -> Int4,
|
||||
mod_user_id -> Int4,
|
||||
community_id -> Int4,
|
||||
reason -> Nullable<Text>,
|
||||
removed -> Nullable<Bool>,
|
||||
expires -> Nullable<Timestamp>,
|
||||
when_ -> Timestamp,
|
||||
}
|
||||
}
|
||||
|
||||
table! {
|
||||
mod_sticky_post (id) {
|
||||
id -> Int4,
|
||||
mod_user_id -> Int4,
|
||||
post_id -> Int4,
|
||||
stickied -> Nullable<Bool>,
|
||||
when_ -> Timestamp,
|
||||
}
|
||||
mod_remove_post (id) {
|
||||
id -> Int4,
|
||||
mod_user_id -> Int4,
|
||||
post_id -> Int4,
|
||||
reason -> Nullable<Text>,
|
||||
removed -> Nullable<Bool>,
|
||||
when_ -> Timestamp,
|
||||
}
|
||||
}
|
||||
|
||||
table! {
|
||||
password_reset_request (id) {
|
||||
id -> Int4,
|
||||
user_id -> Int4,
|
||||
token_encrypted -> Text,
|
||||
published -> Timestamp,
|
||||
}
|
||||
mod_sticky_post (id) {
|
||||
id -> Int4,
|
||||
mod_user_id -> Int4,
|
||||
post_id -> Int4,
|
||||
stickied -> Nullable<Bool>,
|
||||
when_ -> Timestamp,
|
||||
}
|
||||
}
|
||||
|
||||
table! {
|
||||
post (id) {
|
||||
id -> Int4,
|
||||
name -> Varchar,
|
||||
url -> Nullable<Text>,
|
||||
body -> Nullable<Text>,
|
||||
creator_id -> Int4,
|
||||
community_id -> Int4,
|
||||
removed -> Bool,
|
||||
locked -> Bool,
|
||||
published -> Timestamp,
|
||||
updated -> Nullable<Timestamp>,
|
||||
deleted -> Bool,
|
||||
nsfw -> Bool,
|
||||
stickied -> Bool,
|
||||
embed_title -> Nullable<Text>,
|
||||
embed_description -> Nullable<Text>,
|
||||
embed_html -> Nullable<Text>,
|
||||
thumbnail_url -> Nullable<Text>,
|
||||
}
|
||||
password_reset_request (id) {
|
||||
id -> Int4,
|
||||
user_id -> Int4,
|
||||
token_encrypted -> Text,
|
||||
published -> Timestamp,
|
||||
}
|
||||
}
|
||||
|
||||
table! {
|
||||
post_like (id) {
|
||||
id -> Int4,
|
||||
post_id -> Int4,
|
||||
user_id -> Int4,
|
||||
score -> Int2,
|
||||
published -> Timestamp,
|
||||
}
|
||||
post (id) {
|
||||
id -> Int4,
|
||||
name -> Varchar,
|
||||
url -> Nullable<Text>,
|
||||
body -> Nullable<Text>,
|
||||
creator_id -> Int4,
|
||||
community_id -> Int4,
|
||||
removed -> Bool,
|
||||
locked -> Bool,
|
||||
published -> Timestamp,
|
||||
updated -> Nullable<Timestamp>,
|
||||
deleted -> Bool,
|
||||
nsfw -> Bool,
|
||||
stickied -> Bool,
|
||||
embed_title -> Nullable<Text>,
|
||||
embed_description -> Nullable<Text>,
|
||||
embed_html -> Nullable<Text>,
|
||||
thumbnail_url -> Nullable<Text>,
|
||||
ap_id -> Varchar,
|
||||
local -> Bool,
|
||||
}
|
||||
}
|
||||
|
||||
table! {
|
||||
post_read (id) {
|
||||
id -> Int4,
|
||||
post_id -> Int4,
|
||||
user_id -> Int4,
|
||||
published -> Timestamp,
|
||||
}
|
||||
post_like (id) {
|
||||
id -> Int4,
|
||||
post_id -> Int4,
|
||||
user_id -> Int4,
|
||||
score -> Int2,
|
||||
published -> Timestamp,
|
||||
}
|
||||
}
|
||||
|
||||
table! {
|
||||
post_saved (id) {
|
||||
id -> Int4,
|
||||
post_id -> Int4,
|
||||
user_id -> Int4,
|
||||
published -> Timestamp,
|
||||
}
|
||||
post_read (id) {
|
||||
id -> Int4,
|
||||
post_id -> Int4,
|
||||
user_id -> Int4,
|
||||
published -> Timestamp,
|
||||
}
|
||||
}
|
||||
|
||||
table! {
|
||||
private_message (id) {
|
||||
id -> Int4,
|
||||
creator_id -> Int4,
|
||||
recipient_id -> Int4,
|
||||
content -> Text,
|
||||
deleted -> Bool,
|
||||
read -> Bool,
|
||||
published -> Timestamp,
|
||||
updated -> Nullable<Timestamp>,
|
||||
}
|
||||
post_saved (id) {
|
||||
id -> Int4,
|
||||
post_id -> Int4,
|
||||
user_id -> Int4,
|
||||
published -> Timestamp,
|
||||
}
|
||||
}
|
||||
|
||||
table! {
|
||||
site (id) {
|
||||
id -> Int4,
|
||||
name -> Varchar,
|
||||
description -> Nullable<Text>,
|
||||
creator_id -> Int4,
|
||||
published -> Timestamp,
|
||||
updated -> Nullable<Timestamp>,
|
||||
enable_downvotes -> Bool,
|
||||
open_registration -> Bool,
|
||||
enable_nsfw -> Bool,
|
||||
}
|
||||
private_message (id) {
|
||||
id -> Int4,
|
||||
creator_id -> Int4,
|
||||
recipient_id -> Int4,
|
||||
content -> Text,
|
||||
deleted -> Bool,
|
||||
read -> Bool,
|
||||
published -> Timestamp,
|
||||
updated -> Nullable<Timestamp>,
|
||||
}
|
||||
}
|
||||
|
||||
table! {
|
||||
user_ (id) {
|
||||
id -> Int4,
|
||||
name -> Varchar,
|
||||
fedi_name -> Varchar,
|
||||
preferred_username -> Nullable<Varchar>,
|
||||
password_encrypted -> Text,
|
||||
email -> Nullable<Text>,
|
||||
avatar -> Nullable<Text>,
|
||||
admin -> Bool,
|
||||
banned -> Bool,
|
||||
published -> Timestamp,
|
||||
updated -> Nullable<Timestamp>,
|
||||
show_nsfw -> Bool,
|
||||
theme -> Varchar,
|
||||
default_sort_type -> Int2,
|
||||
default_listing_type -> Int2,
|
||||
lang -> Varchar,
|
||||
show_avatars -> Bool,
|
||||
send_notifications_to_email -> Bool,
|
||||
matrix_user_id -> Nullable<Text>,
|
||||
}
|
||||
site (id) {
|
||||
id -> Int4,
|
||||
name -> Varchar,
|
||||
description -> Nullable<Text>,
|
||||
creator_id -> Int4,
|
||||
published -> Timestamp,
|
||||
updated -> Nullable<Timestamp>,
|
||||
enable_downvotes -> Bool,
|
||||
open_registration -> Bool,
|
||||
enable_nsfw -> Bool,
|
||||
}
|
||||
}
|
||||
|
||||
table! {
|
||||
user_ban (id) {
|
||||
id -> Int4,
|
||||
user_id -> Int4,
|
||||
published -> Timestamp,
|
||||
}
|
||||
user_ (id) {
|
||||
id -> Int4,
|
||||
name -> Varchar,
|
||||
preferred_username -> Nullable<Varchar>,
|
||||
password_encrypted -> Text,
|
||||
email -> Nullable<Text>,
|
||||
avatar -> Nullable<Text>,
|
||||
admin -> Bool,
|
||||
banned -> Bool,
|
||||
published -> Timestamp,
|
||||
updated -> Nullable<Timestamp>,
|
||||
show_nsfw -> Bool,
|
||||
theme -> Varchar,
|
||||
default_sort_type -> Int2,
|
||||
default_listing_type -> Int2,
|
||||
lang -> Varchar,
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
table! {
|
||||
user_mention (id) {
|
||||
id -> Int4,
|
||||
recipient_id -> Int4,
|
||||
comment_id -> Int4,
|
||||
read -> Bool,
|
||||
published -> Timestamp,
|
||||
}
|
||||
user_ban (id) {
|
||||
id -> Int4,
|
||||
user_id -> Int4,
|
||||
published -> Timestamp,
|
||||
}
|
||||
}
|
||||
|
||||
table! {
|
||||
user_mention (id) {
|
||||
id -> Int4,
|
||||
recipient_id -> Int4,
|
||||
comment_id -> Int4,
|
||||
read -> Bool,
|
||||
published -> Timestamp,
|
||||
}
|
||||
}
|
||||
|
||||
joinable!(activity -> user_ (user_id));
|
||||
joinable!(comment -> post (post_id));
|
||||
joinable!(comment -> user_ (creator_id));
|
||||
joinable!(comment_like -> comment (comment_id));
|
||||
|
@ -353,6 +379,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",
|
||||
|
|
325
ui/src/api_tests/api.spec.ts
vendored
Normal file
325
ui/src/api_tests/api.spec.ts
vendored
Normal file
|
@ -0,0 +1,325 @@
|
|||
import fetch from 'node-fetch';
|
||||
|
||||
import {
|
||||
LoginForm,
|
||||
LoginResponse,
|
||||
PostForm,
|
||||
PostResponse,
|
||||
SearchResponse,
|
||||
FollowCommunityForm,
|
||||
CommunityResponse,
|
||||
GetFollowedCommunitiesResponse,
|
||||
GetPostForm,
|
||||
GetPostResponse,
|
||||
CommentForm,
|
||||
CommentResponse,
|
||||
} from '../interfaces';
|
||||
|
||||
let lemmyAlphaUrl = 'http://localhost:8540';
|
||||
let lemmyBetaUrl = 'http://localhost:8550';
|
||||
let lemmyAlphaApiUrl = `${lemmyAlphaUrl}/api/v1`;
|
||||
let lemmyBetaApiUrl = `${lemmyBetaUrl}/api/v1`;
|
||||
let lemmyAlphaAuth: string;
|
||||
let lemmyBetaAuth: string;
|
||||
|
||||
// Workaround for tests being run before beforeAll() is finished
|
||||
// https://github.com/facebook/jest/issues/9527#issuecomment-592406108
|
||||
describe('main', () => {
|
||||
beforeAll(async () => {
|
||||
console.log('Logging in as lemmy_alpha');
|
||||
let form: LoginForm = {
|
||||
username_or_email: 'lemmy_alpha',
|
||||
password: 'lemmy',
|
||||
};
|
||||
|
||||
let res: LoginResponse = await fetch(`${lemmyAlphaApiUrl}/user/login`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: wrapper(form),
|
||||
}).then(d => d.json());
|
||||
|
||||
lemmyAlphaAuth = res.jwt;
|
||||
|
||||
console.log('Logging in as lemmy_beta');
|
||||
let formB = {
|
||||
username_or_email: 'lemmy_beta',
|
||||
password: 'lemmy',
|
||||
};
|
||||
|
||||
let resB: LoginResponse = await fetch(`${lemmyBetaApiUrl}/user/login`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: wrapper(formB),
|
||||
}).then(d => d.json());
|
||||
|
||||
lemmyBetaAuth = resB.jwt;
|
||||
});
|
||||
|
||||
describe('beta_fetch', () => {
|
||||
test('Create test post on alpha and fetch it on beta', async () => {
|
||||
let name = 'A jest test post';
|
||||
let postForm: PostForm = {
|
||||
name,
|
||||
auth: lemmyAlphaAuth,
|
||||
community_id: 2,
|
||||
creator_id: 2,
|
||||
nsfw: false,
|
||||
};
|
||||
|
||||
let createResponse: PostResponse = await fetch(
|
||||
`${lemmyAlphaApiUrl}/post`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: wrapper(postForm),
|
||||
}
|
||||
).then(d => d.json());
|
||||
expect(createResponse.post.name).toBe(name);
|
||||
|
||||
let searchUrl = `${lemmyBetaApiUrl}/search?q=${createResponse.post.ap_id}&type_=All&sort=TopAll`;
|
||||
let searchResponse: SearchResponse = await fetch(searchUrl, {
|
||||
method: 'GET',
|
||||
}).then(d => d.json());
|
||||
|
||||
// TODO: check more fields
|
||||
expect(searchResponse.posts[0].name).toBe(name);
|
||||
});
|
||||
});
|
||||
|
||||
describe('follow_accept', () => {
|
||||
test('/u/lemmy_alpha follows and accepts lemmy_beta/c/main', async () => {
|
||||
// Make sure lemmy_beta/c/main is cached on lemmy_alpha
|
||||
let searchUrl = `${lemmyAlphaApiUrl}/search?q=http://lemmy_beta:8550/c/main&type_=All&sort=TopAll`;
|
||||
let searchResponse: SearchResponse = await fetch(searchUrl, {
|
||||
method: 'GET',
|
||||
}).then(d => d.json());
|
||||
|
||||
expect(searchResponse.communities[0].name).toBe('main');
|
||||
|
||||
let followForm: FollowCommunityForm = {
|
||||
community_id: searchResponse.communities[0].id,
|
||||
follow: true,
|
||||
auth: lemmyAlphaAuth,
|
||||
};
|
||||
|
||||
let followRes: CommunityResponse = await fetch(
|
||||
`${lemmyAlphaApiUrl}/community/follow`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: wrapper(followForm),
|
||||
}
|
||||
).then(d => d.json());
|
||||
|
||||
// Make sure the follow response went through
|
||||
expect(followRes.community.local).toBe(false);
|
||||
expect(followRes.community.name).toBe('main');
|
||||
|
||||
// Check that you are subscribed to it locally
|
||||
let followedCommunitiesUrl = `${lemmyAlphaApiUrl}/user/followed_communities?&auth=${lemmyAlphaAuth}`;
|
||||
let followedCommunitiesRes: GetFollowedCommunitiesResponse = await fetch(
|
||||
followedCommunitiesUrl,
|
||||
{
|
||||
method: 'GET',
|
||||
}
|
||||
).then(d => d.json());
|
||||
|
||||
expect(followedCommunitiesRes.communities[1].community_local).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('create test post', () => {
|
||||
test('/u/lemmy_alpha creates a post on /c/lemmy_beta/main, its on both instances', async () => {
|
||||
let name = 'A jest test federated post';
|
||||
let postForm: PostForm = {
|
||||
name,
|
||||
auth: lemmyAlphaAuth,
|
||||
community_id: 3,
|
||||
creator_id: 2,
|
||||
nsfw: false,
|
||||
};
|
||||
|
||||
let createResponse: PostResponse = await fetch(
|
||||
`${lemmyAlphaApiUrl}/post`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: wrapper(postForm),
|
||||
}
|
||||
).then(d => d.json());
|
||||
|
||||
expect(createResponse.post.name).toBe(name);
|
||||
expect(createResponse.post.community_local).toBe(false);
|
||||
expect(createResponse.post.creator_local).toBe(true);
|
||||
|
||||
let getPostUrl = `${lemmyBetaApiUrl}/post?id=2`;
|
||||
let getPostRes: GetPostResponse = await fetch(getPostUrl, {
|
||||
method: 'GET',
|
||||
}).then(d => d.json());
|
||||
|
||||
expect(getPostRes.post.name).toBe(name);
|
||||
expect(getPostRes.post.community_local).toBe(true);
|
||||
expect(getPostRes.post.creator_local).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('update test post', () => {
|
||||
test('/u/lemmy_alpha updates a post on /c/lemmy_beta/main, the update is on both', async () => {
|
||||
let name = 'A jest test federated post, updated';
|
||||
let postForm: PostForm = {
|
||||
name,
|
||||
edit_id: 2,
|
||||
auth: lemmyAlphaAuth,
|
||||
community_id: 3,
|
||||
creator_id: 2,
|
||||
nsfw: false,
|
||||
};
|
||||
|
||||
let updateResponse: PostResponse = await fetch(
|
||||
`${lemmyAlphaApiUrl}/post`,
|
||||
{
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: wrapper(postForm),
|
||||
}
|
||||
).then(d => d.json());
|
||||
|
||||
expect(updateResponse.post.name).toBe(name);
|
||||
expect(updateResponse.post.community_local).toBe(false);
|
||||
expect(updateResponse.post.creator_local).toBe(true);
|
||||
|
||||
let getPostUrl = `${lemmyBetaApiUrl}/post?id=2`;
|
||||
let getPostRes: GetPostResponse = await fetch(getPostUrl, {
|
||||
method: 'GET',
|
||||
}).then(d => d.json());
|
||||
|
||||
expect(getPostRes.post.name).toBe(name);
|
||||
expect(getPostRes.post.community_local).toBe(true);
|
||||
expect(getPostRes.post.creator_local).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('create test comment', () => {
|
||||
test('/u/lemmy_alpha creates a comment on /c/lemmy_beta/main, its on both instances', async () => {
|
||||
let content = 'A jest test federated comment';
|
||||
let commentForm: CommentForm = {
|
||||
content,
|
||||
post_id: 2,
|
||||
auth: lemmyAlphaAuth,
|
||||
};
|
||||
|
||||
let createResponse: CommentResponse = await fetch(
|
||||
`${lemmyAlphaApiUrl}/comment`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: wrapper(commentForm),
|
||||
}
|
||||
).then(d => d.json());
|
||||
|
||||
expect(createResponse.comment.content).toBe(content);
|
||||
expect(createResponse.comment.community_local).toBe(false);
|
||||
expect(createResponse.comment.creator_local).toBe(true);
|
||||
|
||||
let getPostUrl = `${lemmyBetaApiUrl}/post?id=2`;
|
||||
let getPostRes: GetPostResponse = await fetch(getPostUrl, {
|
||||
method: 'GET',
|
||||
}).then(d => d.json());
|
||||
|
||||
expect(getPostRes.comments[0].content).toBe(content);
|
||||
expect(getPostRes.comments[0].community_local).toBe(true);
|
||||
expect(getPostRes.comments[0].creator_local).toBe(false);
|
||||
|
||||
// Now do beta replying to that comment, as a child comment
|
||||
let contentBeta = 'A child federated comment from beta';
|
||||
let commentFormBeta: CommentForm = {
|
||||
content: contentBeta,
|
||||
post_id: getPostRes.post.id,
|
||||
parent_id: getPostRes.comments[0].id,
|
||||
auth: lemmyBetaAuth,
|
||||
};
|
||||
|
||||
let createResponseBeta: CommentResponse = await fetch(
|
||||
`${lemmyBetaApiUrl}/comment`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: wrapper(commentFormBeta),
|
||||
}
|
||||
).then(d => d.json());
|
||||
|
||||
expect(createResponseBeta.comment.content).toBe(contentBeta);
|
||||
expect(createResponseBeta.comment.community_local).toBe(true);
|
||||
expect(createResponseBeta.comment.creator_local).toBe(true);
|
||||
expect(createResponseBeta.comment.parent_id).toBe(1);
|
||||
|
||||
// Make sure lemmy alpha sees that new child comment from beta
|
||||
let getPostUrlAlpha = `${lemmyAlphaApiUrl}/post?id=2`;
|
||||
let getPostResAlpha: GetPostResponse = await fetch(getPostUrlAlpha, {
|
||||
method: 'GET',
|
||||
}).then(d => d.json());
|
||||
|
||||
// The newest show up first
|
||||
expect(getPostResAlpha.comments[0].content).toBe(contentBeta);
|
||||
expect(getPostResAlpha.comments[0].community_local).toBe(false);
|
||||
expect(getPostResAlpha.comments[0].creator_local).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('update test comment', () => {
|
||||
test('/u/lemmy_alpha updates a comment on /c/lemmy_beta/main, its on both instances', async () => {
|
||||
let content = 'A jest test federated comment update';
|
||||
let commentForm: CommentForm = {
|
||||
content,
|
||||
post_id: 2,
|
||||
edit_id: 1,
|
||||
auth: lemmyAlphaAuth,
|
||||
creator_id: 2,
|
||||
};
|
||||
|
||||
let updateResponse: CommentResponse = await fetch(
|
||||
`${lemmyAlphaApiUrl}/comment`,
|
||||
{
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: wrapper(commentForm),
|
||||
}
|
||||
).then(d => d.json());
|
||||
|
||||
expect(updateResponse.comment.content).toBe(content);
|
||||
expect(updateResponse.comment.community_local).toBe(false);
|
||||
expect(updateResponse.comment.creator_local).toBe(true);
|
||||
|
||||
let getPostUrl = `${lemmyBetaApiUrl}/post?id=2`;
|
||||
let getPostRes: GetPostResponse = await fetch(getPostUrl, {
|
||||
method: 'GET',
|
||||
}).then(d => d.json());
|
||||
|
||||
expect(getPostRes.comments[1].content).toBe(content);
|
||||
expect(getPostRes.comments[1].community_local).toBe(true);
|
||||
expect(getPostRes.comments[1].creator_local).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function wrapper(form: any): string {
|
||||
return JSON.stringify(form);
|
||||
}
|
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>
|
||||
|
|
35
ui/src/components/community-link.tsx
vendored
Normal file
35
ui/src/components/community-link.tsx
vendored
Normal file
|
@ -0,0 +1,35 @@
|
|||
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;
|
||||
}
|
||||
|
||||
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_ = `${hostname(community.actor_id)}/${community.name}`;
|
||||
link = `/community/${community.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>
|
||||
|
|
43
ui/src/components/sidebar.tsx
vendored
43
ui/src/components/sidebar.tsx
vendored
|
@ -8,12 +8,7 @@ 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 { i18n } from '../i18next';
|
||||
|
@ -65,6 +60,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_ = `${hostname(community.actor_id)}/${community.name}`;
|
||||
link = community.actor_id;
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
<div class="card border-secondary mb-3">
|
||||
|
@ -82,9 +86,15 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
|
|||
</small>
|
||||
)}
|
||||
</h5>
|
||||
<Link className="text-muted" to={`/c/${community.name}`}>
|
||||
/c/{community.name}
|
||||
</Link>
|
||||
{community.local ? (
|
||||
<Link className="text-muted" to={link}>
|
||||
{name_}
|
||||
</Link>
|
||||
) : (
|
||||
<a className="text-muted" href={link} target="_blank">
|
||||
{name_}
|
||||
</a>
|
||||
)}
|
||||
<ul class="list-inline mb-1 text-muted font-weight-bold">
|
||||
{this.canMod && (
|
||||
<>
|
||||
|
@ -111,8 +121,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 +220,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')}
|
||||
|
|
20
ui/src/components/user-listing.tsx
vendored
20
ui/src/components/user-listing.tsx
vendored
|
@ -1,11 +1,14 @@
|
|||
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 {
|
||||
|
@ -19,8 +22,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_ = `${hostname(user.actor_id)}/${user.name}`;
|
||||
link = `/user/${user.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 +43,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} />
|
||||
</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}`;
|
||||
|
||||
|
|
28
ui/src/interfaces.ts
vendored
28
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;
|
||||
}
|
||||
|
||||
|
@ -628,7 +654,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;
|
||||
|
|
14
ui/src/utils.ts
vendored
14
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;
|
||||
|
@ -841,3 +841,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