Compare commits

...

99 commits

Author SHA1 Message Date
0425e8b114 Fixing nodeinfo error type. 2020-04-21 20:34:37 -04:00
18e570b021 Merge branch 'master' into merge_master_2 2020-04-21 20:29:52 -04:00
4e80543edb Update test_deploy.sh, supporting different branches 2020-04-21 19:19:10 +02:00
Felix Ableitner
a90b16a72c Merge pull request #649 from LemmyNet/federation_merge_from_master_1
Federation merge from master 1
2020-04-21 16:29:30 +00:00
f0026065f5 Merge branch 'master' into federation_merge_from_master_1 2020-04-21 10:25:29 -04:00
2f4b3a4f83 Merge branch 'remove_username_lower_unique' into federation 2020-04-21 09:25:02 -04:00
697c62fb64 Change local unique username constraints. 2020-04-21 09:21:29 -04:00
1e7c3841b2 Update federation dev instructions 2020-04-21 13:24:08 +02:00
7117b5ce32 Verifyt http signatures 2020-04-19 19:35:40 +02:00
5284dc0c52 Simplify signing code 2020-04-19 13:44:44 +02:00
8daf72278d Add http signature to outgoing apub requests 2020-04-18 20:54:20 +02:00
0199b5f169 Use debug logging 2020-04-18 17:24:55 +02:00
a49bd1d42a Fix bug in whitelist implementation 2020-04-18 17:17:25 +02:00
b1b97db11a Implement instance whitelist 2020-04-17 19:34:18 +02:00
c5ced6fa5e Added documentation for most functions 2020-04-17 17:33:55 +02:00
8908c8b184 Some code cleanup and better logging 2020-04-17 16:55:28 +02:00
9c974fbe50 Remove instance follows 2020-04-17 16:39:03 +02:00
86f172076b Implement search for activitypub IDs 2020-04-17 15:46:08 +02:00
9a85f1b25f Send activities to correct inbox, seperate community/user inboxes 2020-04-15 20:12:25 +02:00
a941c20024 Adding yarn run to run-federation-test.bash script. 2020-04-15 14:06:17 -04:00
7ba6ee8714 Redundant to_owned 2020-04-14 19:25:58 -04:00
fcf1c65fc1 Front end federation names and links for users, posts, and communities. 2020-04-14 19:18:13 -04:00
1336b4ed60 Merge branch 'dev' into federation 2020-04-14 16:07:20 -04:00
f040dac647 Initial post-listing community non-local. 2020-04-14 15:43:58 -04:00
26ad37a8c0 Updating views to add apub actor_id and local columns. 2020-04-14 15:12:19 -04:00
5c83cbc1ac Fixing .dockerignore. 2020-04-14 12:27:38 -04:00
9878a58452 Fixing unwrap crash with site_view.rs 2020-04-14 12:24:05 -04:00
9d2046d5a2 Disable nginx access logs for docker/federation/ 2020-04-14 17:47:55 +02:00
19c8461397 Implemented follow/accept 2020-04-14 17:37:23 +02:00
13e6c98e47 Auto-configure federation test instances during initial start 2020-04-13 19:55:43 +02:00
fdaf0b3364 Get inbox working properly 2020-04-13 15:06:41 +02:00
fac1cc7e1d Use summary field for post title 2020-04-13 14:13:06 +02:00
fc951d9295 Added comments about how to federate additional post/user fields 2020-04-12 16:53:55 +02:00
17d3d2492c Federate actor public keys 2020-04-10 15:50:40 +02:00
5e3902a3bc more todos 2020-04-10 14:45:48 +02:00
509005fa0c Rename federation-test to federation, puller.rs to fetcher.rs 2020-04-10 13:37:35 +02:00
492625f6d6 Add to/cc (and a bunch of todo) 2020-04-10 13:26:06 +02:00
483d11e772 Minor code cleanup 2020-04-09 21:26:22 +02:00
0b617377df Implement create activity 2020-04-09 21:04:31 +02:00
f5b58bcdaf Simplify fetch_posts code 2020-04-08 18:39:45 +02:00
5706b533fd Use instance struct instead of raw string 2020-04-08 18:22:44 +02:00
6962b9c433 Use Url instead of String 2020-04-08 14:37:05 +02:00
edd0ef5991 Some refactoring of puller.rs 2020-04-08 14:08:33 +02:00
d2bad5f79e Improve error handling 2020-04-08 13:23:59 +02:00
61c560c12c Get users federated 2020-04-07 23:02:32 +02:00
d3bd7771d2 remove debug log for post creation 2020-04-07 19:13:33 +02:00
b7103a7e14 Store remote communities/posts in db, federate posts! 2020-04-07 18:47:19 +02:00
1b0da74b57 Revert apub endpoint change (again) 2020-04-07 17:34:44 +02:00
095ccae616 Merge branch 'federation_add_fed_columns' of https://yerbamate.dev/dessalines/lemmy into federation 2020-04-07 17:33:50 +02:00
17bf6baa25 Set accept header and timeout for outgoing apub requests 2020-04-07 17:29:23 +02:00
56947e7710 Removing community name unique constraint. Removing useless fedi_name column from user_table. 2020-04-07 10:54:15 -04:00
4fadc4d072 Revert apub endpoint url changes 2020-04-07 13:21:30 +02:00
85ea1046f0 Adding post and comment ap_id columns. 2020-04-03 20:04:57 -04:00
cb7059f832 Move and rename some functions 2020-04-03 18:32:09 +02:00
c16458b728 Avoid using database views in federation code 2020-04-03 11:00:24 +02:00
6a7a262912 Merge branch 'federation_add_fed_columns' of https://yerbamate.dev/dessalines/lemmy into federation 2020-04-03 07:24:46 +02:00
96c3621a80 Share list of communities over apub, some refactoring 2020-04-03 07:02:43 +02:00
9197b39ed6 Federation DB Changes.
- Creating an activity table.
- Adding some federation-related columns to the user_ and community
  tables.
- Generating the actor_id and keys in code, updating the tables.
2020-04-03 00:12:05 -04:00
32b0275257 Merge branch 'nutomic-federation' into federation 2020-04-02 15:15:44 -04:00
31f835db86 Merge branch 'master' into federation 2020-04-02 15:11:11 -04:00
5ca466117d Merge branch 'master' into federation 2020-03-28 15:41:42 -04:00
0d369e6019 Get image uploads working for federation-test 2020-03-28 17:30:59 +01:00
945fd8331b Merge branch 'nutomic-federation' into federation 2020-03-24 15:18:08 -04:00
4354f868fd Merge branch 'federation' of ssh://yerbamate.dev:222/nutomic/lemmy into federation 2020-03-20 10:47:46 +01:00
bf52bc22e4 Replace reqwest with chttp 2020-03-20 01:42:07 +01:00
875545f7e1 Adjust for updated Rust version 2020-03-19 19:01:01 +01:00
672798e711 Populate post data from apub 2020-03-19 17:27:13 +01:00
nutomic
875ed79f3f Merge pull request 'Update to latest activitystreams' (#1) from asonix/lemmy:federation into federation 2020-03-19 01:23:15 +00:00
20a06ce3f2 Merge branch 'federation' of https://yerbamate.dev/nutomic/lemmy into federation 2020-03-18 20:16:40 -05:00
cfe0d9c9c2 Upgraded to latest activitystreams 2020-03-18 20:16:17 -05:00
33cce05300 Merge branch 'master' into federation 2020-03-18 22:51:34 +01:00
390b204272 Rewrite federation settings 2020-03-18 22:09:00 +01:00
bd030470b1 Read remote nodeinfo before doing anything 2020-03-18 16:08:08 +01:00
5043a52b88 Serve post data in apub format, some cleanup 2020-03-16 19:19:04 +01:00
05735b31c0 Remove boilerplate code 2020-03-16 18:30:25 +01:00
8ebcc7ac02 Implemented basics for post federation, plus a bunch of other stuff 2020-03-14 22:03:05 +01:00
5896a9d251 Move apub related code from websocket into api package 2020-03-14 13:15:23 +01:00
b01f4f75d6 WIP: federate posts between instances 2020-03-14 01:05:42 +01:00
8f67a3c634 Cleanup gitignore and dockerignore files 2020-03-12 20:25:14 +01:00
063811cb60 Merge branch 'master' into federation 2020-03-12 12:34:37 +01:00
27c07f1f84 Federate follower count, use string id for community 2020-03-12 03:35:32 +01:00
54172bd322 updated to activitystreams 0.4.0-alpha.3 2020-03-12 01:01:25 +01:00
8867fa1d52 use urls for id again, more comments 2020-03-11 18:26:58 +01:00
18be8b10f5 improved community federation (wip) 2020-03-11 12:29:10 +01:00
34a827a270 comment 2020-03-05 16:02:53 +01:00
91ae9a9d49 Revert "pull in activitypub library"
This reverts commit a52a954eb4.
2020-03-05 11:32:29 +01:00
1f29e91796 Various minor federation improvements 2020-02-29 18:38:47 +01:00
7cdf167e4b pull in activitypub library 2020-02-29 12:42:44 +01:00
b854d8f3a0 Some federation improvements 2020-02-29 03:11:39 +01:00
f9443dfbd3 Merge branch 'master' into federation 2020-02-29 00:47:37 +01:00
1d824ee293 Merge branch 'dev' into federation 2020-02-10 11:52:32 -05:00
8130535af4 Merge branch 'dev' into federation 2020-02-07 12:34:14 -05:00
f247b28262 Merge branch 'dev' into federation 2020-02-05 16:57:20 -05:00
ac4a62636b Merge branch 'dev' into federation 2020-02-05 14:19:25 -05:00
d932acad16 Merge branch 'federation' into dev_1 2020-02-05 12:51:03 -05:00
eaf548b5db Merge branch 'master' into federation 2020-01-14 16:30:54 +01:00
35489a706b Faster Docker build directly on host 2020-01-10 19:04:58 +01:00
e09a035373 Merge branch 'master' into federation 2020-01-02 19:22:23 +01:00
581f36d6ef Implementing very basic federation including test setup 2019-12-30 13:31:54 +01:00
82 changed files with 5088 additions and 2144 deletions

7
.dockerignore vendored
View file

@ -1,6 +1,11 @@
# 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
.git
ansible
# exceptions, needed for federation-test build
!server/target/debug/lemmy_server

15
.gitignore vendored
View file

@ -1,10 +1,17 @@
# 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/federation/volumes
docker/dev/volumes
docker/federation-test/volumes
# local build files
build/
ui/src/translations
# ide config
.idea/

View file

@ -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

17
docker/federation/Dockerfile vendored Normal file
View 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"]

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

@ -0,0 +1,91 @@
version: '3.3'
services:
nginx:
image: nginx:1.17-alpine
ports:
- "8540:8540"
- "8550:8550"
volumes:
- ./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
View 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
View 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 ../../ -f Dockerfile -t lemmy-federation:latest
sudo docker-compose up

View file

@ -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
```

2444
server/Cargo.lock generated vendored

File diff suppressed because it is too large Load diff

7
server/Cargo.toml vendored
View file

@ -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,13 @@ 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"

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

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -102,6 +102,8 @@ impl Perform for Oper<CreateComment> {
deleted: None,
read: None,
updated: None,
ap_id: "changeme".into(),
local: true,
};
let inserted_comment = match Comment::create(&conn, &comment_form) {
@ -109,6 +111,11 @@ impl Perform for Oper<CreateComment> {
Err(_e) => return Err(APIError::err("couldnt_create_comment").into()),
};
match Comment::update_ap_id(&conn, inserted_comment.id) {
Ok(comment) => comment,
Err(_e) => return Err(APIError::err("couldnt_create_comment").into()),
};
let mut recipient_ids = Vec::new();
// Scan the comment for user mentions, add those rows
@ -297,6 +304,8 @@ impl Perform for Oper<EditComment> {
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,
@ -310,6 +319,8 @@ impl Perform for Oper<EditComment> {
} 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) {

View file

@ -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()),
) {
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)?;
follow_community(&community, &user, &conn)?;
// 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) {

View file

@ -22,6 +22,12 @@ use crate::{
naive_now, remove_slurs, send_email, slur_check, slurs_vec_to_str,
};
use crate::apub::{
activities::{follow_community, post_create, post_update},
fetcher::search_by_apub_id,
signatures::generate_actor_keypair,
{make_apub_endpoint, EndpointType},
};
use crate::settings::Settings;
use crate::websocket::UserOperation;
use crate::websocket::{
@ -34,7 +40,7 @@ use crate::websocket::{
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;

View file

@ -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()),
};
post_create(&updated_post, &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)?;
}
post_update(&updated_post, &user, &conn)?;
let post_view = PostView::read(&conn, data.edit_id, Some(user_id))?;
let res = PostResponse { post: post_view };

View file

@ -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)

View file

@ -261,10 +261,11 @@ impl Perform for Oper<Register> {
return Err(APIError::err("admin_already_created").into());
}
let 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(keypair.private_key),
public_key: Some(keypair.public_key),
last_refreshed_at: None,
};
// Create the user
@ -298,12 +305,15 @@ impl Perform for Oper<Register> {
}
};
let 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(keypair.private_key),
public_key: Some(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) {
@ -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()),
};

View file

@ -0,0 +1,180 @@
use crate::apub::is_apub_id_valid;
use crate::apub::signatures::sign;
use crate::db::community::Community;
use crate::db::community_view::CommunityFollowerView;
use crate::db::post::Post;
use crate::db::user::User_;
use crate::db::Crud;
use activitystreams::activity::{Accept, Create, Follow, Update};
use activitystreams::object::properties::ObjectProperties;
use activitystreams::BaseBox;
use activitystreams::{context, public};
use diesel::PgConnection;
use failure::Error;
use failure::_core::fmt::Debug;
use isahc::prelude::*;
use log::debug;
use serde::Serialize;
use url::Url;
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.
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);
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(())
}
/// For a given community, returns the inboxes of all followers.
fn get_follower_inboxes(conn: &PgConnection, community: &Community) -> Result<Vec<String>, Error> {
Ok(
CommunityFollowerView::for_community(conn, community.id)?
.iter()
.filter(|c| !c.user_local)
.map(|c| format!("{}/inbox", c.user_actor_id.to_owned()))
.collect(),
)
}
/// Send out information about a newly created post, to the followers of the community.
pub fn post_create(post: &Post, creator: &User_, conn: &PgConnection) -> Result<(), Error> {
let page = post.as_page(conn)?;
let community = Community::read(conn, post.community_id)?;
let mut create = Create::new();
populate_object_props(
&mut create.object_props,
&community.get_followers_url(),
&post.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,
get_follower_inboxes(conn, &community)?,
)?;
Ok(())
}
/// Send out information about an edited post, to the followers of the community.
pub fn post_update(post: &Post, creator: &User_, conn: &PgConnection) -> Result<(), Error> {
let page = post.as_page(conn)?;
let community = Community::read(conn, post.community_id)?;
let mut update = Update::new();
populate_object_props(
&mut update.object_props,
&community.get_followers_url(),
&post.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,
get_follower_inboxes(conn, &community)?,
)?;
Ok(())
}
/// As a given local user, send out a follow request to a remote community.
pub fn follow_community(
community: &Community,
user: &User_,
_conn: &PgConnection,
) -> Result<(), Error> {
let mut follow = Follow::new();
follow
.object_props
.set_context_xsd_any_uri(context())?
// TODO: needs proper id
.set_id(user.actor_id.clone())?;
follow
.follow_props
.set_actor_xsd_any_uri(user.actor_id.clone())?
.set_object_xsd_any_uri(community.actor_id.clone())?;
let to = format!("{}/inbox", community.actor_id);
send_activity(
&follow,
&user.private_key.as_ref().unwrap(),
&community.actor_id,
vec![to],
)?;
Ok(())
}
/// As a local community, accept the follow request from a remote user.
pub fn accept_follow(follow: &Follow, conn: &PgConnection) -> Result<(), Error> {
let community_uri = follow
.follow_props
.get_object_xsd_any_uri()
.unwrap()
.to_string();
let community = Community::read_from_actor_id(conn, &community_uri)?;
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_object_base_box(BaseBox::from_concrete(follow.clone())?)?;
let to = format!("{}/inbox", community_uri);
send_activity(
&accept,
&community.private_key.unwrap(),
&community.actor_id,
vec![to],
)?;
Ok(())
}

View file

@ -1,109 +1,183 @@
use crate::apub::make_apub_endpoint;
use crate::db::community::Community;
use crate::apub::fetcher::{fetch_remote_object, fetch_remote_user};
use crate::apub::signatures::PublicKey;
use crate::apub::*;
use crate::db::community::{Community, CommunityForm};
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 crate::db::post::Post;
use crate::db::user::User_;
use crate::db::Crud;
use crate::{convert_datetime, naive_now};
use activitystreams::actor::properties::ApActorProperties;
use activitystreams::collection::OrderedCollection;
use activitystreams::{
actor::Group, collection::UnorderedCollection, context, ext::Extensible,
object::properties::ObjectProperties,
};
use actix_web::body::Body;
use actix_web::web::Path;
use actix_web::HttpResponse;
use actix_web::{web, Result};
use diesel::r2d2::{ConnectionManager, Pool};
use diesel::PgConnection;
use failure::Error;
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 url::Url;
#[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 Community {
// Turn a Lemmy Community into an ActivityPub group that can be sent out over the network.
fn as_group(&self, conn: &PgConnection) -> Result<GroupExt, Error> {
let mut group = Group::default();
let oprops: &mut ObjectProperties = group.as_mut();
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()
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 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_followers(self.get_followers_url())?;
let public_key = PublicKey {
id: format!("{}#main-key", self.actor_id),
owner: self.actor_id.to_owned(),
public_key_pem: self.public_key.to_owned().unwrap(),
};
Ok(group.extend(actor_props).extend(public_key.to_ext()))
}
pub fn get_followers_url(&self) -> String {
format!("{}/followers", &self.actor_id)
}
pub fn get_inbox_url(&self) -> String {
format!("{}/inbox", &self.actor_id)
}
pub fn get_outbox_url(&self) -> String {
format!("{}/outbox", &self.actor_id)
}
}
pub async fn get_apub_community_followers(info: Path<CommunityQuery>) -> HttpResponse<Body> {
let connection = establish_unpooled_connection();
impl CommunityForm {
/// Parse an ActivityPub group received from another instance into a Lemmy community.
pub fn from_group(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;
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()
let followers_uri = Url::parse(&aprops.get_followers().unwrap().to_string())?;
let outbox_uri = Url::parse(&aprops.get_outbox().to_string())?;
let _outbox = fetch_remote_object::<OrderedCollection>(&outbox_uri)?;
let _followers = fetch_remote_object::<UnorderedCollection>(&followers_uri)?;
let apub_id = Url::parse(&oprops.get_attributed_to_xsd_any_uri().unwrap().to_string())?;
let creator = fetch_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: web::Data<Pool<ConnectionManager<PgConnection>>>,
) -> Result<HttpResponse<Body>, Error> {
let community = Community::read_from_name(&&db.get()?, info.community_name.to_owned())?;
let c = community.as_group(&db.get().unwrap())?;
Ok(create_apub_response(&c))
}
/// Returns an empty followers collection, only populating the siz (for privacy).
pub async fn get_apub_community_followers(
info: Path<CommunityQuery>,
db: web::Data<Pool<ConnectionManager<PgConnection>>>,
) -> Result<HttpResponse<Body>, Error> {
let community = Community::read_from_name(&&db.get()?, info.community_name.to_owned())?;
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, 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))
}
/// Returns an UnorderedCollection with the latest posts from the community.
pub async fn get_apub_community_outbox(
info: Path<CommunityQuery>,
db: web::Data<Pool<ConnectionManager<PgConnection>>>,
) -> Result<HttpResponse<Body>, Error> {
let community = Community::read_from_name(&&db.get()?, info.community_name.to_owned())?;
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))
}

View file

@ -0,0 +1,69 @@
use crate::apub::activities::accept_follow;
use crate::apub::fetcher::fetch_remote_user;
use crate::apub::signatures::verify;
use crate::db::community::{Community, CommunityFollower, CommunityFollowerForm};
use crate::db::Followable;
use activitystreams::activity::Follow;
use actix_web::{web, HttpRequest, HttpResponse};
use diesel::r2d2::{ConnectionManager, Pool};
use diesel::PgConnection;
use failure::Error;
use log::debug;
use serde::Deserialize;
use url::Url;
#[serde(untagged)]
#[derive(Deserialize, Debug)]
pub enum CommunityAcceptedObjects {
Follow(Follow),
}
/// Handler for all incoming activities to community inboxes.
pub async fn community_inbox(
request: HttpRequest,
input: web::Json<CommunityAcceptedObjects>,
path: web::Path<String>,
db: web::Data<Pool<ConnectionManager<PgConnection>>>,
) -> Result<HttpResponse, Error> {
let input = input.into_inner();
let conn = &db.get().unwrap();
debug!(
"Community {} received activity {:?}",
&path.into_inner(),
&input
);
match input {
CommunityAcceptedObjects::Follow(f) => handle_follow(&f, &request, conn),
}
}
/// Handle a follow request from a remote user, adding it to the local database and returning an
/// Accept activity.
fn handle_follow(
follow: &Follow,
request: &HttpRequest,
conn: &PgConnection,
) -> Result<HttpResponse, Error> {
let user_uri = follow
.follow_props
.get_actor_xsd_any_uri()
.unwrap()
.to_string();
let user = fetch_remote_user(&Url::parse(&user_uri)?, conn)?;
verify(&request, &user.public_key.unwrap())?;
// TODO: make sure this is a local community
let community_uri = follow
.follow_props
.get_object_xsd_any_uri()
.unwrap()
.to_string();
let community = Community::read_from_actor_id(conn, &community_uri)?;
let community_follower_form = CommunityFollowerForm {
community_id: community.id,
user_id: user.id,
};
CommunityFollower::follow(&conn, &community_follower_form)?;
accept_follow(&follow, conn)?;
Ok(HttpResponse::Ok().finish())
}

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

@ -0,0 +1,158 @@
use crate::api::site::SearchResponse;
use crate::apub::*;
use crate::db::community::{Community, CommunityForm};
use crate::db::community_view::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, SearchType};
use crate::routes::nodeinfo::{NodeInfo, NodeInfoWellKnown};
use activitystreams::collection::OrderedCollection;
use activitystreams::object::Page;
use activitystreams::BaseBox;
use diesel::result::Error::NotFound;
use diesel::PgConnection;
use failure::Error;
use isahc::prelude::*;
use serde::Deserialize;
use std::time::Duration;
use url::Url;
// 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)?)
}
// TODO: move these to db
fn upsert_community(
community_form: &CommunityForm,
conn: &PgConnection,
) -> Result<Community, Error> {
let existing = Community::read_from_actor_id(conn, &community_form.actor_id);
match existing {
Err(NotFound {}) => Ok(Community::create(conn, &community_form)?),
Ok(c) => Ok(Community::update(conn, c.id, &community_form)?),
Err(e) => Err(Error::from(e)),
}
}
fn upsert_user(user_form: &UserForm, conn: &PgConnection) -> Result<User_, Error> {
let existing = User_::read_from_apub_id(conn, &user_form.actor_id);
Ok(match existing {
Err(NotFound {}) => User_::create(conn, &user_form)?,
Ok(u) => User_::update(conn, u.id, &user_form)?,
Err(e) => return 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)),
}
}
/// Fetch any type of ActivityPub object, handling things like HTTP headers, deserialisation,
/// timeouts etc.
/// TODO: add an optional param last_updated and only fetch if its too old
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/federation/c/main
/// http://lemmy_alpha:8540/federation/u/lemmy_alpha
/// http://lemmy_alpha:8540/federation/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 u = upsert_user(&UserForm::from_person(&p)?, conn)?;
response.users = vec![UserView::read(conn, u.id)?];
}
SearchAcceptedObjects::Group(g) => {
let c = upsert_community(&CommunityForm::from_group(&g, conn)?, conn)?;
fetch_community_outbox(&c, conn)?;
response.communities = vec![CommunityView::read(conn, c.id, None)?];
}
SearchAcceptedObjects::Page(p) => {
let p = upsert_post(&PostForm::from_page(&p, conn)?, conn)?;
response.posts = vec![PostView::read(conn, p.id, None)?];
}
}
Ok(response)
}
/// 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>>()?,
)
}
/// Fetch a user, insert/update it in the database and return the user.
pub fn fetch_remote_user(apub_id: &Url, conn: &PgConnection) -> Result<User_, Error> {
let person = fetch_remote_object::<PersonExt>(apub_id)?;
let uf = UserForm::from_person(&person)?;
upsert_user(&uf, conn)
}
/// Fetch a community, insert/update it in the database and return the community.
pub fn fetch_remote_community(apub_id: &Url, conn: &PgConnection) -> Result<Community, Error> {
let group = fetch_remote_object::<GroupExt>(apub_id)?;
let cf = CommunityForm::from_group(&group, conn)?;
upsert_community(&cf, conn)
}

View file

@ -1,107 +1,90 @@
pub mod activities;
pub mod community;
pub mod community_inbox;
pub mod fetcher;
pub mod post;
pub mod signatures;
pub mod user;
pub mod user_inbox;
use crate::apub::signatures::PublicKeyExtension;
use crate::Settings;
use activitystreams::actor::{properties::ApActorProperties, Group, Person};
use activitystreams::ext::Ext;
use actix_web::body::Body;
use actix_web::HttpResponse;
use serde::ser::Serialize;
use url::Url;
use std::fmt::Display;
type GroupExt = Ext<Ext<Group, ApActorProperties>, PublicKeyExtension>;
type PersonExt = Ext<Ext<Person, ApActorProperties>, PublicKeyExtension>;
#[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};
static APUB_JSON_CONTENT_TYPE: &str = "application/activity+json";
#[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,
};
let person = user.as_person();
assert_eq!(
format!("https://{}/federation/u/thom", Settings::get().hostname),
person.object_props.id_string().unwrap()
);
}
#[test]
fn test_community() {
let community = Community {
id: 42,
name: "Test".into(),
title: "Test Title".into(),
description: Some("Test community".into()),
category_id: 32,
creator_id: 52,
removed: false,
published: naive_now(),
updated: Some(naive_now()),
deleted: false,
nsfw: false,
};
let group = community.as_group();
assert_eq!(
format!("https://{}/federation/c/Test", Settings::get().hostname),
group.object_props.id_string().unwrap()
);
}
#[test]
fn test_post() {
let post = Post {
id: 62,
name: "A test post".into(),
url: None,
body: None,
creator_id: 52,
community_id: 42,
published: naive_now(),
removed: false,
locked: false,
stickied: false,
nsfw: false,
deleted: false,
updated: None,
embed_title: None,
embed_description: None,
embed_html: None,
thumbnail_url: None,
};
let page = post.as_page();
assert_eq!(
format!("https://{}/federation/post/62", Settings::get().hostname),
page.object_props.id_string().unwrap()
);
}
pub enum EndpointType {
Community,
User,
Post,
Comment,
}
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 => "p",
// TODO I have to change this else my update advanced_migrations crashes the
// server if a comment exists.
EndpointType::Comment => "comment",
};
Url::parse(&format!(
"{}://{}/federation/{}/{}",
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,
}
}

View file

@ -1,38 +1,105 @@
use crate::apub::make_apub_endpoint;
use crate::db::post::Post;
use crate::to_datetime_utc;
use activitypub::{context, object::Page};
use crate::apub::create_apub_response;
use crate::apub::fetcher::{fetch_remote_community, fetch_remote_user};
use crate::convert_datetime;
use crate::db::community::Community;
use crate::db::post::{Post, PostForm};
use crate::db::user::User_;
use crate::db::Crud;
use activitystreams::{context, object::properties::ObjectProperties, object::Page};
use actix_web::body::Body;
use actix_web::web::Path;
use actix_web::{web, HttpResponse};
use diesel::r2d2::{ConnectionManager, Pool};
use diesel::PgConnection;
use failure::Error;
use serde::Deserialize;
use url::Url;
#[derive(Deserialize)]
pub struct PostQuery {
post_id: String,
}
/// Return the post json over HTTP.
pub async fn get_apub_post(
info: Path<PostQuery>,
db: web::Data<Pool<ConnectionManager<PgConnection>>>,
) -> Result<HttpResponse<Body>, Error> {
let id = info.post_id.parse::<i32>()?;
let post = Post::read(&&db.get()?, id)?;
Ok(create_apub_response(&post.as_page(&db.get().unwrap())?))
}
impl Post {
pub fn as_page(&self) -> Page {
let base_url = make_apub_endpoint("post", self.id);
// Turn a Lemmy post into an ActivityPub page that can be sent out over the network.
pub fn as_page(&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 PostForm {
/// Parse an ActivityPub page received from another instance into a Lemmy post.
pub fn from_page(page: &Page, conn: &PgConnection) -> Result<PostForm, Error> {
let oprops = &page.object_props;
let creator_id = Url::parse(&oprops.get_attributed_to_xsd_any_uri().unwrap().to_string())?;
let creator = fetch_remote_user(&creator_id, conn)?;
let community_id = Url::parse(&oprops.get_to_xsd_any_uri().unwrap().to_string())?;
let community = fetch_remote_community(&community_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,
})
}
}

View file

@ -0,0 +1,132 @@
use activitystreams::{actor::Actor, ext::Extension};
use actix_web::HttpRequest;
use failure::Error;
use http::request::Builder;
use http_signature_normalization::Config;
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;
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 {}

View file

@ -1,74 +1,101 @@
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 crate::apub::signatures::PublicKey;
use crate::apub::{create_apub_response, PersonExt};
use crate::db::user::{UserForm, User_};
use crate::{convert_datetime, naive_now};
use activitystreams::{
actor::{properties::ApActorProperties, Person},
context,
ext::Extensible,
object::properties::ObjectProperties,
};
use actix_web::body::Body;
use actix_web::web::Path;
use actix_web::HttpResponse;
use actix_web::{web, Result};
use diesel::r2d2::{ConnectionManager, Pool};
use diesel::PgConnection;
use failure::Error;
use serde::Deserialize;
impl User_ {
pub fn as_person(&self) -> Person {
let base_url = make_apub_endpoint("u", &self.name);
let mut person = Person::default();
person.object_props.set_context_object(context()).ok();
person.object_props.set_id_string(base_url.to_string()).ok();
person
.object_props
.set_name_string(self.name.to_owned())
.ok();
person
.object_props
.set_published_utctime(to_datetime_utc(self.published))
.ok();
if let Some(updated) = self.updated {
person
.object_props
.set_updated_utctime(to_datetime_utc(updated))
.ok();
}
person
.ap_actor_props
.set_inbox_string(format!("{}/inbox", &base_url))
.ok();
person
.ap_actor_props
.set_outbox_string(format!("{}/outbox", &base_url))
.ok();
person
.ap_actor_props
.set_following_string(format!("{}/following", &base_url))
.ok();
person
.ap_actor_props
.set_liked_string(format!("{}/liked", &base_url))
.ok();
if let Some(i) = &self.preferred_username {
person
.ap_actor_props
.set_preferred_username_string(i.to_string())
.ok();
}
person
}
}
#[derive(Deserialize)]
pub struct UserQuery {
user_name: String,
}
pub async fn get_apub_user(info: Path<UserQuery>) -> HttpResponse<Body> {
let connection = establish_unpooled_connection();
// Turn a Lemmy user into an ActivityPub person and return it as json.
pub async fn get_apub_user(
info: Path<UserQuery>,
db: web::Data<Pool<ConnectionManager<PgConnection>>>,
) -> Result<HttpResponse<Body>, Error> {
let user = User_::find_by_email_or_username(&&db.get()?, &info.user_name)?;
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()
let mut person = Person::default();
let oprops: &mut ObjectProperties = person.as_mut();
oprops
.set_context_xsd_any_uri(context())?
.set_id(user.actor_id.to_string())?
.set_name_xsd_string(user.name.to_owned())?
.set_published(convert_datetime(user.published))?;
if let Some(u) = user.updated {
oprops.set_updated(convert_datetime(u))?;
}
if let Some(i) = &user.preferred_username {
oprops.set_name_xsd_string(i.to_owned())?;
}
let mut actor_props = ApActorProperties::default();
actor_props
.set_inbox(format!("{}/inbox", &user.actor_id))?
.set_outbox(format!("{}/outbox", &user.actor_id))?
.set_following(format!("{}/following", &user.actor_id))?
.set_liked(format!("{}/liked", &user.actor_id))?;
let public_key = PublicKey {
id: format!("{}#main-key", user.actor_id),
owner: user.actor_id.to_owned(),
public_key_pem: user.public_key.unwrap(),
};
Ok(create_apub_response(
&person.extend(actor_props).extend(public_key.to_ext()),
))
}
impl UserForm {
/// Parse an ActivityPub person received from another instance into a Lemmy user.
pub fn from_person(person: &PersonExt) -> 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()),
})
}
}

View file

@ -0,0 +1,119 @@
use crate::apub::fetcher::{fetch_remote_community, fetch_remote_user};
use crate::apub::signatures::verify;
use crate::db::post::{Post, PostForm};
use crate::db::Crud;
use activitystreams::activity::{Accept, Create, Update};
use activitystreams::object::Page;
use actix_web::{web, HttpRequest, HttpResponse};
use diesel::r2d2::{ConnectionManager, Pool};
use diesel::PgConnection;
use failure::Error;
use log::debug;
use serde::Deserialize;
use url::Url;
#[serde(untagged)]
#[derive(Deserialize, Debug)]
pub enum UserAcceptedObjects {
Create(Create),
Update(Update),
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: web::Data<Pool<ConnectionManager<PgConnection>>>,
) -> 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();
debug!(
"User {} received activity: {:?}",
&path.into_inner(),
&input
);
match input {
UserAcceptedObjects::Create(c) => handle_create(&c, &request, conn),
UserAcceptedObjects::Update(u) => handle_update(&u, &request, conn),
UserAcceptedObjects::Accept(a) => handle_accept(&a, &request, conn),
}
}
/// Handle create activities and insert them in the database.
fn handle_create(
create: &Create,
request: &HttpRequest,
conn: &PgConnection,
) -> Result<HttpResponse, Error> {
let community_uri = create
.create_props
.get_actor_xsd_any_uri()
.unwrap()
.to_string();
// TODO: should do this in a generic way so we dont need to know if its a user or a community
let user = fetch_remote_user(&Url::parse(&community_uri)?, conn)?;
verify(request, &user.public_key.unwrap())?;
let page = create
.create_props
.get_object_base_box()
.to_owned()
.unwrap()
.to_owned()
.to_concrete::<Page>()?;
let post = PostForm::from_page(&page, conn)?;
Post::create(conn, &post)?;
// TODO: send the new post out via websocket
Ok(HttpResponse::Ok().finish())
}
/// Handle update activities and insert them in the database.
fn handle_update(
update: &Update,
request: &HttpRequest,
conn: &PgConnection,
) -> Result<HttpResponse, Error> {
let community_uri = update
.update_props
.get_actor_xsd_any_uri()
.unwrap()
.to_string();
let user = fetch_remote_user(&Url::parse(&community_uri)?, conn)?;
verify(request, &user.public_key.unwrap())?;
let page = update
.update_props
.get_object_base_box()
.to_owned()
.unwrap()
.to_owned()
.to_concrete::<Page>()?;
let post = PostForm::from_page(&page, conn)?;
let id = Post::read_from_apub_id(conn, &post.ap_id)?.id;
Post::update(conn, id, &post)?;
// TODO: send the new post out via websocket
Ok(HttpResponse::Ok().finish())
}
/// Handle accepted follows.
fn handle_accept(
accept: &Accept,
request: &HttpRequest,
conn: &PgConnection,
) -> Result<HttpResponse, Error> {
let community_uri = accept
.accept_props
.get_actor_xsd_any_uri()
.unwrap()
.to_string();
let community = fetch_remote_community(&Url::parse(&community_uri)?, conn)?;
verify(request, &community.public_key.unwrap())?;
// 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())
}

View 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(())
}

View file

@ -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)]
@ -36,6 +40,8 @@ pub struct CommentForm {
pub read: Option<bool>,
pub updated: Option<chrono::NaiveDateTime>,
pub deleted: Option<bool>,
pub ap_id: String,
pub local: bool,
}
impl Crud<CommentForm> for Comment {
@ -68,6 +74,37 @@ 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 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 +207,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 +222,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 +242,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 +268,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();
@ -233,6 +284,8 @@ mod tests {
read: None,
parent_id: None,
updated: None,
ap_id: "changeme".into(),
local: true,
};
let inserted_comment = Comment::create(&conn, &comment_form).unwrap();
@ -248,6 +301,8 @@ mod tests {
parent_id: None,
published: inserted_comment.published,
updated: None,
ap_id: "changeme".into(),
local: true,
};
let child_comment_form = CommentForm {
@ -259,6 +314,8 @@ mod tests {
deleted: None,
read: None,
updated: None,
ap_id: "changeme".into(),
local: true,
};
let inserted_child_comment = Comment::create(&conn, &child_comment_form).unwrap();

View file

@ -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();
@ -497,6 +541,8 @@ mod tests {
deleted: None,
read: None,
updated: None,
ap_id: "changeme".into(),
local: true,
};
let inserted_comment = Comment::create(&conn, &comment_form).unwrap();
@ -535,6 +581,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 +614,12 @@ mod tests {
my_vote: Some(1),
subscribed: None,
saved: None,
ap_id: "changeme".to_string(),
local: true,
community_actor_id: inserted_community.actor_id.to_owned(),
community_local: true,
creator_actor_id: inserted_user.actor_id.to_owned(),
creator_local: true,
};
let mut read_comment_views_no_user = CommentQueryBuilder::create(&conn)

View file

@ -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 {
@ -69,6 +80,18 @@ impl Community {
.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 {

View file

@ -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,
}

View file

@ -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;

View file

@ -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();
@ -523,6 +542,8 @@ mod tests {
read: None,
parent_id: None,
updated: None,
ap_id: "changeme".into(),
local: true,
};
let inserted_comment = Comment::create(&conn, &comment_form).unwrap();

View file

@ -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();

View file

@ -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

View file

@ -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)

View file

@ -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();

View file

@ -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> {
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_apub_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();

View file

@ -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();
@ -149,6 +168,8 @@ mod tests {
read: None,
parent_id: None,
updated: None,
ap_id: "changeme".into(),
local: true,
};
let inserted_comment = Comment::create(&conn, &comment_form).unwrap();

View file

@ -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> {

View file

@ -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,

View file

@ -16,6 +16,8 @@ pub extern crate dotenv;
pub extern crate jsonwebtoken;
pub extern crate lettre;
pub extern crate lettre_email;
extern crate log;
pub extern crate openssl;
pub extern crate rand;
pub extern crate regex;
pub extern crate rss;
@ -34,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)
}

View file

@ -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?,
)
}

View file

@ -2,17 +2,37 @@ use super::*;
use crate::apub;
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
// TODO: check the user/community params for these
.route(
"/federation/c/{community_name}/inbox",
web::post().to(apub::community_inbox::community_inbox),
)
.route(
"/federation/u/{user_name}/inbox",
web::post().to(apub::user_inbox::user_inbox),
)
.route(
"/federation/c/{community_name}",
web::get().to(apub::community::get_apub_community_http),
)
.route(
"/federation/c/{community_name}/followers",
web::get().to(apub::community::get_apub_community_followers),
)
.route(
"/federation/c/{community_name}/outbox",
web::get().to(apub::community::get_apub_community_outbox),
)
.route(
"/federation/u/{user_name}",
web::get().to(apub::user::get_apub_user),
)
.route(
"/federation/p/{post_id}",
web::get().to(apub::post::get_apub_post),
);
}
}

View file

@ -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>>>;

View file

@ -6,14 +6,18 @@ 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(
@ -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,
}

View file

@ -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),
@ -38,13 +38,9 @@ async fn get_webfinger_response(
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")),
};
@ -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}"
//}
]
}))
})

View file

@ -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,

View file

@ -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,

3
ui/.gitignore vendored
View file

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

2
ui/package.json vendored
View file

@ -18,7 +18,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",

View file

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

View file

@ -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')}

View file

@ -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>

View file

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

35
ui/src/components/community-link.tsx vendored Normal file
View 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>;
}
}

View file

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

View file

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

View file

@ -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;

View file

@ -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>

View file

@ -20,6 +20,7 @@ import { MomentTime } from './moment-time';
import { PostForm } from './post-form';
import { IFramelyCard } from './iframely-card';
import { UserListing } from './user-listing';
import { CommunityLink } from './community-link';
import {
md,
mdToHtml,
@ -30,6 +31,7 @@ import {
getUnixTime,
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>

View file

@ -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

View file

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

View file

@ -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>

View file

@ -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')}

View file

@ -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>
);
}

View file

@ -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
View file

@ -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}`;

26
ui/src/interfaces.ts vendored
View file

@ -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;
}

14
ui/src/utils.ts vendored
View file

@ -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;
}

15
ui/yarn.lock vendored
View file

@ -234,26 +234,13 @@
dependencies:
"@types/markdown-it" "*"
"@types/markdown-it@*":
"@types/markdown-it@*", "@types/markdown-it@^0.0.9":
version "0.0.9"
resolved "https://registry.yarnpkg.com/@types/markdown-it/-/markdown-it-0.0.9.tgz#a5d552f95216c478e0a27a5acc1b28dcffd989ce"
integrity sha512-IFSepyZXbF4dgSvsk8EsgaQ/8Msv1I5eTL0BZ0X3iGO9jw6tCVtPG8HchIPm3wrkmGdqZOD42kE0zplVi1gYDA==
dependencies:
"@types/linkify-it" "*"
"@types/markdown-it@^10.0.0":
version "10.0.0"
resolved "https://registry.yarnpkg.com/@types/markdown-it/-/markdown-it-10.0.0.tgz#a2b5f9fb444bb27c1e0c4a0116fea09b3c6ebc1e"
integrity sha512-7UPBg1W0rfsqQ1JwNFfhxibKO0t7Q0scNt96XcFIFLGE/vhZamzZayaFS2LKha/26Pz7b/2GgiaxQZ1GUwW0dA==
dependencies:
"@types/linkify-it" "*"
"@types/mdurl" "*"
"@types/mdurl@*":
version "1.0.2"
resolved "https://registry.yarnpkg.com/@types/mdurl/-/mdurl-1.0.2.tgz#e2ce9d83a613bacf284c7be7d491945e39e1f8e9"
integrity sha512-eC4U9MlIcu2q0KQmXszyn5Akca/0jrQmwDRgpAMJai7qBWq4amIQhZyNau4VYGtCeALvW1/NtjzJJ567aZxfKA==
"@types/node@^13.11.1":
version "13.11.1"
resolved "https://registry.yarnpkg.com/@types/node/-/node-13.11.1.tgz#49a2a83df9d26daacead30d0ccc8762b128d53c7"