Merge branch 'master' into arrudaricardo-issue-#814

This commit is contained in:
Dessalines 2020-07-03 21:51:42 -04:00
commit b1b755713e
70 changed files with 4409 additions and 2922 deletions

2
ansible/VERSION vendored
View file

@ -1 +1 @@
v0.7.5 v0.7.8

View file

@ -1,6 +1,12 @@
[lemmy] [lemmy]
# define the username and hostname that you use for ssh connection, and specify the domain # to get started, copy this file to `inventory` and adjust the values below.
myuser@example.com domain=example.com letsencrypt_contact_email=your@email.com # - `myuser@example.com`: replace with the destination you use to connect to your server via ssh
# - `domain=example.com`: replace `example.com` with your lemmy domain
# - `letsencrypt_contact_email=your@email.com` replace `your@email.com` with your email address,
# to get notifications if your ssl cert expires
# - `lemmy_base_dir=/srv/lemmy`: the location on the server where lemmy can be installed, can be any folder
# if you are upgrading from a previous version, set this to `/lemmy`
myuser@example.com domain=example.com letsencrypt_contact_email=your@email.com lemmy_base_dir=/srv/lemmy
[all:vars] [all:vars]
ansible_connection=ssh ansible_connection=ssh

76
ansible/lemmy.yml vendored
View file

@ -5,18 +5,41 @@
# https://www.josharcher.uk/code/ansible-python-connection-failure-ubuntu-server-1604/ # https://www.josharcher.uk/code/ansible-python-connection-failure-ubuntu-server-1604/
gather_facts: False gather_facts: False
pre_tasks: pre_tasks:
- name: check lemmy_base_dir
fail:
msg: "`lemmy_base_dir` is unset. if you are upgrading from an older version, add `lemmy_base_dir=/lemmy` to your inventory file."
when: lemmy_base_dir is not defined
- name: install python for Ansible - name: install python for Ansible
# python2-minimal instead of python-minimal for ubuntu 20.04 and up
raw: test -e /usr/bin/python || (apt -y update && apt install -y python-minimal python-setuptools) raw: test -e /usr/bin/python || (apt -y update && apt install -y python-minimal python-setuptools)
args: args:
executable: /bin/bash executable: /bin/bash
register: output register: output
changed_when: output.stdout != "" changed_when: output.stdout != ''
- setup: # gather facts - setup: # gather facts
tasks: tasks:
- name: install dependencies - name: install dependencies
apt: apt:
pkg: ['nginx', 'docker-compose', 'docker.io', 'certbot', 'python-certbot-nginx'] pkg:
- 'nginx'
- 'docker-compose'
- 'docker.io'
- 'certbot'
- name: install certbot-nginx on ubuntu < 20
apt:
pkg:
- 'python-certbot-nginx'
when: ansible_distribution == 'Ubuntu' and ansible_distribution_version|version_compare('20.04', '<')
- name: install certbot-nginx on ubuntu > 20
apt:
pkg:
- 'python3-certbot-nginx'
when: ansible_distribution == 'Ubuntu' and ansible_distribution_version|version_compare('20.04', '>=')
- name: request initial letsencrypt certificate - name: request initial letsencrypt certificate
command: certbot certonly --nginx --agree-tos -d '{{ domain }}' -m '{{ letsencrypt_contact_email }}' command: certbot certonly --nginx --agree-tos -d '{{ domain }}' -m '{{ letsencrypt_contact_email }}'
@ -24,19 +47,34 @@
creates: '/etc/letsencrypt/live/{{domain}}/privkey.pem' creates: '/etc/letsencrypt/live/{{domain}}/privkey.pem'
- name: create lemmy folder - name: create lemmy folder
file: path={{item.path}} {{item.owner}} state=directory file:
path: '{{item.path}}'
owner: '{{item.owner}}'
state: directory
with_items: with_items:
- { path: '/lemmy/', owner: 'root' } - path: '{{lemmy_base_dir}}'
- { path: '/lemmy/volumes/', owner: 'root' } owner: 'root'
- { path: '/lemmy/volumes/pictrs/', owner: '991' } - path: '{{lemmy_base_dir}}/volumes/'
owner: 'root'
- path: '{{lemmy_base_dir}}/volumes/pictrs/'
owner: '991'
- block: - block:
- name: add template files - name: add template files
template: src={{item.src}} dest={{item.dest}} mode={{item.mode}} template:
src: '{{item.src}}'
dest: '{{item.dest}}'
mode: '{{item.mode}}'
with_items: with_items:
- { src: 'templates/docker-compose.yml', dest: '/lemmy/docker-compose.yml', mode: '0600' } - src: 'templates/docker-compose.yml'
- { src: 'templates/nginx.conf', dest: '/etc/nginx/sites-enabled/lemmy.conf', mode: '0644' } dest: '{{lemmy_base_dir}}/docker-compose.yml'
- { src: '../docker/iframely.config.local.js', dest: '/lemmy/iframely.config.local.js', mode: '0600' } mode: '0600'
- src: 'templates/nginx.conf'
dest: '/etc/nginx/sites-enabled/lemmy.conf'
mode: '0644'
- src: '../docker/iframely.config.local.js'
dest: '{{lemmy_base_dir}}/iframely.config.local.js'
mode: '0600'
vars: vars:
lemmy_docker_image: "dessalines/lemmy:{{ lookup('file', 'VERSION') }}" lemmy_docker_image: "dessalines/lemmy:{{ lookup('file', 'VERSION') }}"
lemmy_port: "8536" lemmy_port: "8536"
@ -44,7 +82,13 @@
iframely_port: "8538" iframely_port: "8538"
- name: add config file (only during initial setup) - name: add config file (only during initial setup)
template: src='templates/config.hjson' dest='/lemmy/lemmy.hjson' mode='0600' force='no' owner='1000' group='1000' template:
src: 'templates/config.hjson'
dest: '{{lemmy_base_dir}}/lemmy.hjson'
mode: '0600'
force: false
owner: '1000'
group: '1000'
vars: vars:
postgres_password: "{{ lookup('password', 'passwords/{{ inventory_hostname }}/postgres chars=ascii_letters,digits') }}" postgres_password: "{{ lookup('password', 'passwords/{{ inventory_hostname }}/postgres chars=ascii_letters,digits') }}"
jwt_password: "{{ lookup('password', 'passwords/{{ inventory_hostname }}/jwt chars=ascii_letters,digits') }}" jwt_password: "{{ lookup('password', 'passwords/{{ inventory_hostname }}/jwt chars=ascii_letters,digits') }}"
@ -57,7 +101,7 @@
- name: start docker-compose - name: start docker-compose
docker_compose: docker_compose:
project_src: /lemmy/ project_src: '{{lemmy_base_dir}}'
state: present state: present
pull: yes pull: yes
remove_orphans: yes remove_orphans: yes
@ -67,7 +111,7 @@
- name: certbot renewal cronjob - name: certbot renewal cronjob
cron: cron:
special_time=daily special_time: daily
name=certbot-renew-lemmy name: certbot-renew-lemmy
user=root user: root
job="certbot certonly --nginx -d '{{ domain }}' --deploy-hook 'nginx -s reload'" job: "certbot certonly --nginx -d '{{ domain }}' --deploy-hook 'nginx -s reload'"

80
ansible/lemmy_dev.yml vendored
View file

@ -1,24 +1,34 @@
--- ---
- hosts: all - hosts: all
vars: vars:
lemmy_docker_image: "lemmy:dev" lemmy_docker_image: 'lemmy:dev'
# Install python if required # Install python if required
# https://www.josharcher.uk/code/ansible-python-connection-failure-ubuntu-server-1604/ # https://www.josharcher.uk/code/ansible-python-connection-failure-ubuntu-server-1604/
gather_facts: False gather_facts: False
pre_tasks: pre_tasks:
- name: check lemmy_base_dir
fail:
msg: "`lemmy_base_dir` is unset. if you are upgrading from an older version, add `lemmy_base_dir=/lemmy` to your inventory file."
when: lemmy_base_dir is not defined
- name: install python for Ansible - name: install python for Ansible
raw: test -e /usr/bin/python || (apt -y update && apt install -y python-minimal python-setuptools) raw: test -e /usr/bin/python || (apt -y update && apt install -y python-minimal python-setuptools)
args: args:
executable: /bin/bash executable: /bin/bash
register: output register: output
changed_when: output.stdout != "" changed_when: output.stdout != ''
- setup: # gather facts - setup: # gather facts
tasks: tasks:
- name: install dependencies - name: install dependencies
apt: apt:
pkg: ['nginx', 'docker-compose', 'docker.io', 'certbot', 'python-certbot-nginx'] pkg:
- 'nginx'
- 'docker-compose'
- 'docker.io'
- 'certbot'
- 'python-certbot-nginx'
- name: request initial letsencrypt certificate - name: request initial letsencrypt certificate
command: certbot certonly --nginx --agree-tos -d '{{ domain }}' -m '{{ letsencrypt_contact_email }}' command: certbot certonly --nginx --agree-tos -d '{{ domain }}' -m '{{ letsencrypt_contact_email }}'
@ -26,22 +36,43 @@
creates: '/etc/letsencrypt/live/{{domain}}/privkey.pem' creates: '/etc/letsencrypt/live/{{domain}}/privkey.pem'
- name: create lemmy folder - name: create lemmy folder
file: path={{item.path}} owner={{item.owner}} state=directory file:
path: '{{item.path}}'
owner: '{{item.owner}}'
state: directory
with_items: with_items:
- { path: '/lemmy/', owner: 'root' } - path: '{{lemmy_base_dir}}/lemmy/'
- { path: '/lemmy/volumes/', owner: 'root' } owner: 'root'
- { path: '/lemmy/volumes/pictrs/', owner: '991' } - path: '{{lemmy_base_dir}}/volumes/'
owner: 'root'
- path: '{{lemmy_base_dir}}/volumes/pictrs/'
owner: '991'
- block: - block:
- name: add template files - name: add template files
template: src={{item.src}} dest={{item.dest}} mode={{item.mode}} template:
src: '{{item.src}}'
dest: '{{item.dest}}'
mode: '{{item.mode}}'
with_items: with_items:
- { src: 'templates/docker-compose.yml', dest: '/lemmy/docker-compose.yml', mode: '0600' } - src: 'templates/docker-compose.yml'
- { src: 'templates/nginx.conf', dest: '/etc/nginx/sites-enabled/lemmy.conf', mode: '0644' } dest: '{{lemmy_base_dir}}/docker-compose.yml'
- { src: '../docker/iframely.config.local.js', dest: '/lemmy/iframely.config.local.js', mode: '0600' } mode: '0600'
- src: 'templates/nginx.conf'
dest: '/etc/nginx/sites-enabled/lemmy.conf'
mode: '0644'
- src: '../docker/iframely.config.local.js'
dest: '{{lemmy_base_dir}}/iframely.config.local.js'
mode: '0600'
- name: add config file (only during initial setup) - name: add config file (only during initial setup)
template: src='templates/config.hjson' dest='/lemmy/lemmy.hjson' mode='0600' force='no' owner='1000' group='1000' template:
src: 'templates/config.hjson'
dest: '{{lemmy_base_dir}}/lemmy.hjson'
mode: '0600'
force: false
owner: '1000'
group: '1000'
vars: vars:
postgres_password: "{{ lookup('password', 'passwords/{{ inventory_hostname }}/postgres chars=ascii_letters,digits') }}" postgres_password: "{{ lookup('password', 'passwords/{{ inventory_hostname }}/postgres chars=ascii_letters,digits') }}"
jwt_password: "{{ lookup('password', 'passwords/{{ inventory_hostname }}/jwt chars=ascii_letters,digits') }}" jwt_password: "{{ lookup('password', 'passwords/{{ inventory_hostname }}/jwt chars=ascii_letters,digits') }}"
@ -59,22 +90,29 @@
local_action: shell sudo docker save lemmy:dev > lemmy-dev.tar local_action: shell sudo docker save lemmy:dev > lemmy-dev.tar
- name: copy dev docker image to server - name: copy dev docker image to server
copy: src=lemmy-dev.tar dest=/lemmy/lemmy-dev.tar copy:
src: lemmy-dev.tar
dest: '{{lemmy_base_dir}}/lemmy-dev.tar'
- name: import docker image - name: import docker image
docker_image: docker_image:
name: lemmy name: lemmy
tag: dev tag: dev
load_path: /lemmy/lemmy-dev.tar load_path: '{{lemmy_base_dir}}/lemmy-dev.tar'
source: load source: load
force_source: yes force_source: yes
register: image_import register: image_import
- name: delete remote image file - name: delete remote image file
file: path=/lemmy/lemmy-dev.tar state=absent file:
path: '{{lemmy_base_dir}}/lemmy-dev.tar'
state: absent
- name: delete local image file - name: delete local image file
local_action: file path=lemmy-dev.tar state=absent local_action:
module: file
path: lemmy-dev.tar
state: absent
- name: enable and start docker service - name: enable and start docker service
systemd: systemd:
@ -86,7 +124,7 @@
# be a problem for testing # be a problem for testing
- name: start docker-compose - name: start docker-compose
docker_compose: docker_compose:
project_src: /lemmy/ project_src: '{{lemmy_base_dir}}'
state: present state: present
recreate: always recreate: always
remove_orphans: yes remove_orphans: yes
@ -97,7 +135,7 @@
- name: certbot renewal cronjob - name: certbot renewal cronjob
cron: cron:
special_time=daily special_time: daily
name=certbot-renew-lemmy name: certbot-renew-lemmy
user=root user: root
job="certbot certonly --nginx -d '{{ domain }}' --deploy-hook 'nginx -s reload'" job: "certbot certonly --nginx -d '{{ domain }}' --deploy-hook 'nginx -s reload'"

View file

@ -35,7 +35,7 @@ services:
restart: always restart: always
iframely: iframely:
image: jolt/iframely:v1.4.3 image: dogbin/iframely:latest
ports: ports:
- "127.0.0.1:8061:80" - "127.0.0.1:8061:80"
volumes: volumes:

26
ansible/uninstall.yml vendored
View file

@ -22,27 +22,33 @@
- name: stop docker-compose - name: stop docker-compose
docker_compose: docker_compose:
project_src: /lemmy/ project_src: '{{lemmy_base_dir}}'
state: absent state: absent
- name: delete data - name: delete data
file: path={{item.path}} state=absent file:
path: '{{item.path}}'
state: absent
with_items: with_items:
- { path: '/lemmy/' } - path: '{{lemmy_base_dir}}'
- { path: '/etc/nginx/sites-enabled/lemmy.conf' } - path: '/etc/nginx/sites-enabled/lemmy.conf'
- name: Remove a volume - name: Remove a volume
docker_volume: name={{item.name}} state=absent docker_volume:
name: '{{item.name}}'
state: absent
with_items: with_items:
- { name: 'lemmy_lemmy_db' } - name: 'lemmy_lemmy_db'
- { name: 'lemmy_lemmy_pictshare' } - name: 'lemmy_lemmy_pictshare'
- name: delete entire ecloud folder - name: delete entire ecloud folder
file: path='/mnt/repo-base/' state=absent file:
path: '/mnt/repo-base/'
state: absent
when: delete_certs|bool when: delete_certs|bool
- name: remove certbot cronjob - name: remove certbot cronjob
cron: cron:
name=certbot-renew-lemmy name: certbot-renew-lemmy
state=absent state: absent

View file

@ -20,6 +20,8 @@ services:
postgres: postgres:
image: postgres:12-alpine image: postgres:12-alpine
ports:
- "127.0.0.1:5432:5432"
environment: environment:
- POSTGRES_USER=lemmy - POSTGRES_USER=lemmy
- POSTGRES_PASSWORD=password - POSTGRES_PASSWORD=password
@ -38,7 +40,7 @@ services:
restart: always restart: always
iframely: iframely:
image: jolt/iframely:v1.4.3 image: dogbin/iframely:latest
ports: ports:
- "127.0.0.1:8061:80" - "127.0.0.1:8061:80"
volumes: volumes:

View file

@ -5,17 +5,21 @@ pushd ../../server/
cargo build cargo build
popd popd
pushd ../../ui
yarn
popd
mkdir -p volumes/pictrs_{alpha,beta,gamma}
sudo chown -R 991:991 volumes/pictrs_{alpha,beta,gamma}
sudo docker build ../../ --file ../federation/Dockerfile --tag lemmy-federation:latest sudo docker build ../../ --file ../federation/Dockerfile --tag lemmy-federation:latest
for Item in alpha beta gamma ; do sudo mkdir -p volumes/pictrs_alpha
sudo mkdir -p volumes/pictrs_$Item sudo chown -R 991:991 volumes/pictrs_alpha
sudo chown -R 991:991 volumes/pictrs_$Item
done
sudo docker-compose --file ../federation/docker-compose.yml --project-directory . up -d sudo docker-compose --file ../federation/docker-compose.yml --project-directory . up -d
pushd ../../ui pushd ../../ui
yarn
echo "Waiting for Lemmy to start..." echo "Waiting for Lemmy to start..."
while [[ "$(curl -s -o /dev/null -w '%{http_code}' 'localhost:8540/api/v1/site')" != "200" ]]; do sleep 1; done while [[ "$(curl -s -o /dev/null -w '%{http_code}' 'localhost:8540/api/v1/site')" != "200" ]]; do sleep 1; done
while [[ "$(curl -s -o /dev/null -w '%{http_code}' 'localhost:8550/api/v1/site')" != "200" ]]; do sleep 1; done while [[ "$(curl -s -o /dev/null -w '%{http_code}' 'localhost:8550/api/v1/site')" != "200" ]]; do sleep 1; done

19
docker/federation-test/servers.sh vendored Executable file
View file

@ -0,0 +1,19 @@
#!/bin/bash
set -e
sudo rm -rf volumes
pushd ../../server/
cargo build
popd
pushd ../../ui
yarn
popd
mkdir -p volumes/pictrs_{alpha,beta,gamma}
sudo chown -R 991:991 volumes/pictrs_{alpha,beta,gamma}
sudo docker build ../../ --file ../federation/Dockerfile --tag lemmy-federation:latest
sudo docker-compose --file ../federation/docker-compose.yml --project-directory . up

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

@ -0,0 +1,10 @@
#!/bin/bash
set -xe
pushd ../../ui
echo "Waiting for Lemmy to start..."
while [[ "$(curl -s -o /dev/null -w '%{http_code}' 'localhost:8540/api/v1/site')" != "200" ]]; do sleep 1; done
while [[ "$(curl -s -o /dev/null -w '%{http_code}' 'localhost:8550/api/v1/site')" != "200" ]]; do sleep 1; done
while [[ "$(curl -s -o /dev/null -w '%{http_code}' 'localhost:8560/api/v1/site')" != "200" ]]; do sleep 1; done
yarn api-test || true
popd

View file

@ -3,7 +3,7 @@ FROM ekidd/rust-musl-builder:1.42.0-openssl11
USER root USER root
RUN mkdir /app/dist/documentation/ -p \ RUN mkdir /app/dist/documentation/ -p \
&& addgroup --gid 1001 lemmy \ && addgroup --gid 1001 lemmy \
&& adduser --disabled-password --shell /bin/sh -u 1001 --ingroup lemmy lemmy && adduser --gecos "" --disabled-password --shell /bin/sh -u 1001 --ingroup lemmy lemmy
# Copy resources # Copy resources
COPY server/config/defaults.hjson /app/config/defaults.hjson COPY server/config/defaults.hjson /app/config/defaults.hjson

View file

@ -12,28 +12,33 @@ services:
- ../federation/nginx.conf:/etc/nginx/nginx.conf - ../federation/nginx.conf:/etc/nginx/nginx.conf
restart: on-failure restart: on-failure
depends_on: depends_on:
- lemmy_alpha - lemmy-alpha
- pictrs_alpha - pictrs
- lemmy_beta - lemmy-beta
- pictrs_beta - lemmy-gamma
- lemmy_gamma
- pictrs_gamma
- iframely - iframely
lemmy_alpha: pictrs:
restart: always
image: asonix/pictrs:v0.1.13-r0
user: 991:991
volumes:
- ./volumes/pictrs_alpha:/mnt
lemmy-alpha:
image: lemmy-federation:latest image: lemmy-federation:latest
environment: environment:
- LEMMY_HOSTNAME=lemmy_alpha:8540 - LEMMY_HOSTNAME=lemmy-alpha:8540
- LEMMY_DATABASE_URL=postgres://lemmy:password@postgres_alpha:5432/lemmy - LEMMY_DATABASE_URL=postgres://lemmy:password@postgres_alpha:5432/lemmy
- LEMMY_JWT_SECRET=changeme - LEMMY_JWT_SECRET=changeme
- LEMMY_FRONT_END_DIR=/app/dist - LEMMY_FRONT_END_DIR=/app/dist
- LEMMY_FEDERATION__ENABLED=true - LEMMY_FEDERATION__ENABLED=true
- LEMMY_FEDERATION__TLS_ENABLED=false - LEMMY_FEDERATION__TLS_ENABLED=false
- LEMMY_FEDERATION__ALLOWED_INSTANCES=lemmy_beta,lemmy_gamma - LEMMY_FEDERATION__ALLOWED_INSTANCES=lemmy-beta,lemmy-gamma
- LEMMY_PORT=8540 - LEMMY_PORT=8540
- LEMMY_SETUP__ADMIN_USERNAME=lemmy_alpha - LEMMY_SETUP__ADMIN_USERNAME=lemmy_alpha
- LEMMY_SETUP__ADMIN_PASSWORD=lemmy - LEMMY_SETUP__ADMIN_PASSWORD=lemmy
- LEMMY_SETUP__SITE_NAME=lemmy_alpha - LEMMY_SETUP__SITE_NAME=lemmy-alpha
- RUST_BACKTRACE=1 - RUST_BACKTRACE=1
- RUST_LOG=debug - RUST_LOG=debug
depends_on: depends_on:
@ -46,26 +51,21 @@ services:
- POSTGRES_DB=lemmy - POSTGRES_DB=lemmy
volumes: volumes:
- ./volumes/postgres_alpha:/var/lib/postgresql/data - ./volumes/postgres_alpha:/var/lib/postgresql/data
pictrs_alpha:
image: asonix/pictrs:v0.1.13-r0
user: 991:991
volumes:
- ./volumes/pictrs_alpha:/mnt
lemmy_beta: lemmy-beta:
image: lemmy-federation:latest image: lemmy-federation:latest
environment: environment:
- LEMMY_HOSTNAME=lemmy_beta:8550 - LEMMY_HOSTNAME=lemmy-beta:8550
- LEMMY_DATABASE_URL=postgres://lemmy:password@postgres_beta:5432/lemmy - LEMMY_DATABASE_URL=postgres://lemmy:password@postgres_beta:5432/lemmy
- LEMMY_JWT_SECRET=changeme - LEMMY_JWT_SECRET=changeme
- LEMMY_FRONT_END_DIR=/app/dist - LEMMY_FRONT_END_DIR=/app/dist
- LEMMY_FEDERATION__ENABLED=true - LEMMY_FEDERATION__ENABLED=true
- LEMMY_FEDERATION__TLS_ENABLED=false - LEMMY_FEDERATION__TLS_ENABLED=false
- LEMMY_FEDERATION__ALLOWED_INSTANCES=lemmy_alpha,lemmy_gamma - LEMMY_FEDERATION__ALLOWED_INSTANCES=lemmy-alpha,lemmy-gamma
- LEMMY_PORT=8550 - LEMMY_PORT=8550
- LEMMY_SETUP__ADMIN_USERNAME=lemmy_beta - LEMMY_SETUP__ADMIN_USERNAME=lemmy_beta
- LEMMY_SETUP__ADMIN_PASSWORD=lemmy - LEMMY_SETUP__ADMIN_PASSWORD=lemmy
- LEMMY_SETUP__SITE_NAME=lemmy_beta - LEMMY_SETUP__SITE_NAME=lemmy-beta
- RUST_BACKTRACE=1 - RUST_BACKTRACE=1
- RUST_LOG=debug - RUST_LOG=debug
depends_on: depends_on:
@ -78,26 +78,21 @@ services:
- POSTGRES_DB=lemmy - POSTGRES_DB=lemmy
volumes: volumes:
- ./volumes/postgres_beta:/var/lib/postgresql/data - ./volumes/postgres_beta:/var/lib/postgresql/data
pictrs_beta:
image: asonix/pictrs:v0.1.13-r0
user: 991:991
volumes:
- ./volumes/pictrs_beta:/mnt
lemmy_gamma: lemmy-gamma:
image: lemmy-federation:latest image: lemmy-federation:latest
environment: environment:
- LEMMY_HOSTNAME=lemmy_gamma:8560 - LEMMY_HOSTNAME=lemmy-gamma:8560
- LEMMY_DATABASE_URL=postgres://lemmy:password@postgres_gamma:5432/lemmy - LEMMY_DATABASE_URL=postgres://lemmy:password@postgres_gamma:5432/lemmy
- LEMMY_JWT_SECRET=changeme - LEMMY_JWT_SECRET=changeme
- LEMMY_FRONT_END_DIR=/app/dist - LEMMY_FRONT_END_DIR=/app/dist
- LEMMY_FEDERATION__ENABLED=true - LEMMY_FEDERATION__ENABLED=true
- LEMMY_FEDERATION__TLS_ENABLED=false - LEMMY_FEDERATION__TLS_ENABLED=false
- LEMMY_FEDERATION__ALLOWED_INSTANCES=lemmy_alpha,lemmy_beta - LEMMY_FEDERATION__ALLOWED_INSTANCES=lemmy-alpha,lemmy-beta
- LEMMY_PORT=8560 - LEMMY_PORT=8560
- LEMMY_SETUP__ADMIN_USERNAME=lemmy_gamma - LEMMY_SETUP__ADMIN_USERNAME=lemmy_gamma
- LEMMY_SETUP__ADMIN_PASSWORD=lemmy - LEMMY_SETUP__ADMIN_PASSWORD=lemmy
- LEMMY_SETUP__SITE_NAME=lemmy_gamma - LEMMY_SETUP__SITE_NAME=lemmy-gamma
- RUST_BACKTRACE=1 - RUST_BACKTRACE=1
- RUST_LOG=debug - RUST_LOG=debug
depends_on: depends_on:
@ -110,13 +105,8 @@ services:
- POSTGRES_DB=lemmy - POSTGRES_DB=lemmy
volumes: volumes:
- ./volumes/postgres_gamma:/var/lib/postgresql/data - ./volumes/postgres_gamma:/var/lib/postgresql/data
pictrs_gamma:
image: asonix/pictrs:v0.1.13-r0
user: 991:991
volumes:
- ./volumes/pictrs_gamma:/mnt
iframely: iframely:
image: jolt/iframely:v1.4.3 image: dogbin/iframely:latest
volumes: volumes:
- ../iframely.config.local.js:/iframely/config.local.js:ro - ../iframely.config.local.js:/iframely/config.local.js:ro

View file

@ -12,7 +12,7 @@ http {
client_max_body_size 50M; client_max_body_size 50M;
location / { location / {
proxy_pass http://lemmy_alpha:8540; proxy_pass http://lemmy-alpha:8540;
proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Host $host; proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
@ -26,7 +26,7 @@ http {
# pict-rs images # pict-rs images
location /pictrs { location /pictrs {
location /pictrs/image { location /pictrs/image {
proxy_pass http://pictrs_alpha:8080/image; proxy_pass http://pictrs:8080/image;
proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Host $host; proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
@ -52,7 +52,7 @@ http {
client_max_body_size 50M; client_max_body_size 50M;
location / { location / {
proxy_pass http://lemmy_beta:8550; proxy_pass http://lemmy-beta:8550;
proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Host $host; proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
@ -66,7 +66,7 @@ http {
# pict-rs images # pict-rs images
location /pictrs { location /pictrs {
location /pictrs/image { location /pictrs/image {
proxy_pass http://pictrs_beta:8080/image; proxy_pass http://pictrs:8080/image;
proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Host $host; proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
@ -92,7 +92,7 @@ http {
client_max_body_size 50M; client_max_body_size 50M;
location / { location / {
proxy_pass http://lemmy_gamma:8560; proxy_pass http://lemmy-gamma:8560;
proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Host $host; proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
@ -106,7 +106,7 @@ http {
# pict-rs images # pict-rs images
location /pictrs { location /pictrs {
location /pictrs/image { location /pictrs/image {
proxy_pass http://pictrs_gamma:8080/image; proxy_pass http://pictrs:8080/image;
proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Host $host; proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

View file

@ -12,7 +12,7 @@ services:
restart: always restart: always
lemmy: lemmy:
image: dessalines/lemmy:v0.7.5 image: dessalines/lemmy:v0.7.8
ports: ports:
- "127.0.0.1:8536:8536" - "127.0.0.1:8536:8536"
restart: always restart: always
@ -35,7 +35,7 @@ services:
restart: always restart: always
iframely: iframely:
image: jolt/iframely:v1.4.3 image: dogbin/iframely:latest
ports: ports:
- "127.0.0.1:8061:80" - "127.0.0.1:8061:80"
volumes: volumes:

View file

@ -5,14 +5,7 @@
If you don't have a local clone of the Lemmy repo yet, just run the following command: If you don't have a local clone of the Lemmy repo yet, just run the following command:
```bash ```bash
git clone https://github.com/LemmyNet/lemmy -b federation git clone https://github.com/LemmyNet/lemmy
```
If you already have the Lemmy repo cloned, you need to add a new remote:
```bash
git remote add federation https://github.com/LemmyNet/lemmy
git checkout federation
git pull federation federation
``` ```
## Running locally ## Running locally
@ -26,18 +19,34 @@ You need to have the following packages installed, the Docker service needs to b
Then run the following Then run the following
```bash ```bash
cd dev/federation-test cd docker/federation
./run-federation-test.bash ./run-federation-test.bash -yarn
``` ```
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 The federation test sets up 3 instances:
[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`. Instance / Username | Location
--- | ---
lemmy_alpha | [127.0.0.1:8540](http://127.0.0.1:8540)
lemmy_beta | [127.0.0.1:8550](http://127.0.0.1:8550)
lemmy_gamma | [127.0.0.1:8560](http://127.0.0.1:8560)
You can log into each using the instance name, and `lemmy` as the password, IE (`lemmy_alpha`, `lemmy`).
Firefox containers are a good way to test them interacting.
## Integration tests
To run a suite of suite of federation integration tests:
```bash
cd docker/federation-test
./run-tests.sh
```
## Running on a server ## Running on a server
Note that federation is currently in alpha. Only use it for testing, not on any production server, and be aware Note that federation is currently in alpha. **Only use it for testing**, not on any production server, and be aware that turning on federation may break your instance.
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 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 [manually](administration_install_docker.md). Then replace the line `image: dessalines/lemmy:v0.x.x` in
@ -47,11 +56,12 @@ Follow the normal installation instructions, either with [Ansible](administratio
``` ```
federation: { federation: {
enabled: true enabled: true
allowed_instances: example.com tls_enabled: true,
allowed_instances: example.com,
} }
``` ```
Afterwards, and whenver you want to update to the latest version, run these commands on the server: Afterwards, and whenever you want to update to the latest version, run these commands on the server:
``` ```
cd /lemmy/ cd /lemmy/

569
server/Cargo.lock generated vendored

File diff suppressed because it is too large Load diff

15
server/Cargo.toml vendored
View file

@ -19,11 +19,12 @@ chrono = { version = "0.4.7", features = ["serde"] }
serde_json = { version = "1.0.52", features = ["preserve_order"]} serde_json = { version = "1.0.52", features = ["preserve_order"]}
failure = "0.1.8" failure = "0.1.8"
serde = { version = "1.0.105", features = ["derive"] } serde = { version = "1.0.105", features = ["derive"] }
actix = "0.9.0" actix = "0.10.0-alpha.2"
actix-web = "2.0.0" actix-web = { version = "3.0.0-alpha.3", features = ["rustls"] }
actix-files = "0.2.1" actix-files = "0.3.0-alpha.1"
actix-web-actors = "2.0.0" actix-web-actors = "3.0.0-alpha.1"
actix-rt = "1.1.1" actix-rt = "1.1.1"
awc = "2.0.0-alpha.2"
log = "0.4.0" log = "0.4.0"
env_logger = "0.7.1" env_logger = "0.7.1"
rand = "0.7.3" rand = "0.7.3"
@ -34,19 +35,19 @@ regex = "1.3.5"
lazy_static = "1.3.0" lazy_static = "1.3.0"
lettre = "0.9.3" lettre = "0.9.3"
lettre_email = "0.9.4" lettre_email = "0.9.4"
sha2 = "0.8.1"
rss = "1.9.0" rss = "1.9.0"
htmlescape = "0.3.1" htmlescape = "0.3.1"
url = { version = "2.1.1", features = ["serde"] } url = { version = "2.1.1", features = ["serde"] }
config = {version = "0.10.1", default-features = false, features = ["hjson"] } config = {version = "0.10.1", default-features = false, features = ["hjson"] }
percent-encoding = "2.1.0" percent-encoding = "2.1.0"
isahc = "0.9.2"
comrak = "0.7" comrak = "0.7"
openssl = "0.10" openssl = "0.10"
http = "0.2.1" http = "0.2.1"
http-signature-normalization = "0.5.1" http-signature-normalization-actix = { version = "0.4.0-alpha.1", default-features = false, features = ["sha-2"] }
base64 = "0.12.1" base64 = "0.12.1"
tokio = "0.2.21" tokio = "0.2.21"
futures = "0.3.5" futures = "0.3.5"
itertools = "0.9.0" itertools = "0.9.0"
uuid = { version = "0.8", features = ["serde", "v4"] } uuid = { version = "0.8", features = ["serde", "v4"] }
sha2 = "0.9"
async-trait = "0.1.36"

View file

@ -1,6 +1,7 @@
use crate::{ use crate::{
api::{APIError, Oper, Perform}, api::{APIError, Oper, Perform},
apub::{ApubLikeableType, ApubObjectType}, apub::{ApubLikeableType, ApubObjectType},
blocking,
db::{ db::{
comment::*, comment::*,
comment_view::*, comment_view::*,
@ -27,13 +28,10 @@ use crate::{
UserOperation, UserOperation,
WebsocketInfo, WebsocketInfo,
}, },
DbPool,
LemmyError,
MentionData, MentionData,
}; };
use diesel::{
r2d2::{ConnectionManager, Pool},
PgConnection,
};
use failure::Error;
use log::error; use log::error;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::str::FromStr; use std::str::FromStr;
@ -97,14 +95,15 @@ pub struct GetCommentsResponse {
comments: Vec<CommentView>, comments: Vec<CommentView>,
} }
#[async_trait::async_trait(?Send)]
impl Perform for Oper<CreateComment> { impl Perform for Oper<CreateComment> {
type Response = CommentResponse; type Response = CommentResponse;
fn perform( async fn perform(
&self, &self,
pool: Pool<ConnectionManager<PgConnection>>, pool: &DbPool,
websocket_info: Option<WebsocketInfo>, websocket_info: Option<WebsocketInfo>,
) -> Result<CommentResponse, Error> { ) -> Result<CommentResponse, LemmyError> {
let data: &CreateComment = &self.data; let data: &CreateComment = &self.data;
let claims = match Claims::decode(&data.auth) { let claims = match Claims::decode(&data.auth) {
@ -114,20 +113,6 @@ impl Perform for Oper<CreateComment> {
let user_id = claims.id; let user_id = claims.id;
let conn = pool.get()?;
// Check for a community ban
let post = Post::read(&conn, data.post_id)?;
if CommunityUserBanView::get(&conn, user_id, post.community_id).is_ok() {
return Err(APIError::err("community_ban").into());
}
// Check for a site ban
let user = User_::read(&conn, user_id)?;
if user.banned {
return Err(APIError::err("site_ban").into());
}
let content_slurs_removed = remove_slurs(&data.content.to_owned()); let content_slurs_removed = remove_slurs(&data.content.to_owned());
let comment_form = CommentForm { let comment_form = CommentForm {
@ -144,21 +129,48 @@ impl Perform for Oper<CreateComment> {
local: true, local: true,
}; };
let inserted_comment = match Comment::create(&conn, &comment_form) { // Check for a community ban
let post_id = data.post_id;
let post = blocking(pool, move |conn| Post::read(conn, post_id)).await??;
let community_id = post.community_id;
let is_banned =
move |conn: &'_ _| CommunityUserBanView::get(conn, user_id, community_id).is_ok();
if blocking(pool, is_banned).await? {
return Err(APIError::err("community_ban").into());
}
// Check for a site ban
let user = blocking(pool, move |conn| User_::read(&conn, user_id)).await??;
if user.banned {
return Err(APIError::err("site_ban").into());
}
let comment_form2 = comment_form.clone();
let inserted_comment =
match blocking(pool, move |conn| Comment::create(&conn, &comment_form2)).await? {
Ok(comment) => comment, Ok(comment) => comment,
Err(_e) => return Err(APIError::err("couldnt_create_comment").into()), Err(_e) => return Err(APIError::err("couldnt_create_comment").into()),
}; };
let updated_comment = match Comment::update_ap_id(&conn, inserted_comment.id) { let inserted_comment_id = inserted_comment.id;
let updated_comment: Comment = match blocking(pool, move |conn| {
Comment::update_ap_id(&conn, inserted_comment_id)
})
.await?
{
Ok(comment) => comment, Ok(comment) => comment,
Err(_e) => return Err(APIError::err("couldnt_create_comment").into()), Err(_e) => return Err(APIError::err("couldnt_create_comment").into()),
}; };
updated_comment.send_create(&user, &conn)?; updated_comment
.send_create(&user, &self.client, pool)
.await?;
// Scan the comment for user mentions, add those rows // Scan the comment for user mentions, add those rows
let mentions = scrape_text_for_mentions(&comment_form.content); let mentions = scrape_text_for_mentions(&comment_form.content);
let recipient_ids = send_local_notifs(&conn, &mentions, &updated_comment, &user, &post); let recipient_ids =
send_local_notifs(mentions, updated_comment.clone(), user.clone(), post, pool).await?;
// You like your own comment by default // You like your own comment by default
let like_form = CommentLikeForm { let like_form = CommentLikeForm {
@ -168,14 +180,17 @@ impl Perform for Oper<CreateComment> {
score: 1, score: 1,
}; };
let _inserted_like = match CommentLike::like(&conn, &like_form) { let like = move |conn: &'_ _| CommentLike::like(&conn, &like_form);
Ok(like) => like, if blocking(pool, like).await?.is_err() {
Err(_e) => return Err(APIError::err("couldnt_like_comment").into()), return Err(APIError::err("couldnt_like_comment").into());
}; }
updated_comment.send_like(&user, &conn)?; updated_comment.send_like(&user, &self.client, pool).await?;
let comment_view = CommentView::read(&conn, inserted_comment.id, Some(user_id))?; let comment_view = blocking(pool, move |conn| {
CommentView::read(&conn, inserted_comment.id, Some(user_id))
})
.await??;
let mut res = CommentResponse { let mut res = CommentResponse {
comment: comment_view, comment: comment_view,
@ -198,14 +213,15 @@ impl Perform for Oper<CreateComment> {
} }
} }
#[async_trait::async_trait(?Send)]
impl Perform for Oper<EditComment> { impl Perform for Oper<EditComment> {
type Response = CommentResponse; type Response = CommentResponse;
fn perform( async fn perform(
&self, &self,
pool: Pool<ConnectionManager<PgConnection>>, pool: &DbPool,
websocket_info: Option<WebsocketInfo>, websocket_info: Option<WebsocketInfo>,
) -> Result<CommentResponse, Error> { ) -> Result<CommentResponse, LemmyError> {
let data: &EditComment = &self.data; let data: &EditComment = &self.data;
let claims = match Claims::decode(&data.auth) { let claims = match Claims::decode(&data.auth) {
@ -215,30 +231,44 @@ impl Perform for Oper<EditComment> {
let user_id = claims.id; let user_id = claims.id;
let conn = pool.get()?; let user = blocking(pool, move |conn| User_::read(&conn, user_id)).await??;
let user = User_::read(&conn, user_id)?; let edit_id = data.edit_id;
let orig_comment =
let orig_comment = CommentView::read(&conn, data.edit_id, None)?; blocking(pool, move |conn| CommentView::read(&conn, edit_id, None)).await??;
// You are allowed to mark the comment as read even if you're banned. // You are allowed to mark the comment as read even if you're banned.
if data.read.is_none() { if data.read.is_none() {
// Verify its the creator or a mod, or an admin // Verify its the creator or a mod, or an admin
let mut editors: Vec<i32> = vec![data.creator_id]; let mut editors: Vec<i32> = vec![data.creator_id];
let community_id = orig_comment.community_id;
editors.append( editors.append(
&mut CommunityModeratorView::for_community(&conn, orig_comment.community_id)? &mut blocking(pool, move |conn| {
Ok(
CommunityModeratorView::for_community(&conn, community_id)?
.into_iter() .into_iter()
.map(|m| m.user_id) .map(|m| m.user_id)
.collect(), .collect(),
) as Result<_, LemmyError>
})
.await??,
);
editors.append(
&mut blocking(pool, move |conn| {
Ok(UserView::admins(conn)?.into_iter().map(|a| a.id).collect()) as Result<_, LemmyError>
})
.await??,
); );
editors.append(&mut UserView::admins(&conn)?.into_iter().map(|a| a.id).collect());
if !editors.contains(&user_id) { if !editors.contains(&user_id) {
return Err(APIError::err("no_comment_edit_allowed").into()); return Err(APIError::err("no_comment_edit_allowed").into());
} }
// Check for a community ban // Check for a community ban
if CommunityUserBanView::get(&conn, user_id, orig_comment.community_id).is_ok() { let community_id = orig_comment.community_id;
let is_banned =
move |conn: &'_ _| CommunityUserBanView::get(conn, user_id, community_id).is_ok();
if blocking(pool, is_banned).await? {
return Err(APIError::err("community_ban").into()); return Err(APIError::err("community_ban").into());
} }
@ -250,7 +280,8 @@ impl Perform for Oper<EditComment> {
let content_slurs_removed = remove_slurs(&data.content.to_owned()); let content_slurs_removed = remove_slurs(&data.content.to_owned());
let read_comment = Comment::read(&conn, data.edit_id)?; let edit_id = data.edit_id;
let read_comment = blocking(pool, move |conn| Comment::read(conn, edit_id)).await??;
let comment_form = CommentForm { let comment_form = CommentForm {
content: content_slurs_removed, content: content_slurs_removed,
@ -270,31 +301,48 @@ impl Perform for Oper<EditComment> {
local: read_comment.local, local: read_comment.local,
}; };
let updated_comment = match Comment::update(&conn, data.edit_id, &comment_form) { let edit_id = data.edit_id;
let comment_form2 = comment_form.clone();
let updated_comment = match blocking(pool, move |conn| {
Comment::update(conn, edit_id, &comment_form2)
})
.await?
{
Ok(comment) => comment, Ok(comment) => comment,
Err(_e) => return Err(APIError::err("couldnt_update_comment").into()), Err(_e) => return Err(APIError::err("couldnt_update_comment").into()),
}; };
if let Some(deleted) = data.deleted.to_owned() { if let Some(deleted) = data.deleted.to_owned() {
if deleted { if deleted {
updated_comment.send_delete(&user, &conn)?; updated_comment
.send_delete(&user, &self.client, pool)
.await?;
} else { } else {
updated_comment.send_undo_delete(&user, &conn)?; updated_comment
.send_undo_delete(&user, &self.client, pool)
.await?;
} }
} else if let Some(removed) = data.removed.to_owned() { } else if let Some(removed) = data.removed.to_owned() {
if removed { if removed {
updated_comment.send_remove(&user, &conn)?; updated_comment
.send_remove(&user, &self.client, pool)
.await?;
} else { } else {
updated_comment.send_undo_remove(&user, &conn)?; updated_comment
.send_undo_remove(&user, &self.client, pool)
.await?;
} }
} else { } else {
updated_comment.send_update(&user, &conn)?; updated_comment
.send_update(&user, &self.client, pool)
.await?;
} }
let post = Post::read(&conn, data.post_id)?; let post_id = data.post_id;
let post = blocking(pool, move |conn| Post::read(conn, post_id)).await??;
let mentions = scrape_text_for_mentions(&comment_form.content); let mentions = scrape_text_for_mentions(&comment_form.content);
let recipient_ids = send_local_notifs(&conn, &mentions, &updated_comment, &user, &post); let recipient_ids = send_local_notifs(mentions, updated_comment, user, post, pool).await?;
// Mod tables // Mod tables
if let Some(removed) = data.removed.to_owned() { if let Some(removed) = data.removed.to_owned() {
@ -304,10 +352,14 @@ impl Perform for Oper<EditComment> {
removed: Some(removed), removed: Some(removed),
reason: data.reason.to_owned(), reason: data.reason.to_owned(),
}; };
ModRemoveComment::create(&conn, &form)?; blocking(pool, move |conn| ModRemoveComment::create(conn, &form)).await??;
} }
let comment_view = CommentView::read(&conn, data.edit_id, Some(user_id))?; let edit_id = data.edit_id;
let comment_view = blocking(pool, move |conn| {
CommentView::read(conn, edit_id, Some(user_id))
})
.await??;
let mut res = CommentResponse { let mut res = CommentResponse {
comment: comment_view, comment: comment_view,
@ -330,14 +382,15 @@ impl Perform for Oper<EditComment> {
} }
} }
#[async_trait::async_trait(?Send)]
impl Perform for Oper<SaveComment> { impl Perform for Oper<SaveComment> {
type Response = CommentResponse; type Response = CommentResponse;
fn perform( async fn perform(
&self, &self,
pool: Pool<ConnectionManager<PgConnection>>, pool: &DbPool,
_websocket_info: Option<WebsocketInfo>, _websocket_info: Option<WebsocketInfo>,
) -> Result<CommentResponse, Error> { ) -> Result<CommentResponse, LemmyError> {
let data: &SaveComment = &self.data; let data: &SaveComment = &self.data;
let claims = match Claims::decode(&data.auth) { let claims = match Claims::decode(&data.auth) {
@ -352,21 +405,23 @@ impl Perform for Oper<SaveComment> {
user_id, user_id,
}; };
let conn = pool.get()?;
if data.save { if data.save {
match CommentSaved::save(&conn, &comment_saved_form) { let save_comment = move |conn: &'_ _| CommentSaved::save(conn, &comment_saved_form);
Ok(comment) => comment, if blocking(pool, save_comment).await?.is_err() {
Err(_e) => return Err(APIError::err("couldnt_save_comment").into()), return Err(APIError::err("couldnt_save_comment").into());
}; }
} else { } else {
match CommentSaved::unsave(&conn, &comment_saved_form) { let unsave_comment = move |conn: &'_ _| CommentSaved::unsave(conn, &comment_saved_form);
Ok(comment) => comment, if blocking(pool, unsave_comment).await?.is_err() {
Err(_e) => return Err(APIError::err("couldnt_save_comment").into()), return Err(APIError::err("couldnt_save_comment").into());
}; }
} }
let comment_view = CommentView::read(&conn, data.comment_id, Some(user_id))?; let comment_id = data.comment_id;
let comment_view = blocking(pool, move |conn| {
CommentView::read(conn, comment_id, Some(user_id))
})
.await??;
Ok(CommentResponse { Ok(CommentResponse {
comment: comment_view, comment: comment_view,
@ -375,14 +430,15 @@ impl Perform for Oper<SaveComment> {
} }
} }
#[async_trait::async_trait(?Send)]
impl Perform for Oper<CreateCommentLike> { impl Perform for Oper<CreateCommentLike> {
type Response = CommentResponse; type Response = CommentResponse;
fn perform( async fn perform(
&self, &self,
pool: Pool<ConnectionManager<PgConnection>>, pool: &DbPool,
websocket_info: Option<WebsocketInfo>, websocket_info: Option<WebsocketInfo>,
) -> Result<CommentResponse, Error> { ) -> Result<CommentResponse, LemmyError> {
let data: &CreateCommentLike = &self.data; let data: &CreateCommentLike = &self.data;
let claims = match Claims::decode(&data.auth) { let claims = match Claims::decode(&data.auth) {
@ -394,36 +450,42 @@ impl Perform for Oper<CreateCommentLike> {
let mut recipient_ids = Vec::new(); let mut recipient_ids = Vec::new();
let conn = pool.get()?;
// Don't do a downvote if site has downvotes disabled // Don't do a downvote if site has downvotes disabled
if data.score == -1 { if data.score == -1 {
let site = SiteView::read(&conn)?; let site = blocking(pool, move |conn| SiteView::read(conn)).await??;
if !site.enable_downvotes { if !site.enable_downvotes {
return Err(APIError::err("downvotes_disabled").into()); return Err(APIError::err("downvotes_disabled").into());
} }
} }
// Check for a community ban // Check for a community ban
let post = Post::read(&conn, data.post_id)?; let post_id = data.post_id;
if CommunityUserBanView::get(&conn, user_id, post.community_id).is_ok() { let post = blocking(pool, move |conn| Post::read(conn, post_id)).await??;
let community_id = post.community_id;
let is_banned =
move |conn: &'_ _| CommunityUserBanView::get(conn, user_id, community_id).is_ok();
if blocking(pool, is_banned).await? {
return Err(APIError::err("community_ban").into()); return Err(APIError::err("community_ban").into());
} }
// Check for a site ban // Check for a site ban
let user = User_::read(&conn, user_id)?; let user = blocking(pool, move |conn| User_::read(conn, user_id)).await??;
if user.banned { if user.banned {
return Err(APIError::err("site_ban").into()); return Err(APIError::err("site_ban").into());
} }
let comment = Comment::read(&conn, data.comment_id)?; let comment_id = data.comment_id;
let comment = blocking(pool, move |conn| Comment::read(conn, comment_id)).await??;
// Add to recipient ids // Add to recipient ids
match comment.parent_id { match comment.parent_id {
Some(parent_id) => { Some(parent_id) => {
let parent_comment = Comment::read(&conn, parent_id)?; let parent_comment = blocking(pool, move |conn| Comment::read(conn, parent_id)).await??;
if parent_comment.creator_id != user_id { if parent_comment.creator_id != user_id {
let parent_user = User_::read(&conn, parent_comment.creator_id)?; let parent_user = blocking(pool, move |conn| {
User_::read(conn, parent_comment.creator_id)
})
.await??;
recipient_ids.push(parent_user.id); recipient_ids.push(parent_user.id);
} }
} }
@ -440,27 +502,33 @@ impl Perform for Oper<CreateCommentLike> {
}; };
// Remove any likes first // Remove any likes first
CommentLike::remove(&conn, &like_form)?; let like_form2 = like_form.clone();
blocking(pool, move |conn| CommentLike::remove(conn, &like_form2)).await??;
// Only add the like if the score isnt 0 // Only add the like if the score isnt 0
let do_add = like_form.score != 0 && (like_form.score == 1 || like_form.score == -1); let do_add = like_form.score != 0 && (like_form.score == 1 || like_form.score == -1);
if do_add { if do_add {
let _inserted_like = match CommentLike::like(&conn, &like_form) { let like_form2 = like_form.clone();
Ok(like) => like, let like = move |conn: &'_ _| CommentLike::like(conn, &like_form2);
Err(_e) => return Err(APIError::err("couldnt_like_comment").into()), if blocking(pool, like).await?.is_err() {
}; return Err(APIError::err("couldnt_like_comment").into());
}
if like_form.score == 1 { if like_form.score == 1 {
comment.send_like(&user, &conn)?; comment.send_like(&user, &self.client, pool).await?;
} else if like_form.score == -1 { } else if like_form.score == -1 {
comment.send_dislike(&user, &conn)?; comment.send_dislike(&user, &self.client, pool).await?;
} }
} else { } else {
comment.send_undo_like(&user, &conn)?; comment.send_undo_like(&user, &self.client, pool).await?;
} }
// Have to refetch the comment to get the current state // Have to refetch the comment to get the current state
let liked_comment = CommentView::read(&conn, data.comment_id, Some(user_id))?; let comment_id = data.comment_id;
let liked_comment = blocking(pool, move |conn| {
CommentView::read(conn, comment_id, Some(user_id))
})
.await??;
let mut res = CommentResponse { let mut res = CommentResponse {
comment: liked_comment, comment: liked_comment,
@ -483,14 +551,15 @@ impl Perform for Oper<CreateCommentLike> {
} }
} }
#[async_trait::async_trait(?Send)]
impl Perform for Oper<GetComments> { impl Perform for Oper<GetComments> {
type Response = GetCommentsResponse; type Response = GetCommentsResponse;
fn perform( async fn perform(
&self, &self,
pool: Pool<ConnectionManager<PgConnection>>, pool: &DbPool,
websocket_info: Option<WebsocketInfo>, websocket_info: Option<WebsocketInfo>,
) -> Result<GetCommentsResponse, Error> { ) -> Result<GetCommentsResponse, LemmyError> {
let data: &GetComments = &self.data; let data: &GetComments = &self.data;
let user_claims: Option<Claims> = match &data.auth { let user_claims: Option<Claims> = match &data.auth {
@ -509,19 +578,23 @@ impl Perform for Oper<GetComments> {
let type_ = ListingType::from_str(&data.type_)?; let type_ = ListingType::from_str(&data.type_)?;
let sort = SortType::from_str(&data.sort)?; let sort = SortType::from_str(&data.sort)?;
let conn = pool.get()?; let community_id = data.community_id;
let page = data.page;
let comments = match CommentQueryBuilder::create(&conn) let limit = data.limit;
let comments = blocking(pool, move |conn| {
CommentQueryBuilder::create(conn)
.listing_type(type_) .listing_type(type_)
.sort(&sort) .sort(&sort)
.for_community_id(data.community_id) .for_community_id(community_id)
.my_user_id(user_id) .my_user_id(user_id)
.page(data.page) .page(page)
.limit(data.limit) .limit(limit)
.list() .list()
{ })
.await?;
let comments = match comments {
Ok(comments) => comments, Ok(comments) => comments,
Err(_e) => return Err(APIError::err("couldnt_get_comments").into()), Err(_) => return Err(APIError::err("couldnt_get_comments").into()),
}; };
if let Some(ws) = websocket_info { if let Some(ws) = websocket_info {
@ -542,8 +615,23 @@ impl Perform for Oper<GetComments> {
} }
} }
pub fn send_local_notifs( pub async fn send_local_notifs(
conn: &PgConnection, mentions: Vec<MentionData>,
comment: Comment,
user: User_,
post: Post,
pool: &DbPool,
) -> Result<Vec<i32>, LemmyError> {
let ids = blocking(pool, move |conn| {
do_send_local_notifs(conn, &mentions, &comment, &user, &post)
})
.await?;
Ok(ids)
}
fn do_send_local_notifs(
conn: &diesel::PgConnection,
mentions: &[MentionData], mentions: &[MentionData],
comment: &Comment, comment: &Comment,
user: &User_, user: &User_,

View file

@ -7,6 +7,7 @@ use crate::{
ActorType, ActorType,
EndpointType, EndpointType,
}, },
blocking,
db::{Bannable, Crud, Followable, Joinable, SortType}, db::{Bannable, Crud, Followable, Joinable, SortType},
is_valid_community_name, is_valid_community_name,
naive_from_unix, naive_from_unix,
@ -18,12 +19,9 @@ use crate::{
UserOperation, UserOperation,
WebsocketInfo, WebsocketInfo,
}, },
DbPool,
LemmyError,
}; };
use diesel::{
r2d2::{ConnectionManager, Pool},
PgConnection,
};
use failure::Error;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::str::FromStr; use std::str::FromStr;
@ -138,14 +136,15 @@ pub struct TransferCommunity {
auth: String, auth: String,
} }
#[async_trait::async_trait(?Send)]
impl Perform for Oper<GetCommunity> { impl Perform for Oper<GetCommunity> {
type Response = GetCommunityResponse; type Response = GetCommunityResponse;
fn perform( async fn perform(
&self, &self,
pool: Pool<ConnectionManager<PgConnection>>, pool: &DbPool,
websocket_info: Option<WebsocketInfo>, websocket_info: Option<WebsocketInfo>,
) -> Result<GetCommunityResponse, Error> { ) -> Result<GetCommunityResponse, LemmyError> {
let data: &GetCommunity = &self.data; let data: &GetCommunity = &self.data;
let user_id: Option<i32> = match &data.auth { let user_id: Option<i32> = match &data.auth {
@ -159,33 +158,38 @@ impl Perform for Oper<GetCommunity> {
None => None, None => None,
}; };
let conn = pool.get()?; let name = data.name.to_owned().unwrap_or_else(|| "main".to_string());
let community = match data.id { let community = match data.id {
Some(id) => Community::read(&conn, id)?, Some(id) => blocking(pool, move |conn| Community::read(conn, id)).await??,
None => { None => match blocking(pool, move |conn| Community::read_from_name(conn, &name)).await? {
match Community::read_from_name(
&conn,
&data.name.to_owned().unwrap_or_else(|| "main".to_string()),
) {
Ok(community) => community, Ok(community) => community,
Err(_e) => return Err(APIError::err("couldnt_find_community").into()), Err(_e) => return Err(APIError::err("couldnt_find_community").into()),
} },
}
}; };
let community_view = match CommunityView::read(&conn, community.id, user_id) { let community_id = community.id;
let community_view = match blocking(pool, move |conn| {
CommunityView::read(conn, community_id, user_id)
})
.await?
{
Ok(community) => community, Ok(community) => community,
Err(_e) => return Err(APIError::err("couldnt_find_community").into()), Err(_e) => return Err(APIError::err("couldnt_find_community").into()),
}; };
let moderators = match CommunityModeratorView::for_community(&conn, community.id) { let community_id = community.id;
let moderators: Vec<CommunityModeratorView> = match blocking(pool, move |conn| {
CommunityModeratorView::for_community(conn, community_id)
})
.await?
{
Ok(moderators) => moderators, Ok(moderators) => moderators,
Err(_e) => return Err(APIError::err("couldnt_find_community").into()), Err(_e) => return Err(APIError::err("couldnt_find_community").into()),
}; };
let site_creator_id = Site::read(&conn, 1)?.creator_id; let site = blocking(pool, move |conn| Site::read(conn, 1)).await??;
let mut admins = UserView::admins(&conn)?; let site_creator_id = site.creator_id;
let mut admins = blocking(pool, move |conn| UserView::admins(conn)).await??;
let creator_index = admins.iter().position(|r| r.id == site_creator_id).unwrap(); let creator_index = admins.iter().position(|r| r.id == site_creator_id).unwrap();
let creator_user = admins.remove(creator_index); let creator_user = admins.remove(creator_index);
admins.insert(0, creator_user); admins.insert(0, creator_user);
@ -220,14 +224,15 @@ impl Perform for Oper<GetCommunity> {
} }
} }
#[async_trait::async_trait(?Send)]
impl Perform for Oper<CreateCommunity> { impl Perform for Oper<CreateCommunity> {
type Response = CommunityResponse; type Response = CommunityResponse;
fn perform( async fn perform(
&self, &self,
pool: Pool<ConnectionManager<PgConnection>>, pool: &DbPool,
_websocket_info: Option<WebsocketInfo>, _websocket_info: Option<WebsocketInfo>,
) -> Result<CommunityResponse, Error> { ) -> Result<CommunityResponse, LemmyError> {
let data: &CreateCommunity = &self.data; let data: &CreateCommunity = &self.data;
let claims = match Claims::decode(&data.auth) { let claims = match Claims::decode(&data.auth) {
@ -255,10 +260,9 @@ impl Perform for Oper<CreateCommunity> {
let user_id = claims.id; let user_id = claims.id;
let conn = pool.get()?;
// Check for a site ban // Check for a site ban
if UserView::read(&conn, user_id)?.banned { let user_view = blocking(pool, move |conn| UserView::read(conn, user_id)).await??;
if user_view.banned {
return Err(APIError::err("site_ban").into()); return Err(APIError::err("site_ban").into());
} }
@ -283,7 +287,8 @@ impl Perform for Oper<CreateCommunity> {
published: None, published: None,
}; };
let inserted_community = match Community::create(&conn, &community_form) { let inserted_community =
match blocking(pool, move |conn| Community::create(conn, &community_form)).await? {
Ok(community) => community, Ok(community) => community,
Err(_e) => return Err(APIError::err("community_already_exists").into()), Err(_e) => return Err(APIError::err("community_already_exists").into()),
}; };
@ -293,24 +298,25 @@ impl Perform for Oper<CreateCommunity> {
user_id, user_id,
}; };
let _inserted_community_moderator = let join = move |conn: &'_ _| CommunityModerator::join(conn, &community_moderator_form);
match CommunityModerator::join(&conn, &community_moderator_form) { if blocking(pool, join).await?.is_err() {
Ok(user) => user, return Err(APIError::err("community_moderator_already_exists").into());
Err(_e) => return Err(APIError::err("community_moderator_already_exists").into()), }
};
let community_follower_form = CommunityFollowerForm { let community_follower_form = CommunityFollowerForm {
community_id: inserted_community.id, community_id: inserted_community.id,
user_id, user_id,
}; };
let _inserted_community_follower = let follow = move |conn: &'_ _| CommunityFollower::follow(conn, &community_follower_form);
match CommunityFollower::follow(&conn, &community_follower_form) { if blocking(pool, follow).await?.is_err() {
Ok(user) => user, return Err(APIError::err("community_follower_already_exists").into());
Err(_e) => return Err(APIError::err("community_follower_already_exists").into()), }
};
let community_view = CommunityView::read(&conn, inserted_community.id, Some(user_id))?; let community_view = blocking(pool, move |conn| {
CommunityView::read(conn, inserted_community.id, Some(user_id))
})
.await??;
Ok(CommunityResponse { Ok(CommunityResponse {
community: community_view, community: community_view,
@ -318,14 +324,15 @@ impl Perform for Oper<CreateCommunity> {
} }
} }
#[async_trait::async_trait(?Send)]
impl Perform for Oper<EditCommunity> { impl Perform for Oper<EditCommunity> {
type Response = CommunityResponse; type Response = CommunityResponse;
fn perform( async fn perform(
&self, &self,
pool: Pool<ConnectionManager<PgConnection>>, pool: &DbPool,
websocket_info: Option<WebsocketInfo>, websocket_info: Option<WebsocketInfo>,
) -> Result<CommunityResponse, Error> { ) -> Result<CommunityResponse, LemmyError> {
let data: &EditCommunity = &self.data; let data: &EditCommunity = &self.data;
if let Err(slurs) = slur_check(&data.name) { if let Err(slurs) = slur_check(&data.name) {
@ -353,28 +360,34 @@ impl Perform for Oper<EditCommunity> {
let user_id = claims.id; let user_id = claims.id;
let conn = pool.get()?;
// Check for a site ban // Check for a site ban
let user = User_::read(&conn, user_id)?; let user = blocking(pool, move |conn| User_::read(conn, user_id)).await??;
if user.banned { if user.banned {
return Err(APIError::err("site_ban").into()); return Err(APIError::err("site_ban").into());
} }
// Verify its a mod // Verify its a mod
let edit_id = data.edit_id;
let mut editors: Vec<i32> = Vec::new(); let mut editors: Vec<i32> = Vec::new();
editors.append( editors.append(
&mut CommunityModeratorView::for_community(&conn, data.edit_id)? &mut blocking(pool, move |conn| {
.into_iter() CommunityModeratorView::for_community(conn, edit_id)
.map(|m| m.user_id) .map(|v| v.into_iter().map(|m| m.user_id).collect())
.collect(), })
.await??,
);
editors.append(
&mut blocking(pool, move |conn| {
UserView::admins(conn).map(|v| v.into_iter().map(|a| a.id).collect())
})
.await??,
); );
editors.append(&mut UserView::admins(&conn)?.into_iter().map(|a| a.id).collect());
if !editors.contains(&user_id) { if !editors.contains(&user_id) {
return Err(APIError::err("no_community_edit_allowed").into()); return Err(APIError::err("no_community_edit_allowed").into());
} }
let read_community = Community::read(&conn, data.edit_id)?; let edit_id = data.edit_id;
let read_community = blocking(pool, move |conn| Community::read(conn, edit_id)).await??;
let community_form = CommunityForm { let community_form = CommunityForm {
name: data.name.to_owned(), name: data.name.to_owned(),
@ -394,7 +407,12 @@ impl Perform for Oper<EditCommunity> {
published: None, published: None,
}; };
let updated_community = match Community::update(&conn, data.edit_id, &community_form) { let edit_id = data.edit_id;
let updated_community = match blocking(pool, move |conn| {
Community::update(conn, edit_id, &community_form)
})
.await?
{
Ok(community) => community, Ok(community) => community,
Err(_e) => return Err(APIError::err("couldnt_update_community").into()), Err(_e) => return Err(APIError::err("couldnt_update_community").into()),
}; };
@ -412,24 +430,36 @@ impl Perform for Oper<EditCommunity> {
reason: data.reason.to_owned(), reason: data.reason.to_owned(),
expires, expires,
}; };
ModRemoveCommunity::create(&conn, &form)?; blocking(pool, move |conn| ModRemoveCommunity::create(conn, &form)).await??;
} }
if let Some(deleted) = data.deleted.to_owned() { if let Some(deleted) = data.deleted.to_owned() {
if deleted { if deleted {
updated_community.send_delete(&user, &conn)?; updated_community
.send_delete(&user, &self.client, pool)
.await?;
} else { } else {
updated_community.send_undo_delete(&user, &conn)?; updated_community
.send_undo_delete(&user, &self.client, pool)
.await?;
} }
} else if let Some(removed) = data.removed.to_owned() { } else if let Some(removed) = data.removed.to_owned() {
if removed { if removed {
updated_community.send_remove(&user, &conn)?; updated_community
.send_remove(&user, &self.client, pool)
.await?;
} else { } else {
updated_community.send_undo_remove(&user, &conn)?; updated_community
.send_undo_remove(&user, &self.client, pool)
.await?;
} }
} }
let community_view = CommunityView::read(&conn, data.edit_id, Some(user_id))?; let edit_id = data.edit_id;
let community_view = blocking(pool, move |conn| {
CommunityView::read(conn, edit_id, Some(user_id))
})
.await??;
let res = CommunityResponse { let res = CommunityResponse {
community: community_view, community: community_view,
@ -453,14 +483,15 @@ impl Perform for Oper<EditCommunity> {
} }
} }
#[async_trait::async_trait(?Send)]
impl Perform for Oper<ListCommunities> { impl Perform for Oper<ListCommunities> {
type Response = ListCommunitiesResponse; type Response = ListCommunitiesResponse;
fn perform( async fn perform(
&self, &self,
pool: Pool<ConnectionManager<PgConnection>>, pool: &DbPool,
_websocket_info: Option<WebsocketInfo>, _websocket_info: Option<WebsocketInfo>,
) -> Result<ListCommunitiesResponse, Error> { ) -> Result<ListCommunitiesResponse, LemmyError> {
let data: &ListCommunities = &self.data; let data: &ListCommunities = &self.data;
let user_claims: Option<Claims> = match &data.auth { let user_claims: Option<Claims> = match &data.auth {
@ -483,29 +514,33 @@ impl Perform for Oper<ListCommunities> {
let sort = SortType::from_str(&data.sort)?; let sort = SortType::from_str(&data.sort)?;
let conn = pool.get()?; let page = data.page;
let limit = data.limit;
let communities = CommunityQueryBuilder::create(&conn) let communities = blocking(pool, move |conn| {
CommunityQueryBuilder::create(conn)
.sort(&sort) .sort(&sort)
.for_user(user_id) .for_user(user_id)
.show_nsfw(show_nsfw) .show_nsfw(show_nsfw)
.page(data.page) .page(page)
.limit(data.limit) .limit(limit)
.list()?; .list()
})
.await??;
// Return the jwt // Return the jwt
Ok(ListCommunitiesResponse { communities }) Ok(ListCommunitiesResponse { communities })
} }
} }
#[async_trait::async_trait(?Send)]
impl Perform for Oper<FollowCommunity> { impl Perform for Oper<FollowCommunity> {
type Response = CommunityResponse; type Response = CommunityResponse;
fn perform( async fn perform(
&self, &self,
pool: Pool<ConnectionManager<PgConnection>>, pool: &DbPool,
_websocket_info: Option<WebsocketInfo>, _websocket_info: Option<WebsocketInfo>,
) -> Result<CommunityResponse, Error> { ) -> Result<CommunityResponse, LemmyError> {
let data: &FollowCommunity = &self.data; let data: &FollowCommunity = &self.data;
let claims = match Claims::decode(&data.auth) { let claims = match Claims::decode(&data.auth) {
@ -515,9 +550,8 @@ impl Perform for Oper<FollowCommunity> {
let user_id = claims.id; let user_id = claims.id;
let conn = pool.get()?; let community_id = data.community_id;
let community = blocking(pool, move |conn| Community::read(conn, community_id)).await??;
let community = Community::read(&conn, data.community_id)?;
let community_follower_form = CommunityFollowerForm { let community_follower_form = CommunityFollowerForm {
community_id: data.community_id, community_id: data.community_id,
user_id, user_id,
@ -525,34 +559,44 @@ impl Perform for Oper<FollowCommunity> {
if community.local { if community.local {
if data.follow { if data.follow {
match CommunityFollower::follow(&conn, &community_follower_form) { let follow = move |conn: &'_ _| CommunityFollower::follow(conn, &community_follower_form);
Ok(user) => user, if blocking(pool, follow).await?.is_err() {
Err(_e) => return Err(APIError::err("community_follower_already_exists").into()), return Err(APIError::err("community_follower_already_exists").into());
};
} else {
match CommunityFollower::unfollow(&conn, &community_follower_form) {
Ok(user) => user,
Err(_e) => return Err(APIError::err("community_follower_already_exists").into()),
};
} }
} else { } else {
let user = User_::read(&conn, user_id)?; let unfollow =
move |conn: &'_ _| CommunityFollower::unfollow(conn, &community_follower_form);
if blocking(pool, unfollow).await?.is_err() {
return Err(APIError::err("community_follower_already_exists").into());
}
}
} else {
let user = blocking(pool, move |conn| User_::read(conn, user_id)).await??;
if data.follow { if data.follow {
// Dont actually add to the community followers here, because you need // Dont actually add to the community followers here, because you need
// to wait for the accept // to wait for the accept
user.send_follow(&community.actor_id, &conn)?; user
.send_follow(&community.actor_id, &self.client, pool)
.await?;
} else { } else {
user.send_unfollow(&community.actor_id, &conn)?; user
match CommunityFollower::unfollow(&conn, &community_follower_form) { .send_unfollow(&community.actor_id, &self.client, pool)
Ok(user) => user, .await?;
Err(_e) => return Err(APIError::err("community_follower_already_exists").into()), let unfollow =
}; move |conn: &'_ _| CommunityFollower::unfollow(conn, &community_follower_form);
if blocking(pool, unfollow).await?.is_err() {
return Err(APIError::err("community_follower_already_exists").into());
}
} }
// TODO: this needs to return a "pending" state, until Accept is received from the remote server // 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))?; let community_id = data.community_id;
let community_view = blocking(pool, move |conn| {
CommunityView::read(conn, community_id, Some(user_id))
})
.await??;
Ok(CommunityResponse { Ok(CommunityResponse {
community: community_view, community: community_view,
@ -560,14 +604,15 @@ impl Perform for Oper<FollowCommunity> {
} }
} }
#[async_trait::async_trait(?Send)]
impl Perform for Oper<GetFollowedCommunities> { impl Perform for Oper<GetFollowedCommunities> {
type Response = GetFollowedCommunitiesResponse; type Response = GetFollowedCommunitiesResponse;
fn perform( async fn perform(
&self, &self,
pool: Pool<ConnectionManager<PgConnection>>, pool: &DbPool,
_websocket_info: Option<WebsocketInfo>, _websocket_info: Option<WebsocketInfo>,
) -> Result<GetFollowedCommunitiesResponse, Error> { ) -> Result<GetFollowedCommunitiesResponse, LemmyError> {
let data: &GetFollowedCommunities = &self.data; let data: &GetFollowedCommunities = &self.data;
let claims = match Claims::decode(&data.auth) { let claims = match Claims::decode(&data.auth) {
@ -577,12 +622,13 @@ impl Perform for Oper<GetFollowedCommunities> {
let user_id = claims.id; let user_id = claims.id;
let conn = pool.get()?; let communities = match blocking(pool, move |conn| {
CommunityFollowerView::for_user(conn, user_id)
let communities: Vec<CommunityFollowerView> = })
match CommunityFollowerView::for_user(&conn, user_id) { .await?
{
Ok(communities) => communities, Ok(communities) => communities,
Err(_e) => return Err(APIError::err("system_err_login").into()), _ => return Err(APIError::err("system_err_login").into()),
}; };
// Return the jwt // Return the jwt
@ -590,14 +636,15 @@ impl Perform for Oper<GetFollowedCommunities> {
} }
} }
#[async_trait::async_trait(?Send)]
impl Perform for Oper<BanFromCommunity> { impl Perform for Oper<BanFromCommunity> {
type Response = BanFromCommunityResponse; type Response = BanFromCommunityResponse;
fn perform( async fn perform(
&self, &self,
pool: Pool<ConnectionManager<PgConnection>>, pool: &DbPool,
websocket_info: Option<WebsocketInfo>, websocket_info: Option<WebsocketInfo>,
) -> Result<BanFromCommunityResponse, Error> { ) -> Result<BanFromCommunityResponse, LemmyError> {
let data: &BanFromCommunity = &self.data; let data: &BanFromCommunity = &self.data;
let claims = match Claims::decode(&data.auth) { let claims = match Claims::decode(&data.auth) {
@ -612,18 +659,16 @@ impl Perform for Oper<BanFromCommunity> {
user_id: data.user_id, user_id: data.user_id,
}; };
let conn = pool.get()?;
if data.ban { if data.ban {
match CommunityUserBan::ban(&conn, &community_user_ban_form) { let ban = move |conn: &'_ _| CommunityUserBan::ban(conn, &community_user_ban_form);
Ok(user) => user, if blocking(pool, ban).await?.is_err() {
Err(_e) => return Err(APIError::err("community_user_already_banned").into()), return Err(APIError::err("community_user_already_banned").into());
}; }
} else { } else {
match CommunityUserBan::unban(&conn, &community_user_ban_form) { let unban = move |conn: &'_ _| CommunityUserBan::unban(conn, &community_user_ban_form);
Ok(user) => user, if blocking(pool, unban).await?.is_err() {
Err(_e) => return Err(APIError::err("community_user_already_banned").into()), return Err(APIError::err("community_user_already_banned").into());
}; }
} }
// Mod tables // Mod tables
@ -640,9 +685,10 @@ impl Perform for Oper<BanFromCommunity> {
banned: Some(data.ban), banned: Some(data.ban),
expires, expires,
}; };
ModBanFromCommunity::create(&conn, &form)?; blocking(pool, move |conn| ModBanFromCommunity::create(conn, &form)).await??;
let user_view = UserView::read(&conn, data.user_id)?; let user_id = data.user_id;
let user_view = blocking(pool, move |conn| UserView::read(conn, user_id)).await??;
let res = BanFromCommunityResponse { let res = BanFromCommunityResponse {
user: user_view, user: user_view,
@ -662,14 +708,15 @@ impl Perform for Oper<BanFromCommunity> {
} }
} }
#[async_trait::async_trait(?Send)]
impl Perform for Oper<AddModToCommunity> { impl Perform for Oper<AddModToCommunity> {
type Response = AddModToCommunityResponse; type Response = AddModToCommunityResponse;
fn perform( async fn perform(
&self, &self,
pool: Pool<ConnectionManager<PgConnection>>, pool: &DbPool,
websocket_info: Option<WebsocketInfo>, websocket_info: Option<WebsocketInfo>,
) -> Result<AddModToCommunityResponse, Error> { ) -> Result<AddModToCommunityResponse, LemmyError> {
let data: &AddModToCommunity = &self.data; let data: &AddModToCommunity = &self.data;
let claims = match Claims::decode(&data.auth) { let claims = match Claims::decode(&data.auth) {
@ -684,18 +731,16 @@ impl Perform for Oper<AddModToCommunity> {
user_id: data.user_id, user_id: data.user_id,
}; };
let conn = pool.get()?;
if data.added { if data.added {
match CommunityModerator::join(&conn, &community_moderator_form) { let join = move |conn: &'_ _| CommunityModerator::join(conn, &community_moderator_form);
Ok(user) => user, if blocking(pool, join).await?.is_err() {
Err(_e) => return Err(APIError::err("community_moderator_already_exists").into()), return Err(APIError::err("community_moderator_already_exists").into());
}; }
} else { } else {
match CommunityModerator::leave(&conn, &community_moderator_form) { let leave = move |conn: &'_ _| CommunityModerator::leave(conn, &community_moderator_form);
Ok(user) => user, if blocking(pool, leave).await?.is_err() {
Err(_e) => return Err(APIError::err("community_moderator_already_exists").into()), return Err(APIError::err("community_moderator_already_exists").into());
}; }
} }
// Mod tables // Mod tables
@ -705,9 +750,13 @@ impl Perform for Oper<AddModToCommunity> {
community_id: data.community_id, community_id: data.community_id,
removed: Some(!data.added), removed: Some(!data.added),
}; };
ModAddCommunity::create(&conn, &form)?; blocking(pool, move |conn| ModAddCommunity::create(conn, &form)).await??;
let moderators = CommunityModeratorView::for_community(&conn, data.community_id)?; let community_id = data.community_id;
let moderators = blocking(pool, move |conn| {
CommunityModeratorView::for_community(conn, community_id)
})
.await??;
let res = AddModToCommunityResponse { moderators }; let res = AddModToCommunityResponse { moderators };
@ -724,14 +773,15 @@ impl Perform for Oper<AddModToCommunity> {
} }
} }
#[async_trait::async_trait(?Send)]
impl Perform for Oper<TransferCommunity> { impl Perform for Oper<TransferCommunity> {
type Response = GetCommunityResponse; type Response = GetCommunityResponse;
fn perform( async fn perform(
&self, &self,
pool: Pool<ConnectionManager<PgConnection>>, pool: &DbPool,
_websocket_info: Option<WebsocketInfo>, _websocket_info: Option<WebsocketInfo>,
) -> Result<GetCommunityResponse, Error> { ) -> Result<GetCommunityResponse, LemmyError> {
let data: &TransferCommunity = &self.data; let data: &TransferCommunity = &self.data;
let claims = match Claims::decode(&data.auth) { let claims = match Claims::decode(&data.auth) {
@ -741,12 +791,14 @@ impl Perform for Oper<TransferCommunity> {
let user_id = claims.id; let user_id = claims.id;
let conn = pool.get()?; let community_id = data.community_id;
let read_community = blocking(pool, move |conn| Community::read(conn, community_id)).await??;
let read_community = Community::read(&conn, data.community_id)?; let site_creator_id =
blocking(pool, move |conn| Site::read(conn, 1).map(|s| s.creator_id)).await??;
let mut admins = blocking(pool, move |conn| UserView::admins(conn)).await??;
let site_creator_id = Site::read(&conn, 1)?.creator_id;
let mut admins = UserView::admins(&conn)?;
let creator_index = admins.iter().position(|r| r.id == site_creator_id).unwrap(); let creator_index = admins.iter().position(|r| r.id == site_creator_id).unwrap();
let creator_user = admins.remove(creator_index); let creator_user = admins.remove(creator_index);
admins.insert(0, creator_user); admins.insert(0, creator_user);
@ -774,13 +826,18 @@ impl Perform for Oper<TransferCommunity> {
published: None, published: None,
}; };
let _updated_community = match Community::update(&conn, data.community_id, &community_form) { let community_id = data.community_id;
Ok(community) => community, let update = move |conn: &'_ _| Community::update(conn, community_id, &community_form);
Err(_e) => return Err(APIError::err("couldnt_update_community").into()), if blocking(pool, update).await?.is_err() {
return Err(APIError::err("couldnt_update_community").into());
}; };
// You also have to re-do the community_moderator table, reordering it. // You also have to re-do the community_moderator table, reordering it.
let mut community_mods = CommunityModeratorView::for_community(&conn, data.community_id)?; let community_id = data.community_id;
let mut community_mods = blocking(pool, move |conn| {
CommunityModeratorView::for_community(conn, community_id)
})
.await??;
let creator_index = community_mods let creator_index = community_mods
.iter() .iter()
.position(|r| r.user_id == data.user_id) .position(|r| r.user_id == data.user_id)
@ -788,19 +845,23 @@ impl Perform for Oper<TransferCommunity> {
let creator_user = community_mods.remove(creator_index); let creator_user = community_mods.remove(creator_index);
community_mods.insert(0, creator_user); community_mods.insert(0, creator_user);
CommunityModerator::delete_for_community(&conn, data.community_id)?; let community_id = data.community_id;
blocking(pool, move |conn| {
CommunityModerator::delete_for_community(conn, community_id)
})
.await??;
// TODO: this should probably be a bulk operation
for cmod in &community_mods { for cmod in &community_mods {
let community_moderator_form = CommunityModeratorForm { let community_moderator_form = CommunityModeratorForm {
community_id: cmod.community_id, community_id: cmod.community_id,
user_id: cmod.user_id, user_id: cmod.user_id,
}; };
let _inserted_community_moderator = let join = move |conn: &'_ _| CommunityModerator::join(conn, &community_moderator_form);
match CommunityModerator::join(&conn, &community_moderator_form) { if blocking(pool, join).await?.is_err() {
Ok(user) => user, return Err(APIError::err("community_moderator_already_exists").into());
Err(_e) => return Err(APIError::err("community_moderator_already_exists").into()), }
};
} }
// Mod tables // Mod tables
@ -810,14 +871,24 @@ impl Perform for Oper<TransferCommunity> {
community_id: data.community_id, community_id: data.community_id,
removed: Some(false), removed: Some(false),
}; };
ModAddCommunity::create(&conn, &form)?; blocking(pool, move |conn| ModAddCommunity::create(conn, &form)).await??;
let community_view = match CommunityView::read(&conn, data.community_id, Some(user_id)) { let community_id = data.community_id;
let community_view = match blocking(pool, move |conn| {
CommunityView::read(conn, community_id, Some(user_id))
})
.await?
{
Ok(community) => community, Ok(community) => community,
Err(_e) => return Err(APIError::err("couldnt_find_community").into()), Err(_e) => return Err(APIError::err("couldnt_find_community").into()),
}; };
let moderators = match CommunityModeratorView::for_community(&conn, data.community_id) { let community_id = data.community_id;
let moderators = match blocking(pool, move |conn| {
CommunityModeratorView::for_community(conn, community_id)
})
.await?
{
Ok(moderators) => moderators, Ok(moderators) => moderators,
Err(_e) => return Err(APIError::err("couldnt_find_community").into()), Err(_e) => return Err(APIError::err("couldnt_find_community").into()),
}; };

View file

@ -1,12 +1,10 @@
use crate::{ use crate::{
db::{community::*, community_view::*, moderator::*, site::*, user::*, user_view::*}, db::{community::*, community_view::*, moderator::*, site::*, user::*, user_view::*},
websocket::WebsocketInfo, websocket::WebsocketInfo,
DbPool,
LemmyError,
}; };
use diesel::{ use actix_web::client::Client;
r2d2::{ConnectionManager, Pool},
PgConnection,
};
use failure::Error;
pub mod comment; pub mod comment;
pub mod community; pub mod community;
@ -30,20 +28,22 @@ impl APIError {
pub struct Oper<T> { pub struct Oper<T> {
data: T, data: T,
client: Client,
} }
impl<Data> Oper<Data> { impl<Data> Oper<Data> {
pub fn new(data: Data) -> Oper<Data> { pub fn new(data: Data, client: Client) -> Oper<Data> {
Oper { data } Oper { data, client }
} }
} }
#[async_trait::async_trait(?Send)]
pub trait Perform { pub trait Perform {
type Response: serde::ser::Serialize + Send; type Response: serde::ser::Serialize + Send;
fn perform( async fn perform(
&self, &self,
pool: Pool<ConnectionManager<PgConnection>>, pool: &DbPool,
websocket_info: Option<WebsocketInfo>, websocket_info: Option<WebsocketInfo>,
) -> Result<Self::Response, Error>; ) -> Result<Self::Response, LemmyError>;
} }

View file

@ -1,6 +1,7 @@
use crate::{ use crate::{
api::{APIError, Oper, Perform}, api::{APIError, Oper, Perform},
apub::{ApubLikeableType, ApubObjectType}, apub::{ApubLikeableType, ApubObjectType},
blocking,
db::{ db::{
comment_view::*, comment_view::*,
community_view::*, community_view::*,
@ -26,12 +27,9 @@ use crate::{
UserOperation, UserOperation,
WebsocketInfo, WebsocketInfo,
}, },
DbPool,
LemmyError,
}; };
use diesel::{
r2d2::{ConnectionManager, Pool},
PgConnection,
};
use failure::Error;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::str::FromStr; use std::str::FromStr;
@ -112,14 +110,15 @@ pub struct SavePost {
auth: String, auth: String,
} }
#[async_trait::async_trait(?Send)]
impl Perform for Oper<CreatePost> { impl Perform for Oper<CreatePost> {
type Response = PostResponse; type Response = PostResponse;
fn perform( async fn perform(
&self, &self,
pool: Pool<ConnectionManager<PgConnection>>, pool: &DbPool,
websocket_info: Option<WebsocketInfo>, websocket_info: Option<WebsocketInfo>,
) -> Result<PostResponse, Error> { ) -> Result<PostResponse, LemmyError> {
let data: &CreatePost = &self.data; let data: &CreatePost = &self.data;
let claims = match Claims::decode(&data.auth) { let claims = match Claims::decode(&data.auth) {
@ -139,22 +138,23 @@ impl Perform for Oper<CreatePost> {
let user_id = claims.id; let user_id = claims.id;
let conn = pool.get()?;
// Check for a community ban // Check for a community ban
if CommunityUserBanView::get(&conn, user_id, data.community_id).is_ok() { let community_id = data.community_id;
let is_banned =
move |conn: &'_ _| CommunityUserBanView::get(conn, user_id, community_id).is_ok();
if blocking(pool, is_banned).await? {
return Err(APIError::err("community_ban").into()); return Err(APIError::err("community_ban").into());
} }
// Check for a site ban // Check for a site ban
let user = User_::read(&conn, user_id)?; let user = blocking(pool, move |conn| User_::read(conn, user_id)).await??;
if user.banned { if user.banned {
return Err(APIError::err("site_ban").into()); return Err(APIError::err("site_ban").into());
} }
// Fetch Iframely and pictrs cached image // Fetch Iframely and pictrs cached image
let (iframely_title, iframely_description, iframely_html, pictrs_thumbnail) = let (iframely_title, iframely_description, iframely_html, pictrs_thumbnail) =
fetch_iframely_and_pictrs_data(data.url.to_owned()); fetch_iframely_and_pictrs_data(&self.client, data.url.to_owned()).await;
let post_form = PostForm { let post_form = PostForm {
name: data.name.to_owned(), name: data.name.to_owned(),
@ -177,7 +177,7 @@ impl Perform for Oper<CreatePost> {
published: None, published: None,
}; };
let inserted_post = match Post::create(&conn, &post_form) { let inserted_post = match blocking(pool, move |conn| Post::create(conn, &post_form)).await? {
Ok(post) => post, Ok(post) => post,
Err(e) => { Err(e) => {
let err_type = if e.to_string() == "value too long for type character varying(200)" { let err_type = if e.to_string() == "value too long for type character varying(200)" {
@ -190,12 +190,14 @@ impl Perform for Oper<CreatePost> {
} }
}; };
let updated_post = match Post::update_ap_id(&conn, inserted_post.id) { let inserted_post_id = inserted_post.id;
let updated_post =
match blocking(pool, move |conn| Post::update_ap_id(conn, inserted_post_id)).await? {
Ok(post) => post, Ok(post) => post,
Err(_e) => return Err(APIError::err("couldnt_create_post").into()), Err(_e) => return Err(APIError::err("couldnt_create_post").into()),
}; };
updated_post.send_create(&user, &conn)?; updated_post.send_create(&user, &self.client, pool).await?;
// They like their own post by default // They like their own post by default
let like_form = PostLikeForm { let like_form = PostLikeForm {
@ -204,15 +206,20 @@ impl Perform for Oper<CreatePost> {
score: 1, score: 1,
}; };
let _inserted_like = match PostLike::like(&conn, &like_form) { let like = move |conn: &'_ _| PostLike::like(conn, &like_form);
Ok(like) => like, if blocking(pool, like).await?.is_err() {
Err(_e) => return Err(APIError::err("couldnt_like_post").into()), return Err(APIError::err("couldnt_like_post").into());
}; }
updated_post.send_like(&user, &conn)?; updated_post.send_like(&user, &self.client, pool).await?;
// Refetch the view // Refetch the view
let post_view = match PostView::read(&conn, inserted_post.id, Some(user_id)) { let inserted_post_id = inserted_post.id;
let post_view = match blocking(pool, move |conn| {
PostView::read(conn, inserted_post_id, Some(user_id))
})
.await?
{
Ok(post) => post, Ok(post) => post,
Err(_e) => return Err(APIError::err("couldnt_find_post").into()), Err(_e) => return Err(APIError::err("couldnt_find_post").into()),
}; };
@ -231,14 +238,15 @@ impl Perform for Oper<CreatePost> {
} }
} }
#[async_trait::async_trait(?Send)]
impl Perform for Oper<GetPost> { impl Perform for Oper<GetPost> {
type Response = GetPostResponse; type Response = GetPostResponse;
fn perform( async fn perform(
&self, &self,
pool: Pool<ConnectionManager<PgConnection>>, pool: &DbPool,
websocket_info: Option<WebsocketInfo>, websocket_info: Option<WebsocketInfo>,
) -> Result<GetPostResponse, Error> { ) -> Result<GetPostResponse, LemmyError> {
let data: &GetPost = &self.data; let data: &GetPost = &self.data;
let user_id: Option<i32> = match &data.auth { let user_id: Option<i32> = match &data.auth {
@ -252,25 +260,38 @@ impl Perform for Oper<GetPost> {
None => None, None => None,
}; };
let conn = pool.get()?; let id = data.id;
let post_view = match blocking(pool, move |conn| PostView::read(conn, id, user_id)).await? {
let post_view = match PostView::read(&conn, data.id, user_id) {
Ok(post) => post, Ok(post) => post,
Err(_e) => return Err(APIError::err("couldnt_find_post").into()), Err(_e) => return Err(APIError::err("couldnt_find_post").into()),
}; };
let comments = CommentQueryBuilder::create(&conn) let id = data.id;
.for_post_id(data.id) let comments = blocking(pool, move |conn| {
CommentQueryBuilder::create(conn)
.for_post_id(id)
.my_user_id(user_id) .my_user_id(user_id)
.limit(9999) .limit(9999)
.list()?; .list()
})
.await??;
let community = CommunityView::read(&conn, post_view.community_id, user_id)?; let community_id = post_view.community_id;
let community = blocking(pool, move |conn| {
CommunityView::read(conn, community_id, user_id)
})
.await??;
let moderators = CommunityModeratorView::for_community(&conn, post_view.community_id)?; let community_id = post_view.community_id;
let moderators = blocking(pool, move |conn| {
CommunityModeratorView::for_community(conn, community_id)
})
.await??;
let site_creator_id = Site::read(&conn, 1)?.creator_id; let site_creator_id =
let mut admins = UserView::admins(&conn)?; blocking(pool, move |conn| Site::read(conn, 1).map(|s| s.creator_id)).await??;
let mut admins = blocking(pool, move |conn| UserView::admins(conn)).await??;
let creator_index = admins.iter().position(|r| r.id == site_creator_id).unwrap(); let creator_index = admins.iter().position(|r| r.id == site_creator_id).unwrap();
let creator_user = admins.remove(creator_index); let creator_user = admins.remove(creator_index);
admins.insert(0, creator_user); admins.insert(0, creator_user);
@ -305,14 +326,15 @@ impl Perform for Oper<GetPost> {
} }
} }
#[async_trait::async_trait(?Send)]
impl Perform for Oper<GetPosts> { impl Perform for Oper<GetPosts> {
type Response = GetPostsResponse; type Response = GetPostsResponse;
fn perform( async fn perform(
&self, &self,
pool: Pool<ConnectionManager<PgConnection>>, pool: &DbPool,
websocket_info: Option<WebsocketInfo>, websocket_info: Option<WebsocketInfo>,
) -> Result<GetPostsResponse, Error> { ) -> Result<GetPostsResponse, LemmyError> {
let data: &GetPosts = &self.data; let data: &GetPosts = &self.data;
let user_claims: Option<Claims> = match &data.auth { let user_claims: Option<Claims> = match &data.auth {
@ -336,17 +358,21 @@ impl Perform for Oper<GetPosts> {
let type_ = ListingType::from_str(&data.type_)?; let type_ = ListingType::from_str(&data.type_)?;
let sort = SortType::from_str(&data.sort)?; let sort = SortType::from_str(&data.sort)?;
let conn = pool.get()?; let page = data.page;
let limit = data.limit;
let posts = match PostQueryBuilder::create(&conn) let community_id = data.community_id;
let posts = match blocking(pool, move |conn| {
PostQueryBuilder::create(conn)
.listing_type(type_) .listing_type(type_)
.sort(&sort) .sort(&sort)
.show_nsfw(show_nsfw) .show_nsfw(show_nsfw)
.for_community_id(data.community_id) .for_community_id(community_id)
.my_user_id(user_id) .my_user_id(user_id)
.page(data.page) .page(page)
.limit(data.limit) .limit(limit)
.list() .list()
})
.await?
{ {
Ok(posts) => posts, Ok(posts) => posts,
Err(_e) => return Err(APIError::err("couldnt_get_posts").into()), Err(_e) => return Err(APIError::err("couldnt_get_posts").into()),
@ -370,14 +396,15 @@ impl Perform for Oper<GetPosts> {
} }
} }
#[async_trait::async_trait(?Send)]
impl Perform for Oper<CreatePostLike> { impl Perform for Oper<CreatePostLike> {
type Response = PostResponse; type Response = PostResponse;
fn perform( async fn perform(
&self, &self,
pool: Pool<ConnectionManager<PgConnection>>, pool: &DbPool,
websocket_info: Option<WebsocketInfo>, websocket_info: Option<WebsocketInfo>,
) -> Result<PostResponse, Error> { ) -> Result<PostResponse, LemmyError> {
let data: &CreatePostLike = &self.data; let data: &CreatePostLike = &self.data;
let claims = match Claims::decode(&data.auth) { let claims = match Claims::decode(&data.auth) {
@ -387,24 +414,27 @@ impl Perform for Oper<CreatePostLike> {
let user_id = claims.id; let user_id = claims.id;
let conn = pool.get()?;
// Don't do a downvote if site has downvotes disabled // Don't do a downvote if site has downvotes disabled
if data.score == -1 { if data.score == -1 {
let site = SiteView::read(&conn)?; let site = blocking(pool, move |conn| SiteView::read(conn)).await??;
if !site.enable_downvotes { if !site.enable_downvotes {
return Err(APIError::err("downvotes_disabled").into()); return Err(APIError::err("downvotes_disabled").into());
} }
} }
// Check for a community ban // Check for a community ban
let post = Post::read(&conn, data.post_id)?; let post_id = data.post_id;
if CommunityUserBanView::get(&conn, user_id, post.community_id).is_ok() { let post = blocking(pool, move |conn| Post::read(conn, post_id)).await??;
let community_id = post.community_id;
let is_banned =
move |conn: &'_ _| CommunityUserBanView::get(conn, user_id, community_id).is_ok();
if blocking(pool, is_banned).await? {
return Err(APIError::err("community_ban").into()); return Err(APIError::err("community_ban").into());
} }
// Check for a site ban // Check for a site ban
let user = User_::read(&conn, user_id)?; let user = blocking(pool, move |conn| User_::read(conn, user_id)).await??;
if user.banned { if user.banned {
return Err(APIError::err("site_ban").into()); return Err(APIError::err("site_ban").into());
} }
@ -416,26 +446,33 @@ impl Perform for Oper<CreatePostLike> {
}; };
// Remove any likes first // Remove any likes first
PostLike::remove(&conn, &like_form)?; let like_form2 = like_form.clone();
blocking(pool, move |conn| PostLike::remove(conn, &like_form2)).await??;
// Only add the like if the score isnt 0 // Only add the like if the score isnt 0
let do_add = like_form.score != 0 && (like_form.score == 1 || like_form.score == -1); let do_add = like_form.score != 0 && (like_form.score == 1 || like_form.score == -1);
if do_add { if do_add {
let _inserted_like = match PostLike::like(&conn, &like_form) { let like_form2 = like_form.clone();
Ok(like) => like, let like = move |conn: &'_ _| PostLike::like(conn, &like_form2);
Err(_e) => return Err(APIError::err("couldnt_like_post").into()), if blocking(pool, like).await?.is_err() {
}; return Err(APIError::err("couldnt_like_post").into());
}
if like_form.score == 1 { if like_form.score == 1 {
post.send_like(&user, &conn)?; post.send_like(&user, &self.client, pool).await?;
} else if like_form.score == -1 { } else if like_form.score == -1 {
post.send_dislike(&user, &conn)?; post.send_dislike(&user, &self.client, pool).await?;
} }
} else { } else {
post.send_undo_like(&user, &conn)?; post.send_undo_like(&user, &self.client, pool).await?;
} }
let post_view = match PostView::read(&conn, data.post_id, Some(user_id)) { let post_id = data.post_id;
let post_view = match blocking(pool, move |conn| {
PostView::read(conn, post_id, Some(user_id))
})
.await?
{
Ok(post) => post, Ok(post) => post,
Err(_e) => return Err(APIError::err("couldnt_find_post").into()), Err(_e) => return Err(APIError::err("couldnt_find_post").into()),
}; };
@ -454,14 +491,15 @@ impl Perform for Oper<CreatePostLike> {
} }
} }
#[async_trait::async_trait(?Send)]
impl Perform for Oper<EditPost> { impl Perform for Oper<EditPost> {
type Response = PostResponse; type Response = PostResponse;
fn perform( async fn perform(
&self, &self,
pool: Pool<ConnectionManager<PgConnection>>, pool: &DbPool,
websocket_info: Option<WebsocketInfo>, websocket_info: Option<WebsocketInfo>,
) -> Result<PostResponse, Error> { ) -> Result<PostResponse, LemmyError> {
let data: &EditPost = &self.data; let data: &EditPost = &self.data;
if let Err(slurs) = slur_check(&data.name) { if let Err(slurs) = slur_check(&data.name) {
@ -481,37 +519,46 @@ impl Perform for Oper<EditPost> {
let user_id = claims.id; let user_id = claims.id;
let conn = pool.get()?;
// Verify its the creator or a mod or admin // Verify its the creator or a mod or admin
let community_id = data.community_id;
let mut editors: Vec<i32> = vec![data.creator_id]; let mut editors: Vec<i32> = vec![data.creator_id];
editors.append( editors.append(
&mut CommunityModeratorView::for_community(&conn, data.community_id)? &mut blocking(pool, move |conn| {
.into_iter() CommunityModeratorView::for_community(conn, community_id)
.map(|m| m.user_id) .map(|v| v.into_iter().map(|m| m.user_id).collect())
.collect(), })
.await??,
);
editors.append(
&mut blocking(pool, move |conn| {
UserView::admins(conn).map(|v| v.into_iter().map(|a| a.id).collect())
})
.await??,
); );
editors.append(&mut UserView::admins(&conn)?.into_iter().map(|a| a.id).collect());
if !editors.contains(&user_id) { if !editors.contains(&user_id) {
return Err(APIError::err("no_post_edit_allowed").into()); return Err(APIError::err("no_post_edit_allowed").into());
} }
// Check for a community ban // Check for a community ban
if CommunityUserBanView::get(&conn, user_id, data.community_id).is_ok() { let community_id = data.community_id;
let is_banned =
move |conn: &'_ _| CommunityUserBanView::get(conn, user_id, community_id).is_ok();
if blocking(pool, is_banned).await? {
return Err(APIError::err("community_ban").into()); return Err(APIError::err("community_ban").into());
} }
// Check for a site ban // Check for a site ban
let user = User_::read(&conn, user_id)?; let user = blocking(pool, move |conn| User_::read(conn, user_id)).await??;
if user.banned { if user.banned {
return Err(APIError::err("site_ban").into()); return Err(APIError::err("site_ban").into());
} }
// Fetch Iframely and Pictrs cached image // Fetch Iframely and Pictrs cached image
let (iframely_title, iframely_description, iframely_html, pictrs_thumbnail) = let (iframely_title, iframely_description, iframely_html, pictrs_thumbnail) =
fetch_iframely_and_pictrs_data(data.url.to_owned()); fetch_iframely_and_pictrs_data(&self.client, data.url.to_owned()).await;
let read_post = Post::read(&conn, data.edit_id)?; let edit_id = data.edit_id;
let read_post = blocking(pool, move |conn| Post::read(conn, edit_id)).await??;
let post_form = PostForm { let post_form = PostForm {
name: data.name.to_owned(), name: data.name.to_owned(),
@ -534,7 +581,9 @@ impl Perform for Oper<EditPost> {
published: None, published: None,
}; };
let updated_post = match Post::update(&conn, data.edit_id, &post_form) { let edit_id = data.edit_id;
let res = blocking(pool, move |conn| Post::update(conn, edit_id, &post_form)).await?;
let updated_post: Post = match res {
Ok(post) => post, Ok(post) => post,
Err(e) => { Err(e) => {
let err_type = if e.to_string() == "value too long for type character varying(200)" { let err_type = if e.to_string() == "value too long for type character varying(200)" {
@ -555,7 +604,7 @@ impl Perform for Oper<EditPost> {
removed: Some(removed), removed: Some(removed),
reason: data.reason.to_owned(), reason: data.reason.to_owned(),
}; };
ModRemovePost::create(&conn, &form)?; blocking(pool, move |conn| ModRemovePost::create(conn, &form)).await??;
} }
if let Some(locked) = data.locked.to_owned() { if let Some(locked) = data.locked.to_owned() {
@ -564,7 +613,7 @@ impl Perform for Oper<EditPost> {
post_id: data.edit_id, post_id: data.edit_id,
locked: Some(locked), locked: Some(locked),
}; };
ModLockPost::create(&conn, &form)?; blocking(pool, move |conn| ModLockPost::create(conn, &form)).await??;
} }
if let Some(stickied) = data.stickied.to_owned() { if let Some(stickied) = data.stickied.to_owned() {
@ -573,26 +622,34 @@ impl Perform for Oper<EditPost> {
post_id: data.edit_id, post_id: data.edit_id,
stickied: Some(stickied), stickied: Some(stickied),
}; };
ModStickyPost::create(&conn, &form)?; blocking(pool, move |conn| ModStickyPost::create(conn, &form)).await??;
} }
if let Some(deleted) = data.deleted.to_owned() { if let Some(deleted) = data.deleted.to_owned() {
if deleted { if deleted {
updated_post.send_delete(&user, &conn)?; updated_post.send_delete(&user, &self.client, pool).await?;
} else { } else {
updated_post.send_undo_delete(&user, &conn)?; updated_post
.send_undo_delete(&user, &self.client, pool)
.await?;
} }
} else if let Some(removed) = data.removed.to_owned() { } else if let Some(removed) = data.removed.to_owned() {
if removed { if removed {
updated_post.send_remove(&user, &conn)?; updated_post.send_remove(&user, &self.client, pool).await?;
} else { } else {
updated_post.send_undo_remove(&user, &conn)?; updated_post
.send_undo_remove(&user, &self.client, pool)
.await?;
} }
} else { } else {
updated_post.send_update(&user, &conn)?; updated_post.send_update(&user, &self.client, pool).await?;
} }
let post_view = PostView::read(&conn, data.edit_id, Some(user_id))?; let edit_id = data.edit_id;
let post_view = blocking(pool, move |conn| {
PostView::read(conn, edit_id, Some(user_id))
})
.await??;
let res = PostResponse { post: post_view }; let res = PostResponse { post: post_view };
@ -608,14 +665,15 @@ impl Perform for Oper<EditPost> {
} }
} }
#[async_trait::async_trait(?Send)]
impl Perform for Oper<SavePost> { impl Perform for Oper<SavePost> {
type Response = PostResponse; type Response = PostResponse;
fn perform( async fn perform(
&self, &self,
pool: Pool<ConnectionManager<PgConnection>>, pool: &DbPool,
_websocket_info: Option<WebsocketInfo>, _websocket_info: Option<WebsocketInfo>,
) -> Result<PostResponse, Error> { ) -> Result<PostResponse, LemmyError> {
let data: &SavePost = &self.data; let data: &SavePost = &self.data;
let claims = match Claims::decode(&data.auth) { let claims = match Claims::decode(&data.auth) {
@ -630,21 +688,23 @@ impl Perform for Oper<SavePost> {
user_id, user_id,
}; };
let conn = pool.get()?;
if data.save { if data.save {
match PostSaved::save(&conn, &post_saved_form) { let save = move |conn: &'_ _| PostSaved::save(conn, &post_saved_form);
Ok(post) => post, if blocking(pool, save).await?.is_err() {
Err(_e) => return Err(APIError::err("couldnt_save_post").into()), return Err(APIError::err("couldnt_save_post").into());
}; }
} else { } else {
match PostSaved::unsave(&conn, &post_saved_form) { let unsave = move |conn: &'_ _| PostSaved::unsave(conn, &post_saved_form);
Ok(post) => post, if blocking(pool, unsave).await?.is_err() {
Err(_e) => return Err(APIError::err("couldnt_save_post").into()), return Err(APIError::err("couldnt_save_post").into());
}; }
} }
let post_view = PostView::read(&conn, data.post_id, Some(user_id))?; let post_id = data.post_id;
let post_view = blocking(pool, move |conn| {
PostView::read(conn, post_id, Some(user_id))
})
.await??;
Ok(PostResponse { post: post_view }) Ok(PostResponse { post: post_view })
} }

View file

@ -2,6 +2,7 @@ use super::user::Register;
use crate::{ use crate::{
api::{APIError, Oper, Perform}, api::{APIError, Oper, Perform},
apub::fetcher::search_by_apub_id, apub::fetcher::search_by_apub_id,
blocking,
db::{ db::{
category::*, category::*,
comment_view::*, comment_view::*,
@ -22,12 +23,9 @@ use crate::{
slur_check, slur_check,
slurs_vec_to_str, slurs_vec_to_str,
websocket::{server::SendAllMessage, UserOperation, WebsocketInfo}, websocket::{server::SendAllMessage, UserOperation, WebsocketInfo},
DbPool,
LemmyError,
}; };
use diesel::{
r2d2::{ConnectionManager, Pool},
PgConnection,
};
use failure::Error;
use log::{debug, info}; use log::{debug, info};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::str::FromStr; use std::str::FromStr;
@ -139,87 +137,79 @@ pub struct SaveSiteConfig {
auth: String, auth: String,
} }
#[async_trait::async_trait(?Send)]
impl Perform for Oper<ListCategories> { impl Perform for Oper<ListCategories> {
type Response = ListCategoriesResponse; type Response = ListCategoriesResponse;
fn perform( async fn perform(
&self, &self,
pool: Pool<ConnectionManager<PgConnection>>, pool: &DbPool,
_websocket_info: Option<WebsocketInfo>, _websocket_info: Option<WebsocketInfo>,
) -> Result<ListCategoriesResponse, Error> { ) -> Result<ListCategoriesResponse, LemmyError> {
let _data: &ListCategories = &self.data; let _data: &ListCategories = &self.data;
let conn = pool.get()?; let categories = blocking(pool, move |conn| Category::list_all(conn)).await??;
let categories: Vec<Category> = Category::list_all(&conn)?;
// Return the jwt // Return the jwt
Ok(ListCategoriesResponse { categories }) Ok(ListCategoriesResponse { categories })
} }
} }
#[async_trait::async_trait(?Send)]
impl Perform for Oper<GetModlog> { impl Perform for Oper<GetModlog> {
type Response = GetModlogResponse; type Response = GetModlogResponse;
fn perform( async fn perform(
&self, &self,
pool: Pool<ConnectionManager<PgConnection>>, pool: &DbPool,
_websocket_info: Option<WebsocketInfo>, _websocket_info: Option<WebsocketInfo>,
) -> Result<GetModlogResponse, Error> { ) -> Result<GetModlogResponse, LemmyError> {
let data: &GetModlog = &self.data; let data: &GetModlog = &self.data;
let conn = pool.get()?; let community_id = data.community_id;
let mod_user_id = data.mod_user_id;
let page = data.page;
let limit = data.limit;
let removed_posts = blocking(pool, move |conn| {
ModRemovePostView::list(conn, community_id, mod_user_id, page, limit)
})
.await??;
let removed_posts = ModRemovePostView::list( let locked_posts = blocking(pool, move |conn| {
&conn, ModLockPostView::list(conn, community_id, mod_user_id, page, limit)
data.community_id, })
data.mod_user_id, .await??;
data.page,
data.limit, let stickied_posts = blocking(pool, move |conn| {
)?; ModStickyPostView::list(conn, community_id, mod_user_id, page, limit)
let locked_posts = ModLockPostView::list( })
&conn, .await??;
data.community_id,
data.mod_user_id, let removed_comments = blocking(pool, move |conn| {
data.page, ModRemoveCommentView::list(conn, community_id, mod_user_id, page, limit)
data.limit, })
)?; .await??;
let stickied_posts = ModStickyPostView::list(
&conn, let banned_from_community = blocking(pool, move |conn| {
data.community_id, ModBanFromCommunityView::list(conn, community_id, mod_user_id, page, limit)
data.mod_user_id, })
data.page, .await??;
data.limit,
)?; let added_to_community = blocking(pool, move |conn| {
let removed_comments = ModRemoveCommentView::list( ModAddCommunityView::list(conn, community_id, mod_user_id, page, limit)
&conn, })
data.community_id, .await??;
data.mod_user_id,
data.page,
data.limit,
)?;
let banned_from_community = ModBanFromCommunityView::list(
&conn,
data.community_id,
data.mod_user_id,
data.page,
data.limit,
)?;
let added_to_community = ModAddCommunityView::list(
&conn,
data.community_id,
data.mod_user_id,
data.page,
data.limit,
)?;
// These arrays are only for the full modlog, when a community isn't given // These arrays are only for the full modlog, when a community isn't given
let (removed_communities, banned, added) = if data.community_id.is_none() { let (removed_communities, banned, added) = if data.community_id.is_none() {
( blocking(pool, move |conn| {
ModRemoveCommunityView::list(&conn, data.mod_user_id, data.page, data.limit)?, Ok((
ModBanView::list(&conn, data.mod_user_id, data.page, data.limit)?, ModRemoveCommunityView::list(conn, mod_user_id, page, limit)?,
ModAddView::list(&conn, data.mod_user_id, data.page, data.limit)?, ModBanView::list(conn, mod_user_id, page, limit)?,
) ModAddView::list(conn, mod_user_id, page, limit)?,
)) as Result<_, LemmyError>
})
.await??
} else { } else {
(Vec::new(), Vec::new(), Vec::new()) (Vec::new(), Vec::new(), Vec::new())
}; };
@ -239,14 +229,15 @@ impl Perform for Oper<GetModlog> {
} }
} }
#[async_trait::async_trait(?Send)]
impl Perform for Oper<CreateSite> { impl Perform for Oper<CreateSite> {
type Response = SiteResponse; type Response = SiteResponse;
fn perform( async fn perform(
&self, &self,
pool: Pool<ConnectionManager<PgConnection>>, pool: &DbPool,
_websocket_info: Option<WebsocketInfo>, _websocket_info: Option<WebsocketInfo>,
) -> Result<SiteResponse, Error> { ) -> Result<SiteResponse, LemmyError> {
let data: &CreateSite = &self.data; let data: &CreateSite = &self.data;
let claims = match Claims::decode(&data.auth) { let claims = match Claims::decode(&data.auth) {
@ -266,10 +257,9 @@ impl Perform for Oper<CreateSite> {
let user_id = claims.id; let user_id = claims.id;
let conn = pool.get()?;
// Make sure user is an admin // Make sure user is an admin
if !UserView::read(&conn, user_id)?.admin { let user = blocking(pool, move |conn| UserView::read(conn, user_id)).await??;
if !user.admin {
return Err(APIError::err("not_an_admin").into()); return Err(APIError::err("not_an_admin").into());
} }
@ -283,24 +273,25 @@ impl Perform for Oper<CreateSite> {
updated: None, updated: None,
}; };
match Site::create(&conn, &site_form) { let create_site = move |conn: &'_ _| Site::create(conn, &site_form);
Ok(site) => site, if blocking(pool, create_site).await?.is_err() {
Err(_e) => return Err(APIError::err("site_already_exists").into()), return Err(APIError::err("site_already_exists").into());
}; }
let site_view = SiteView::read(&conn)?; let site_view = blocking(pool, move |conn| SiteView::read(conn)).await??;
Ok(SiteResponse { site: site_view }) Ok(SiteResponse { site: site_view })
} }
} }
#[async_trait::async_trait(?Send)]
impl Perform for Oper<EditSite> { impl Perform for Oper<EditSite> {
type Response = SiteResponse; type Response = SiteResponse;
fn perform( async fn perform(
&self, &self,
pool: Pool<ConnectionManager<PgConnection>>, pool: &DbPool,
websocket_info: Option<WebsocketInfo>, websocket_info: Option<WebsocketInfo>,
) -> Result<SiteResponse, Error> { ) -> Result<SiteResponse, LemmyError> {
let data: &EditSite = &self.data; let data: &EditSite = &self.data;
let claims = match Claims::decode(&data.auth) { let claims = match Claims::decode(&data.auth) {
@ -320,14 +311,13 @@ impl Perform for Oper<EditSite> {
let user_id = claims.id; let user_id = claims.id;
let conn = pool.get()?;
// Make sure user is an admin // Make sure user is an admin
if !UserView::read(&conn, user_id)?.admin { let user = blocking(pool, move |conn| UserView::read(conn, user_id)).await??;
if !user.admin {
return Err(APIError::err("not_an_admin").into()); return Err(APIError::err("not_an_admin").into());
} }
let found_site = Site::read(&conn, 1)?; let found_site = blocking(pool, move |conn| Site::read(conn, 1)).await??;
let site_form = SiteForm { let site_form = SiteForm {
name: data.name.to_owned(), name: data.name.to_owned(),
@ -339,12 +329,12 @@ impl Perform for Oper<EditSite> {
enable_nsfw: data.enable_nsfw, enable_nsfw: data.enable_nsfw,
}; };
match Site::update(&conn, 1, &site_form) { let update_site = move |conn: &'_ _| Site::update(conn, 1, &site_form);
Ok(site) => site, if blocking(pool, update_site).await?.is_err() {
Err(_e) => return Err(APIError::err("couldnt_update_site").into()), return Err(APIError::err("couldnt_update_site").into());
}; }
let site_view = SiteView::read(&conn)?; let site_view = blocking(pool, move |conn| SiteView::read(conn)).await??;
let res = SiteResponse { site: site_view }; let res = SiteResponse { site: site_view };
@ -360,21 +350,21 @@ impl Perform for Oper<EditSite> {
} }
} }
#[async_trait::async_trait(?Send)]
impl Perform for Oper<GetSite> { impl Perform for Oper<GetSite> {
type Response = GetSiteResponse; type Response = GetSiteResponse;
fn perform( async fn perform(
&self, &self,
pool: Pool<ConnectionManager<PgConnection>>, pool: &DbPool,
websocket_info: Option<WebsocketInfo>, websocket_info: Option<WebsocketInfo>,
) -> Result<GetSiteResponse, Error> { ) -> Result<GetSiteResponse, LemmyError> {
let _data: &GetSite = &self.data; let _data: &GetSite = &self.data;
let conn = pool.get()?;
// TODO refactor this a little // TODO refactor this a little
let site_view = if let Ok(_site) = Site::read(&conn, 1) { let res = blocking(pool, move |conn| Site::read(conn, 1)).await?;
Some(SiteView::read(&conn)?) let site_view = if res.is_ok() {
Some(blocking(pool, move |conn| SiteView::read(conn)).await??)
} else if let Some(setup) = Settings::get().setup.as_ref() { } else if let Some(setup) = Settings::get().setup.as_ref() {
let register = Register { let register = Register {
username: setup.admin_username.to_owned(), username: setup.admin_username.to_owned(),
@ -384,7 +374,9 @@ impl Perform for Oper<GetSite> {
admin: true, admin: true,
show_nsfw: true, show_nsfw: true,
}; };
let login_response = Oper::new(register).perform(pool.clone(), websocket_info.clone())?; let login_response = Oper::new(register, self.client.clone())
.perform(pool, websocket_info.clone())
.await?;
info!("Admin {} created", setup.admin_username); info!("Admin {} created", setup.admin_username);
let create_site = CreateSite { let create_site = CreateSite {
@ -395,14 +387,16 @@ impl Perform for Oper<GetSite> {
enable_nsfw: true, enable_nsfw: true,
auth: login_response.jwt, auth: login_response.jwt,
}; };
Oper::new(create_site).perform(pool, websocket_info.clone())?; Oper::new(create_site, self.client.clone())
.perform(pool, websocket_info.clone())
.await?;
info!("Site {} created", setup.site_name); info!("Site {} created", setup.site_name);
Some(SiteView::read(&conn)?) Some(blocking(pool, move |conn| SiteView::read(conn)).await??)
} else { } else {
None None
}; };
let mut admins = UserView::admins(&conn)?; let mut admins = blocking(pool, move |conn| UserView::admins(conn)).await??;
// Make sure the site creator is the top admin // Make sure the site creator is the top admin
if let Some(site_view) = site_view.to_owned() { if let Some(site_view) = site_view.to_owned() {
@ -415,7 +409,7 @@ impl Perform for Oper<GetSite> {
} }
} }
let banned = UserView::banned(&conn)?; let banned = blocking(pool, move |conn| UserView::banned(conn)).await??;
let online = if let Some(_ws) = websocket_info { let online = if let Some(_ws) = websocket_info {
// TODO // TODO
@ -437,21 +431,20 @@ impl Perform for Oper<GetSite> {
} }
} }
#[async_trait::async_trait(?Send)]
impl Perform for Oper<Search> { impl Perform for Oper<Search> {
type Response = SearchResponse; type Response = SearchResponse;
fn perform( async fn perform(
&self, &self,
pool: Pool<ConnectionManager<PgConnection>>, pool: &DbPool,
_websocket_info: Option<WebsocketInfo>, _websocket_info: Option<WebsocketInfo>,
) -> Result<SearchResponse, Error> { ) -> Result<SearchResponse, LemmyError> {
let data: &Search = &self.data; let data: &Search = &self.data;
dbg!(&data); dbg!(&data);
let conn = pool.get()?; match search_by_apub_id(&data.q, &self.client, pool).await {
match search_by_apub_id(&data.q, &conn) {
Ok(r) => return Ok(r), Ok(r) => return Ok(r),
Err(e) => debug!("Failed to resolve search query as activitypub ID: {}", e), Err(e) => debug!("Failed to resolve search query as activitypub ID: {}", e),
} }
@ -467,7 +460,6 @@ impl Perform for Oper<Search> {
None => None, None => None,
}; };
let sort = SortType::from_str(&data.sort)?;
let type_ = SearchType::from_str(&data.type_)?; let type_ = SearchType::from_str(&data.type_)?;
let mut posts = Vec::new(); let mut posts = Vec::new();
@ -477,85 +469,126 @@ impl Perform for Oper<Search> {
// TODO no clean / non-nsfw searching rn // TODO no clean / non-nsfw searching rn
let q = data.q.to_owned();
let page = data.page;
let limit = data.limit;
let sort = SortType::from_str(&data.sort)?;
let community_id = data.community_id;
match type_ { match type_ {
SearchType::Posts => { SearchType::Posts => {
posts = PostQueryBuilder::create(&conn) posts = blocking(pool, move |conn| {
PostQueryBuilder::create(conn)
.sort(&sort) .sort(&sort)
.show_nsfw(true) .show_nsfw(true)
.for_community_id(data.community_id) .for_community_id(community_id)
.search_term(data.q.to_owned()) .search_term(q)
.my_user_id(user_id) .my_user_id(user_id)
.page(data.page) .page(page)
.limit(data.limit) .limit(limit)
.list()?; .list()
})
.await??;
} }
SearchType::Comments => { SearchType::Comments => {
comments = CommentQueryBuilder::create(&conn) comments = blocking(pool, move |conn| {
CommentQueryBuilder::create(&conn)
.sort(&sort) .sort(&sort)
.search_term(data.q.to_owned()) .search_term(q)
.my_user_id(user_id) .my_user_id(user_id)
.page(data.page) .page(page)
.limit(data.limit) .limit(limit)
.list()?; .list()
})
.await??;
} }
SearchType::Communities => { SearchType::Communities => {
communities = CommunityQueryBuilder::create(&conn) communities = blocking(pool, move |conn| {
CommunityQueryBuilder::create(conn)
.sort(&sort) .sort(&sort)
.search_term(data.q.to_owned()) .search_term(q)
.page(data.page) .page(page)
.limit(data.limit) .limit(limit)
.list()?; .list()
})
.await??;
} }
SearchType::Users => { SearchType::Users => {
users = UserQueryBuilder::create(&conn) users = blocking(pool, move |conn| {
UserQueryBuilder::create(conn)
.sort(&sort) .sort(&sort)
.search_term(data.q.to_owned()) .search_term(q)
.page(data.page) .page(page)
.limit(data.limit) .limit(limit)
.list()?; .list()
})
.await??;
} }
SearchType::All => { SearchType::All => {
posts = PostQueryBuilder::create(&conn) posts = blocking(pool, move |conn| {
PostQueryBuilder::create(conn)
.sort(&sort) .sort(&sort)
.show_nsfw(true) .show_nsfw(true)
.for_community_id(data.community_id) .for_community_id(community_id)
.search_term(data.q.to_owned()) .search_term(q)
.my_user_id(user_id) .my_user_id(user_id)
.page(data.page) .page(page)
.limit(data.limit) .limit(limit)
.list()?; .list()
})
.await??;
comments = CommentQueryBuilder::create(&conn) let q = data.q.to_owned();
let sort = SortType::from_str(&data.sort)?;
comments = blocking(pool, move |conn| {
CommentQueryBuilder::create(conn)
.sort(&sort) .sort(&sort)
.search_term(data.q.to_owned()) .search_term(q)
.my_user_id(user_id) .my_user_id(user_id)
.page(data.page) .page(page)
.limit(data.limit) .limit(limit)
.list()?; .list()
})
.await??;
communities = CommunityQueryBuilder::create(&conn) let q = data.q.to_owned();
.sort(&sort) let sort = SortType::from_str(&data.sort)?;
.search_term(data.q.to_owned())
.page(data.page)
.limit(data.limit)
.list()?;
users = UserQueryBuilder::create(&conn) communities = blocking(pool, move |conn| {
CommunityQueryBuilder::create(conn)
.sort(&sort) .sort(&sort)
.search_term(data.q.to_owned()) .search_term(q)
.page(data.page) .page(page)
.limit(data.limit) .limit(limit)
.list()?; .list()
})
.await??;
let q = data.q.to_owned();
let sort = SortType::from_str(&data.sort)?;
users = blocking(pool, move |conn| {
UserQueryBuilder::create(conn)
.sort(&sort)
.search_term(q)
.page(page)
.limit(limit)
.list()
})
.await??;
} }
SearchType::Url => { SearchType::Url => {
posts = PostQueryBuilder::create(&conn) posts = blocking(pool, move |conn| {
PostQueryBuilder::create(conn)
.sort(&sort) .sort(&sort)
.show_nsfw(true) .show_nsfw(true)
.for_community_id(data.community_id) .for_community_id(community_id)
.url_search(data.q.to_owned()) .url_search(q)
.page(data.page) .page(page)
.limit(data.limit) .limit(limit)
.list()?; .list()
})
.await??;
} }
}; };
@ -570,14 +603,15 @@ impl Perform for Oper<Search> {
} }
} }
#[async_trait::async_trait(?Send)]
impl Perform for Oper<TransferSite> { impl Perform for Oper<TransferSite> {
type Response = GetSiteResponse; type Response = GetSiteResponse;
fn perform( async fn perform(
&self, &self,
pool: Pool<ConnectionManager<PgConnection>>, pool: &DbPool,
_websocket_info: Option<WebsocketInfo>, _websocket_info: Option<WebsocketInfo>,
) -> Result<GetSiteResponse, Error> { ) -> Result<GetSiteResponse, LemmyError> {
let data: &TransferSite = &self.data; let data: &TransferSite = &self.data;
let claims = match Claims::decode(&data.auth) { let claims = match Claims::decode(&data.auth) {
@ -587,9 +621,7 @@ impl Perform for Oper<TransferSite> {
let user_id = claims.id; let user_id = claims.id;
let conn = pool.get()?; let read_site = blocking(pool, move |conn| Site::read(conn, 1)).await??;
let read_site = Site::read(&conn, 1)?;
// Make sure user is the creator // Make sure user is the creator
if read_site.creator_id != user_id { if read_site.creator_id != user_id {
@ -606,9 +638,9 @@ impl Perform for Oper<TransferSite> {
enable_nsfw: read_site.enable_nsfw, enable_nsfw: read_site.enable_nsfw,
}; };
match Site::update(&conn, 1, &site_form) { let update_site = move |conn: &'_ _| Site::update(conn, 1, &site_form);
Ok(site) => site, if blocking(pool, update_site).await?.is_err() {
Err(_e) => return Err(APIError::err("couldnt_update_site").into()), return Err(APIError::err("couldnt_update_site").into());
}; };
// Mod tables // Mod tables
@ -618,11 +650,11 @@ impl Perform for Oper<TransferSite> {
removed: Some(false), removed: Some(false),
}; };
ModAdd::create(&conn, &form)?; blocking(pool, move |conn| ModAdd::create(conn, &form)).await??;
let site_view = SiteView::read(&conn)?; let site_view = blocking(pool, move |conn| SiteView::read(conn)).await??;
let mut admins = UserView::admins(&conn)?; let mut admins = blocking(pool, move |conn| UserView::admins(conn)).await??;
let creator_index = admins let creator_index = admins
.iter() .iter()
.position(|r| r.id == site_view.creator_id) .position(|r| r.id == site_view.creator_id)
@ -630,7 +662,7 @@ impl Perform for Oper<TransferSite> {
let creator_user = admins.remove(creator_index); let creator_user = admins.remove(creator_index);
admins.insert(0, creator_user); admins.insert(0, creator_user);
let banned = UserView::banned(&conn)?; let banned = blocking(pool, move |conn| UserView::banned(conn)).await??;
Ok(GetSiteResponse { Ok(GetSiteResponse {
site: Some(site_view), site: Some(site_view),
@ -641,14 +673,15 @@ impl Perform for Oper<TransferSite> {
} }
} }
#[async_trait::async_trait(?Send)]
impl Perform for Oper<GetSiteConfig> { impl Perform for Oper<GetSiteConfig> {
type Response = GetSiteConfigResponse; type Response = GetSiteConfigResponse;
fn perform( async fn perform(
&self, &self,
pool: Pool<ConnectionManager<PgConnection>>, pool: &DbPool,
_websocket_info: Option<WebsocketInfo>, _websocket_info: Option<WebsocketInfo>,
) -> Result<GetSiteConfigResponse, Error> { ) -> Result<GetSiteConfigResponse, LemmyError> {
let data: &GetSiteConfig = &self.data; let data: &GetSiteConfig = &self.data;
let claims = match Claims::decode(&data.auth) { let claims = match Claims::decode(&data.auth) {
@ -658,10 +691,8 @@ impl Perform for Oper<GetSiteConfig> {
let user_id = claims.id; let user_id = claims.id;
let conn = pool.get()?;
// Only let admins read this // Only let admins read this
let admins = UserView::admins(&conn)?; let admins = blocking(pool, move |conn| UserView::admins(conn)).await??;
let admin_ids: Vec<i32> = admins.into_iter().map(|m| m.id).collect(); let admin_ids: Vec<i32> = admins.into_iter().map(|m| m.id).collect();
if !admin_ids.contains(&user_id) { if !admin_ids.contains(&user_id) {
@ -674,14 +705,15 @@ impl Perform for Oper<GetSiteConfig> {
} }
} }
#[async_trait::async_trait(?Send)]
impl Perform for Oper<SaveSiteConfig> { impl Perform for Oper<SaveSiteConfig> {
type Response = GetSiteConfigResponse; type Response = GetSiteConfigResponse;
fn perform( async fn perform(
&self, &self,
pool: Pool<ConnectionManager<PgConnection>>, pool: &DbPool,
_websocket_info: Option<WebsocketInfo>, _websocket_info: Option<WebsocketInfo>,
) -> Result<GetSiteConfigResponse, Error> { ) -> Result<GetSiteConfigResponse, LemmyError> {
let data: &SaveSiteConfig = &self.data; let data: &SaveSiteConfig = &self.data;
let claims = match Claims::decode(&data.auth) { let claims = match Claims::decode(&data.auth) {
@ -691,10 +723,8 @@ impl Perform for Oper<SaveSiteConfig> {
let user_id = claims.id; let user_id = claims.id;
let conn = pool.get()?;
// Only let admins read this // Only let admins read this
let admins = UserView::admins(&conn)?; let admins = blocking(pool, move |conn| UserView::admins(conn)).await??;
let admin_ids: Vec<i32> = admins.into_iter().map(|m| m.id).collect(); let admin_ids: Vec<i32> = admins.into_iter().map(|m| m.id).collect();
if !admin_ids.contains(&user_id) { if !admin_ids.contains(&user_id) {

File diff suppressed because it is too large Load diff

View file

@ -1,20 +1,22 @@
use crate::{ use crate::{
apub::{extensions::signatures::sign, is_apub_id_valid, ActorType}, apub::{extensions::signatures::sign, is_apub_id_valid, ActorType},
db::{activity::insert_activity, community::Community, user::User_}, db::{activity::insert_activity, community::Community, user::User_},
request::retry_custom,
DbPool,
LemmyError,
}; };
use activitystreams::{context, object::properties::ObjectProperties, public, Activity, Base}; use activitystreams::{context, object::properties::ObjectProperties, public, Activity, Base};
use diesel::PgConnection; use actix_web::client::Client;
use failure::{Error, _core::fmt::Debug};
use isahc::prelude::*;
use log::debug; use log::debug;
use serde::Serialize; use serde::Serialize;
use std::fmt::Debug;
use url::Url; use url::Url;
pub fn populate_object_props( pub fn populate_object_props(
props: &mut ObjectProperties, props: &mut ObjectProperties,
addressed_ccs: Vec<String>, addressed_ccs: Vec<String>,
object_id: &str, object_id: &str,
) -> Result<(), Error> { ) -> Result<(), LemmyError> {
props props
.set_context_xsd_any_uri(context())? .set_context_xsd_any_uri(context())?
// TODO: the activity needs a seperate id from the object // TODO: the activity needs a seperate id from the object
@ -26,48 +28,61 @@ pub fn populate_object_props(
Ok(()) Ok(())
} }
pub fn send_activity_to_community<A>( pub async fn send_activity_to_community<A>(
creator: &User_, creator: &User_,
conn: &PgConnection,
community: &Community, community: &Community,
to: Vec<String>, to: Vec<String>,
activity: A, activity: A,
) -> Result<(), Error> client: &Client,
pool: &DbPool,
) -> Result<(), LemmyError>
where where
A: Activity + Base + Serialize + Debug, A: Activity + Base + Serialize + Debug + Clone + Send + 'static,
{ {
insert_activity(&conn, creator.id, &activity, true)?; insert_activity(creator.id, activity.clone(), true, pool).await?;
// if this is a local community, we need to do an announce from the community instead // if this is a local community, we need to do an announce from the community instead
if community.local { if community.local {
Community::do_announce(activity, &community, creator, conn)?; Community::do_announce(activity, &community, creator, client, pool).await?;
} else { } else {
send_activity(&activity, creator, to)?; send_activity(client, &activity, creator, to).await?;
} }
Ok(()) Ok(())
} }
/// Send an activity to a list of recipients, using the correct headers etc. /// Send an activity to a list of recipients, using the correct headers etc.
pub fn send_activity<A>(activity: &A, actor: &dyn ActorType, to: Vec<String>) -> Result<(), Error> pub async fn send_activity<A>(
client: &Client,
activity: &A,
actor: &dyn ActorType,
to: Vec<String>,
) -> Result<(), LemmyError>
where where
A: Serialize + Debug, A: Serialize,
{ {
let json = serde_json::to_string(&activity)?; let activity = serde_json::to_string(&activity)?;
debug!("Sending activitypub activity {} to {:?}", json, to); debug!("Sending activitypub activity {} to {:?}", activity, to);
for t in to { for t in to {
let to_url = Url::parse(&t)?; let to_url = Url::parse(&t)?;
if !is_apub_id_valid(&to_url) { if !is_apub_id_valid(&to_url) {
debug!("Not sending activity to {} (invalid or blocklisted)", t); debug!("Not sending activity to {} (invalid or blocklisted)", t);
continue; continue;
} }
let request = Request::post(t).header("Host", to_url.domain().unwrap());
let signature = sign(&request, actor)?; let res = retry_custom(|| async {
let res = request let request = client.post(&t).header("Content-Type", "application/json");
.header("Signature", signature)
.header("Content-Type", "application/json") match sign(request, actor, activity.clone()).await {
.body(json.to_owned())? Ok(signed) => Ok(signed.send().await),
.send()?; Err(e) => Err(e),
}
})
.await?;
debug!("Result for activity send: {:?}", res); debug!("Result for activity send: {:?}", res);
} }
Ok(()) Ok(())
} }

View file

@ -16,6 +16,7 @@ use crate::{
FromApub, FromApub,
ToApub, ToApub,
}, },
blocking,
convert_datetime, convert_datetime,
db::{ db::{
comment::{Comment, CommentForm}, comment::{Comment, CommentForm},
@ -26,6 +27,8 @@ use crate::{
}, },
routes::DbPoolParam, routes::DbPoolParam,
scrape_text_for_mentions, scrape_text_for_mentions,
DbPool,
LemmyError,
MentionData, MentionData,
}; };
use activitystreams::{ use activitystreams::{
@ -35,9 +38,7 @@ use activitystreams::{
object::{kind::NoteType, properties::ObjectProperties, Note}, object::{kind::NoteType, properties::ObjectProperties, Note},
}; };
use activitystreams_new::object::Tombstone; use activitystreams_new::object::Tombstone;
use actix_web::{body::Body, web::Path, HttpResponse, Result}; use actix_web::{body::Body, client::Client, web::Path, HttpResponse};
use diesel::PgConnection;
use failure::Error;
use itertools::Itertools; use itertools::Itertools;
use log::debug; use log::debug;
use serde::Deserialize; use serde::Deserialize;
@ -51,32 +52,41 @@ pub struct CommentQuery {
pub async fn get_apub_comment( pub async fn get_apub_comment(
info: Path<CommentQuery>, info: Path<CommentQuery>,
db: DbPoolParam, db: DbPoolParam,
) -> Result<HttpResponse<Body>, Error> { ) -> Result<HttpResponse<Body>, LemmyError> {
let id = info.comment_id.parse::<i32>()?; let id = info.comment_id.parse::<i32>()?;
let comment = Comment::read(&&db.get()?, id)?; let comment = blocking(&db, move |conn| Comment::read(conn, id)).await??;
if !comment.deleted { if !comment.deleted {
Ok(create_apub_response(&comment.to_apub(&db.get().unwrap())?)) Ok(create_apub_response(&comment.to_apub(&db).await?))
} else { } else {
Ok(create_apub_tombstone_response(&comment.to_tombstone()?)) Ok(create_apub_tombstone_response(&comment.to_tombstone()?))
} }
} }
#[async_trait::async_trait(?Send)]
impl ToApub for Comment { impl ToApub for Comment {
type Response = Note; type Response = Note;
fn to_apub(&self, conn: &PgConnection) -> Result<Note, Error> { async fn to_apub(&self, pool: &DbPool) -> Result<Note, LemmyError> {
let mut comment = Note::default(); let mut comment = Note::default();
let oprops: &mut ObjectProperties = comment.as_mut(); let oprops: &mut ObjectProperties = comment.as_mut();
let creator = User_::read(&conn, self.creator_id)?;
let post = Post::read(&conn, self.post_id)?; let creator_id = self.creator_id;
let community = Community::read(&conn, post.community_id)?; let creator = blocking(pool, move |conn| User_::read(conn, creator_id)).await??;
let post_id = self.post_id;
let post = blocking(pool, move |conn| Post::read(conn, post_id)).await??;
let community_id = post.community_id;
let community = blocking(pool, move |conn| Community::read(conn, community_id)).await??;
// Add a vector containing some important info to the "in_reply_to" field // Add a vector containing some important info to the "in_reply_to" field
// [post_ap_id, Option(parent_comment_ap_id)] // [post_ap_id, Option(parent_comment_ap_id)]
let mut in_reply_to_vec = vec![post.ap_id]; let mut in_reply_to_vec = vec![post.ap_id];
if let Some(parent_id) = self.parent_id { if let Some(parent_id) = self.parent_id {
let parent_comment = Comment::read(&conn, parent_id)?; let parent_comment = blocking(pool, move |conn| Comment::read(conn, parent_id)).await??;
in_reply_to_vec.push(parent_comment.ap_id); in_reply_to_vec.push(parent_comment.ap_id);
} }
@ -97,7 +107,7 @@ impl ToApub for Comment {
Ok(comment) Ok(comment)
} }
fn to_tombstone(&self) -> Result<Tombstone, Error> { fn to_tombstone(&self) -> Result<Tombstone, LemmyError> {
create_tombstone( create_tombstone(
self.deleted, self.deleted,
&self.ap_id, &self.ap_id,
@ -107,27 +117,34 @@ impl ToApub for Comment {
} }
} }
#[async_trait::async_trait(?Send)]
impl FromApub for CommentForm { impl FromApub for CommentForm {
type ApubType = Note; type ApubType = Note;
/// Parse an ActivityPub note received from another instance into a Lemmy comment /// Parse an ActivityPub note received from another instance into a Lemmy comment
fn from_apub(note: &Note, conn: &PgConnection) -> Result<CommentForm, Error> { async fn from_apub(
note: &Note,
client: &Client,
pool: &DbPool,
) -> Result<CommentForm, LemmyError> {
let oprops = &note.object_props; let oprops = &note.object_props;
let creator_actor_id = &oprops.get_attributed_to_xsd_any_uri().unwrap().to_string(); let creator_actor_id = &oprops.get_attributed_to_xsd_any_uri().unwrap().to_string();
let creator = get_or_fetch_and_upsert_remote_user(&creator_actor_id, &conn)?;
let creator = get_or_fetch_and_upsert_remote_user(&creator_actor_id, client, pool).await?;
let mut in_reply_tos = oprops.get_many_in_reply_to_xsd_any_uris().unwrap(); let mut in_reply_tos = oprops.get_many_in_reply_to_xsd_any_uris().unwrap();
let post_ap_id = in_reply_tos.next().unwrap().to_string(); let post_ap_id = in_reply_tos.next().unwrap().to_string();
// This post, or the parent comment might not yet exist on this server yet, fetch them. // This post, or the parent comment might not yet exist on this server yet, fetch them.
let post = get_or_fetch_and_insert_remote_post(&post_ap_id, &conn)?; let post = get_or_fetch_and_insert_remote_post(&post_ap_id, client, pool).await?;
// The 2nd item, if it exists, is the parent comment apub_id // The 2nd item, if it exists, is the parent comment apub_id
// For deeply nested comments, FromApub automatically gets called recursively // For deeply nested comments, FromApub automatically gets called recursively
let parent_id: Option<i32> = match in_reply_tos.next() { let parent_id: Option<i32> = match in_reply_tos.next() {
Some(parent_comment_uri) => { Some(parent_comment_uri) => {
let parent_comment_ap_id = &parent_comment_uri.to_string(); let parent_comment_ap_id = &parent_comment_uri.to_string();
let parent_comment = get_or_fetch_and_insert_remote_comment(&parent_comment_ap_id, &conn)?; let parent_comment =
get_or_fetch_and_insert_remote_comment(&parent_comment_ap_id, client, pool).await?;
Some(parent_comment.id) Some(parent_comment.id)
} }
@ -157,17 +174,27 @@ impl FromApub for CommentForm {
} }
} }
#[async_trait::async_trait(?Send)]
impl ApubObjectType for Comment { impl ApubObjectType for Comment {
/// Send out information about a newly created comment, to the followers of the community. /// Send out information about a newly created comment, to the followers of the community.
fn send_create(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error> { async fn send_create(
let note = self.to_apub(conn)?; &self,
let post = Post::read(&conn, self.post_id)?; creator: &User_,
let community = Community::read(conn, post.community_id)?; client: &Client,
pool: &DbPool,
) -> Result<(), LemmyError> {
let note = self.to_apub(pool).await?;
let post_id = self.post_id;
let post = blocking(pool, move |conn| Post::read(conn, post_id)).await??;
let community_id = post.community_id;
let community = blocking(pool, move |conn| Community::read(conn, community_id)).await??;
let maa =
collect_non_local_mentions_and_addresses(&self.content, &community, client, pool).await?;
let id = format!("{}/create/{}", self.ap_id, uuid::Uuid::new_v4()); let id = format!("{}/create/{}", self.ap_id, uuid::Uuid::new_v4());
let maa: MentionsAndAddresses =
collect_non_local_mentions_and_addresses(&conn, &self.content, &community)?;
let mut create = Create::new(); let mut create = Create::new();
populate_object_props(&mut create.object_props, maa.addressed_ccs, &id)?; populate_object_props(&mut create.object_props, maa.addressed_ccs, &id)?;
@ -179,20 +206,29 @@ impl ApubObjectType for Comment {
.set_actor_xsd_any_uri(creator.actor_id.to_owned())? .set_actor_xsd_any_uri(creator.actor_id.to_owned())?
.set_object_base_box(note)?; .set_object_base_box(note)?;
send_activity_to_community(&creator, &conn, &community, maa.inboxes, create)?; send_activity_to_community(&creator, &community, maa.inboxes, create, client, pool).await?;
Ok(()) Ok(())
} }
/// Send out information about an edited post, to the followers of the community. /// Send out information about an edited post, to the followers of the community.
fn send_update(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error> { async fn send_update(
let note = self.to_apub(&conn)?; &self,
let post = Post::read(&conn, self.post_id)?; creator: &User_,
let community = Community::read(&conn, post.community_id)?; client: &Client,
pool: &DbPool,
) -> Result<(), LemmyError> {
let note = self.to_apub(pool).await?;
let post_id = self.post_id;
let post = blocking(pool, move |conn| Post::read(conn, post_id)).await??;
let community_id = post.community_id;
let community = blocking(pool, move |conn| Community::read(conn, community_id)).await??;
let maa =
collect_non_local_mentions_and_addresses(&self.content, &community, client, pool).await?;
let id = format!("{}/update/{}", self.ap_id, uuid::Uuid::new_v4()); let id = format!("{}/update/{}", self.ap_id, uuid::Uuid::new_v4());
let maa: MentionsAndAddresses =
collect_non_local_mentions_and_addresses(&conn, &self.content, &community)?;
let mut update = Update::new(); let mut update = Update::new();
populate_object_props(&mut update.object_props, maa.addressed_ccs, &id)?; populate_object_props(&mut update.object_props, maa.addressed_ccs, &id)?;
@ -204,14 +240,24 @@ impl ApubObjectType for Comment {
.set_actor_xsd_any_uri(creator.actor_id.to_owned())? .set_actor_xsd_any_uri(creator.actor_id.to_owned())?
.set_object_base_box(note)?; .set_object_base_box(note)?;
send_activity_to_community(&creator, &conn, &community, maa.inboxes, update)?; send_activity_to_community(&creator, &community, maa.inboxes, update, client, pool).await?;
Ok(()) Ok(())
} }
fn send_delete(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error> { async fn send_delete(
let note = self.to_apub(&conn)?; &self,
let post = Post::read(&conn, self.post_id)?; creator: &User_,
let community = Community::read(&conn, post.community_id)?; client: &Client,
pool: &DbPool,
) -> Result<(), LemmyError> {
let note = self.to_apub(pool).await?;
let post_id = self.post_id;
let post = blocking(pool, move |conn| Post::read(conn, post_id)).await??;
let community_id = post.community_id;
let community = blocking(pool, move |conn| Community::read(conn, community_id)).await??;
let id = format!("{}/delete/{}", self.ap_id, uuid::Uuid::new_v4()); let id = format!("{}/delete/{}", self.ap_id, uuid::Uuid::new_v4());
let mut delete = Delete::default(); let mut delete = Delete::default();
@ -228,18 +274,29 @@ impl ApubObjectType for Comment {
send_activity_to_community( send_activity_to_community(
&creator, &creator,
&conn,
&community, &community,
vec![community.get_shared_inbox_url()], vec![community.get_shared_inbox_url()],
delete, delete,
)?; client,
pool,
)
.await?;
Ok(()) Ok(())
} }
fn send_undo_delete(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error> { async fn send_undo_delete(
let note = self.to_apub(&conn)?; &self,
let post = Post::read(&conn, self.post_id)?; creator: &User_,
let community = Community::read(&conn, post.community_id)?; client: &Client,
pool: &DbPool,
) -> Result<(), LemmyError> {
let note = self.to_apub(pool).await?;
let post_id = self.post_id;
let post = blocking(pool, move |conn| Post::read(conn, post_id)).await??;
let community_id = post.community_id;
let community = blocking(pool, move |conn| Community::read(conn, community_id)).await??;
// Generate a fake delete activity, with the correct object // Generate a fake delete activity, with the correct object
let id = format!("{}/delete/{}", self.ap_id, uuid::Uuid::new_v4()); let id = format!("{}/delete/{}", self.ap_id, uuid::Uuid::new_v4());
@ -274,18 +331,30 @@ impl ApubObjectType for Comment {
send_activity_to_community( send_activity_to_community(
&creator, &creator,
&conn,
&community, &community,
vec![community.get_shared_inbox_url()], vec![community.get_shared_inbox_url()],
undo, undo,
)?; client,
pool,
)
.await?;
Ok(()) Ok(())
} }
fn send_remove(&self, mod_: &User_, conn: &PgConnection) -> Result<(), Error> { async fn send_remove(
let note = self.to_apub(&conn)?; &self,
let post = Post::read(&conn, self.post_id)?; mod_: &User_,
let community = Community::read(&conn, post.community_id)?; client: &Client,
pool: &DbPool,
) -> Result<(), LemmyError> {
let note = self.to_apub(pool).await?;
let post_id = self.post_id;
let post = blocking(pool, move |conn| Post::read(conn, post_id)).await??;
let community_id = post.community_id;
let community = blocking(pool, move |conn| Community::read(conn, community_id)).await??;
let id = format!("{}/remove/{}", self.ap_id, uuid::Uuid::new_v4()); let id = format!("{}/remove/{}", self.ap_id, uuid::Uuid::new_v4());
let mut remove = Remove::default(); let mut remove = Remove::default();
@ -302,18 +371,29 @@ impl ApubObjectType for Comment {
send_activity_to_community( send_activity_to_community(
&mod_, &mod_,
&conn,
&community, &community,
vec![community.get_shared_inbox_url()], vec![community.get_shared_inbox_url()],
remove, remove,
)?; client,
pool,
)
.await?;
Ok(()) Ok(())
} }
fn send_undo_remove(&self, mod_: &User_, conn: &PgConnection) -> Result<(), Error> { async fn send_undo_remove(
let note = self.to_apub(&conn)?; &self,
let post = Post::read(&conn, self.post_id)?; mod_: &User_,
let community = Community::read(&conn, post.community_id)?; client: &Client,
pool: &DbPool,
) -> Result<(), LemmyError> {
let note = self.to_apub(pool).await?;
let post_id = self.post_id;
let post = blocking(pool, move |conn| Post::read(conn, post_id)).await??;
let community_id = post.community_id;
let community = blocking(pool, move |conn| Community::read(conn, community_id)).await??;
// Generate a fake delete activity, with the correct object // Generate a fake delete activity, with the correct object
let id = format!("{}/remove/{}", self.ap_id, uuid::Uuid::new_v4()); let id = format!("{}/remove/{}", self.ap_id, uuid::Uuid::new_v4());
@ -347,20 +427,33 @@ impl ApubObjectType for Comment {
send_activity_to_community( send_activity_to_community(
&mod_, &mod_,
&conn,
&community, &community,
vec![community.get_shared_inbox_url()], vec![community.get_shared_inbox_url()],
undo, undo,
)?; client,
pool,
)
.await?;
Ok(()) Ok(())
} }
} }
#[async_trait::async_trait(?Send)]
impl ApubLikeableType for Comment { impl ApubLikeableType for Comment {
fn send_like(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error> { async fn send_like(
let note = self.to_apub(&conn)?; &self,
let post = Post::read(&conn, self.post_id)?; creator: &User_,
let community = Community::read(&conn, post.community_id)?; client: &Client,
pool: &DbPool,
) -> Result<(), LemmyError> {
let note = self.to_apub(pool).await?;
let post_id = self.post_id;
let post = blocking(pool, move |conn| Post::read(conn, post_id)).await??;
let community_id = post.community_id;
let community = blocking(pool, move |conn| Community::read(conn, community_id)).await??;
let id = format!("{}/like/{}", self.ap_id, uuid::Uuid::new_v4()); let id = format!("{}/like/{}", self.ap_id, uuid::Uuid::new_v4());
let mut like = Like::new(); let mut like = Like::new();
@ -376,18 +469,30 @@ impl ApubLikeableType for Comment {
send_activity_to_community( send_activity_to_community(
&creator, &creator,
&conn,
&community, &community,
vec![community.get_shared_inbox_url()], vec![community.get_shared_inbox_url()],
like, like,
)?; client,
pool,
)
.await?;
Ok(()) Ok(())
} }
fn send_dislike(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error> { async fn send_dislike(
let note = self.to_apub(&conn)?; &self,
let post = Post::read(&conn, self.post_id)?; creator: &User_,
let community = Community::read(&conn, post.community_id)?; client: &Client,
pool: &DbPool,
) -> Result<(), LemmyError> {
let note = self.to_apub(pool).await?;
let post_id = self.post_id;
let post = blocking(pool, move |conn| Post::read(conn, post_id)).await??;
let community_id = post.community_id;
let community = blocking(pool, move |conn| Community::read(conn, community_id)).await??;
let id = format!("{}/dislike/{}", self.ap_id, uuid::Uuid::new_v4()); let id = format!("{}/dislike/{}", self.ap_id, uuid::Uuid::new_v4());
let mut dislike = Dislike::new(); let mut dislike = Dislike::new();
@ -403,18 +508,30 @@ impl ApubLikeableType for Comment {
send_activity_to_community( send_activity_to_community(
&creator, &creator,
&conn,
&community, &community,
vec![community.get_shared_inbox_url()], vec![community.get_shared_inbox_url()],
dislike, dislike,
)?; client,
pool,
)
.await?;
Ok(()) Ok(())
} }
fn send_undo_like(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error> { async fn send_undo_like(
let note = self.to_apub(&conn)?; &self,
let post = Post::read(&conn, self.post_id)?; creator: &User_,
let community = Community::read(&conn, post.community_id)?; client: &Client,
pool: &DbPool,
) -> Result<(), LemmyError> {
let note = self.to_apub(pool).await?;
let post_id = self.post_id;
let post = blocking(pool, move |conn| Post::read(conn, post_id)).await??;
let community_id = post.community_id;
let community = blocking(pool, move |conn| Community::read(conn, community_id)).await??;
let id = format!("{}/dislike/{}", self.ap_id, uuid::Uuid::new_v4()); let id = format!("{}/dislike/{}", self.ap_id, uuid::Uuid::new_v4());
let mut like = Like::new(); let mut like = Like::new();
@ -446,11 +563,13 @@ impl ApubLikeableType for Comment {
send_activity_to_community( send_activity_to_community(
&creator, &creator,
&conn,
&community, &community,
vec![community.get_shared_inbox_url()], vec![community.get_shared_inbox_url()],
undo, undo,
)?; client,
pool,
)
.await?;
Ok(()) Ok(())
} }
} }
@ -464,11 +583,12 @@ struct MentionsAndAddresses {
/// This takes a comment, and builds a list of to_addresses, inboxes, /// This takes a comment, and builds a list of to_addresses, inboxes,
/// and mention tags, so they know where to be sent to. /// and mention tags, so they know where to be sent to.
/// Addresses are the users / addresses that go in the cc field. /// Addresses are the users / addresses that go in the cc field.
fn collect_non_local_mentions_and_addresses( async fn collect_non_local_mentions_and_addresses(
conn: &PgConnection,
content: &str, content: &str,
community: &Community, community: &Community,
) -> Result<MentionsAndAddresses, Error> { client: &Client,
pool: &DbPool,
) -> Result<MentionsAndAddresses, LemmyError> {
let mut addressed_ccs = vec![community.get_followers_url()]; let mut addressed_ccs = vec![community.get_followers_url()];
// Add the mention tag // Add the mention tag
@ -480,14 +600,17 @@ fn collect_non_local_mentions_and_addresses(
// Filter only the non-local ones // Filter only the non-local ones
.filter(|m| !m.is_local()) .filter(|m| !m.is_local())
.collect::<Vec<MentionData>>(); .collect::<Vec<MentionData>>();
let mut mention_inboxes = Vec::new(); let mut mention_inboxes = Vec::new();
for mention in &mentions { for mention in &mentions {
// TODO should it be fetching it every time? // TODO should it be fetching it every time?
if let Ok(actor_id) = fetch_webfinger_url(mention) { if let Ok(actor_id) = fetch_webfinger_url(mention, client).await {
debug!("mention actor_id: {}", actor_id); debug!("mention actor_id: {}", actor_id);
addressed_ccs.push(actor_id.to_owned()); addressed_ccs.push(actor_id.to_owned());
let mention_user = get_or_fetch_and_upsert_remote_user(&actor_id, &conn)?;
let mention_user = get_or_fetch_and_upsert_remote_user(&actor_id, client, pool).await?;
let shared_inbox = mention_user.get_shared_inbox_url(); let shared_inbox = mention_user.get_shared_inbox_url();
mention_inboxes.push(shared_inbox); mention_inboxes.push(shared_inbox);
let mut mention_tag = Mention::new(); let mut mention_tag = Mention::new();
mention_tag mention_tag

View file

@ -12,6 +12,7 @@ use crate::{
GroupExt, GroupExt,
ToApub, ToApub,
}, },
blocking,
convert_datetime, convert_datetime,
db::{ db::{
activity::insert_activity, activity::insert_activity,
@ -21,6 +22,8 @@ use crate::{
}, },
naive_now, naive_now,
routes::DbPoolParam, routes::DbPoolParam,
DbPool,
LemmyError,
}; };
use activitystreams::{ use activitystreams::{
activity::{Accept, Announce, Delete, Remove, Undo}, activity::{Accept, Announce, Delete, Remove, Undo},
@ -35,22 +38,22 @@ use activitystreams::{
}; };
use activitystreams_ext::Ext3; use activitystreams_ext::Ext3;
use activitystreams_new::{activity::Follow, object::Tombstone}; use activitystreams_new::{activity::Follow, object::Tombstone};
use actix_web::{body::Body, web::Path, HttpResponse, Result}; use actix_web::{body::Body, client::Client, web, HttpResponse};
use diesel::PgConnection;
use failure::{Error, _core::fmt::Debug};
use itertools::Itertools; use itertools::Itertools;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::fmt::Debug;
#[derive(Deserialize)] #[derive(Deserialize)]
pub struct CommunityQuery { pub struct CommunityQuery {
community_name: String, community_name: String,
} }
#[async_trait::async_trait(?Send)]
impl ToApub for Community { impl ToApub for Community {
type Response = GroupExt; type Response = GroupExt;
// Turn a Lemmy Community into an ActivityPub group that can be sent out over the network. // Turn a Lemmy Community into an ActivityPub group that can be sent out over the network.
fn to_apub(&self, conn: &PgConnection) -> Result<GroupExt, Error> { async fn to_apub(&self, pool: &DbPool) -> Result<GroupExt, LemmyError> {
let mut group = Group::default(); let mut group = Group::default();
let oprops: &mut ObjectProperties = group.as_mut(); let oprops: &mut ObjectProperties = group.as_mut();
@ -58,10 +61,12 @@ impl ToApub for Community {
// then the rest of the moderators // then the rest of the moderators
// TODO Technically the instance admins can mod the community, but lets // TODO Technically the instance admins can mod the community, but lets
// ignore that for now // ignore that for now
let moderators = CommunityModeratorView::for_community(&conn, self.id)? let id = self.id;
.into_iter() let moderators = blocking(pool, move |conn| {
.map(|m| m.user_actor_id) CommunityModeratorView::for_community(&conn, id)
.collect(); })
.await??;
let moderators = moderators.into_iter().map(|m| m.user_actor_id).collect();
oprops oprops
.set_context_xsd_any_uri(context())? .set_context_xsd_any_uri(context())?
@ -92,7 +97,12 @@ impl ToApub for Community {
.set_endpoints(endpoint_props)? .set_endpoints(endpoint_props)?
.set_followers(self.get_followers_url())?; .set_followers(self.get_followers_url())?;
let group_extension = GroupExtension::new(conn, self.category_id, self.nsfw)?; let nsfw = self.nsfw;
let category_id = self.category_id;
let group_extension = blocking(pool, move |conn| {
GroupExtension::new(conn, category_id, nsfw)
})
.await??;
Ok(Ext3::new( Ok(Ext3::new(
group, group,
@ -102,7 +112,7 @@ impl ToApub for Community {
)) ))
} }
fn to_tombstone(&self) -> Result<Tombstone, Error> { fn to_tombstone(&self) -> Result<Tombstone, LemmyError> {
create_tombstone( create_tombstone(
self.deleted, self.deleted,
&self.actor_id, &self.actor_id,
@ -112,6 +122,7 @@ impl ToApub for Community {
} }
} }
#[async_trait::async_trait(?Send)]
impl ActorType for Community { impl ActorType for Community {
fn actor_id(&self) -> String { fn actor_id(&self) -> String {
self.actor_id.to_owned() self.actor_id.to_owned()
@ -125,7 +136,12 @@ impl ActorType for Community {
} }
/// As a local community, accept the follow request from a remote user. /// As a local community, accept the follow request from a remote user.
fn send_accept_follow(&self, follow: &Follow, conn: &PgConnection) -> Result<(), Error> { async fn send_accept_follow(
&self,
follow: &Follow,
client: &Client,
pool: &DbPool,
) -> Result<(), LemmyError> {
let actor_uri = follow.actor.as_single_xsd_any_uri().unwrap().to_string(); let actor_uri = follow.actor.as_single_xsd_any_uri().unwrap().to_string();
let id = format!("{}/accept/{}", self.actor_id, uuid::Uuid::new_v4()); let id = format!("{}/accept/{}", self.actor_id, uuid::Uuid::new_v4());
@ -140,14 +156,20 @@ impl ActorType for Community {
.set_object_base_box(BaseBox::from_concrete(follow.clone())?)?; .set_object_base_box(BaseBox::from_concrete(follow.clone())?)?;
let to = format!("{}/inbox", actor_uri); let to = format!("{}/inbox", actor_uri);
insert_activity(&conn, self.creator_id, &accept, true)?; insert_activity(self.creator_id, accept.clone(), true, pool).await?;
send_activity(&accept, self, vec![to])?; send_activity(client, &accept, self, vec![to]).await?;
Ok(()) Ok(())
} }
fn send_delete(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error> { async fn send_delete(
let group = self.to_apub(conn)?; &self,
creator: &User_,
client: &Client,
pool: &DbPool,
) -> Result<(), LemmyError> {
let group = self.to_apub(pool).await?;
let id = format!("{}/delete/{}", self.actor_id, uuid::Uuid::new_v4()); let id = format!("{}/delete/{}", self.actor_id, uuid::Uuid::new_v4());
let mut delete = Delete::default(); let mut delete = Delete::default();
@ -162,17 +184,25 @@ impl ActorType for Community {
.set_actor_xsd_any_uri(creator.actor_id.to_owned())? .set_actor_xsd_any_uri(creator.actor_id.to_owned())?
.set_object_base_box(BaseBox::from_concrete(group)?)?; .set_object_base_box(BaseBox::from_concrete(group)?)?;
insert_activity(&conn, self.creator_id, &delete, true)?; insert_activity(self.creator_id, delete.clone(), true, pool).await?;
let inboxes = self.get_follower_inboxes(pool).await?;
// Note: For an accept, since it was automatic, no one pushed a button, // Note: For an accept, since it was automatic, no one pushed a button,
// the community was the actor. // the community was the actor.
// But for delete, the creator is the actor, and does the signing // But for delete, the creator is the actor, and does the signing
send_activity(&delete, creator, self.get_follower_inboxes(&conn)?)?; send_activity(client, &delete, creator, inboxes).await?;
Ok(()) Ok(())
} }
fn send_undo_delete(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error> { async fn send_undo_delete(
let group = self.to_apub(conn)?; &self,
creator: &User_,
client: &Client,
pool: &DbPool,
) -> Result<(), LemmyError> {
let group = self.to_apub(pool).await?;
let id = format!("{}/delete/{}", self.actor_id, uuid::Uuid::new_v4()); let id = format!("{}/delete/{}", self.actor_id, uuid::Uuid::new_v4());
let mut delete = Delete::default(); let mut delete = Delete::default();
@ -203,17 +233,25 @@ impl ActorType for Community {
.set_actor_xsd_any_uri(creator.actor_id.to_owned())? .set_actor_xsd_any_uri(creator.actor_id.to_owned())?
.set_object_base_box(delete)?; .set_object_base_box(delete)?;
insert_activity(&conn, self.creator_id, &undo, true)?; insert_activity(self.creator_id, undo.clone(), true, pool).await?;
let inboxes = self.get_follower_inboxes(pool).await?;
// Note: For an accept, since it was automatic, no one pushed a button, // Note: For an accept, since it was automatic, no one pushed a button,
// the community was the actor. // the community was the actor.
// But for delete, the creator is the actor, and does the signing // But for delete, the creator is the actor, and does the signing
send_activity(&undo, creator, self.get_follower_inboxes(&conn)?)?; send_activity(client, &undo, creator, inboxes).await?;
Ok(()) Ok(())
} }
fn send_remove(&self, mod_: &User_, conn: &PgConnection) -> Result<(), Error> { async fn send_remove(
let group = self.to_apub(conn)?; &self,
mod_: &User_,
client: &Client,
pool: &DbPool,
) -> Result<(), LemmyError> {
let group = self.to_apub(pool).await?;
let id = format!("{}/remove/{}", self.actor_id, uuid::Uuid::new_v4()); let id = format!("{}/remove/{}", self.actor_id, uuid::Uuid::new_v4());
let mut remove = Remove::default(); let mut remove = Remove::default();
@ -228,17 +266,25 @@ impl ActorType for Community {
.set_actor_xsd_any_uri(mod_.actor_id.to_owned())? .set_actor_xsd_any_uri(mod_.actor_id.to_owned())?
.set_object_base_box(BaseBox::from_concrete(group)?)?; .set_object_base_box(BaseBox::from_concrete(group)?)?;
insert_activity(&conn, mod_.id, &remove, true)?; insert_activity(mod_.id, remove.clone(), true, pool).await?;
let inboxes = self.get_follower_inboxes(pool).await?;
// Note: For an accept, since it was automatic, no one pushed a button, // Note: For an accept, since it was automatic, no one pushed a button,
// the community was the actor. // the community was the actor.
// But for delete, the creator is the actor, and does the signing // But for delete, the creator is the actor, and does the signing
send_activity(&remove, mod_, self.get_follower_inboxes(&conn)?)?; send_activity(client, &remove, mod_, inboxes).await?;
Ok(()) Ok(())
} }
fn send_undo_remove(&self, mod_: &User_, conn: &PgConnection) -> Result<(), Error> { async fn send_undo_remove(
let group = self.to_apub(conn)?; &self,
mod_: &User_,
client: &Client,
pool: &DbPool,
) -> Result<(), LemmyError> {
let group = self.to_apub(pool).await?;
let id = format!("{}/remove/{}", self.actor_id, uuid::Uuid::new_v4()); let id = format!("{}/remove/{}", self.actor_id, uuid::Uuid::new_v4());
let mut remove = Remove::default(); let mut remove = Remove::default();
@ -268,51 +314,69 @@ impl ActorType for Community {
.set_actor_xsd_any_uri(mod_.actor_id.to_owned())? .set_actor_xsd_any_uri(mod_.actor_id.to_owned())?
.set_object_base_box(remove)?; .set_object_base_box(remove)?;
insert_activity(&conn, mod_.id, &undo, true)?; insert_activity(mod_.id, undo.clone(), true, pool).await?;
let inboxes = self.get_follower_inboxes(pool).await?;
// Note: For an accept, since it was automatic, no one pushed a button, // Note: For an accept, since it was automatic, no one pushed a button,
// the community was the actor. // the community was the actor.
// But for remove , the creator is the actor, and does the signing // But for remove , the creator is the actor, and does the signing
send_activity(&undo, mod_, self.get_follower_inboxes(&conn)?)?; send_activity(client, &undo, mod_, inboxes).await?;
Ok(()) Ok(())
} }
/// For a given community, returns the inboxes of all followers. /// For a given community, returns the inboxes of all followers.
fn get_follower_inboxes(&self, conn: &PgConnection) -> Result<Vec<String>, Error> { async fn get_follower_inboxes(&self, pool: &DbPool) -> Result<Vec<String>, LemmyError> {
Ok( let id = self.id;
CommunityFollowerView::for_community(conn, self.id)?
let inboxes = blocking(pool, move |conn| {
CommunityFollowerView::for_community(conn, id)
})
.await??;
let inboxes = inboxes
.into_iter() .into_iter()
.map(|c| get_shared_inbox(&c.user_actor_id)) .map(|c| get_shared_inbox(&c.user_actor_id))
.filter(|s| !s.is_empty()) .filter(|s| !s.is_empty())
.unique() .unique()
.collect(), .collect();
)
Ok(inboxes)
} }
fn send_follow(&self, _follow_actor_id: &str, _conn: &PgConnection) -> Result<(), Error> { async fn send_follow(
&self,
_follow_actor_id: &str,
_client: &Client,
_pool: &DbPool,
) -> Result<(), LemmyError> {
unimplemented!() unimplemented!()
} }
fn send_unfollow(&self, _follow_actor_id: &str, _conn: &PgConnection) -> Result<(), Error> { async fn send_unfollow(
&self,
_follow_actor_id: &str,
_client: &Client,
_pool: &DbPool,
) -> Result<(), LemmyError> {
unimplemented!() unimplemented!()
} }
} }
#[async_trait::async_trait(?Send)]
impl FromApub for CommunityForm { impl FromApub for CommunityForm {
type ApubType = GroupExt; type ApubType = GroupExt;
/// Parse an ActivityPub group received from another instance into a Lemmy community. /// Parse an ActivityPub group received from another instance into a Lemmy community.
fn from_apub(group: &GroupExt, conn: &PgConnection) -> Result<Self, Error> { async fn from_apub(group: &GroupExt, client: &Client, pool: &DbPool) -> Result<Self, LemmyError> {
let group_extensions: &GroupExtension = &group.ext_one; let group_extensions: &GroupExtension = &group.ext_one;
let oprops = &group.inner.object_props; let oprops = &group.inner.object_props;
let aprops = &group.ext_two; let aprops = &group.ext_two;
let public_key: &PublicKey = &group.ext_three.public_key; let public_key: &PublicKey = &group.ext_three.public_key;
let mut creator_and_moderator_uris = oprops.get_many_attributed_to_xsd_any_uris().unwrap(); let mut creator_and_moderator_uris = oprops.get_many_attributed_to_xsd_any_uris().unwrap();
let creator = creator_and_moderator_uris let creator_uri = creator_and_moderator_uris.next().unwrap();
.next()
.map(|c| get_or_fetch_and_upsert_remote_user(&c.to_string(), &conn).unwrap()) let creator = get_or_fetch_and_upsert_remote_user(creator_uri.as_str(), client, pool).await?;
.unwrap();
Ok(CommunityForm { Ok(CommunityForm {
name: oprops.get_name_xsd_string().unwrap().to_string(), name: oprops.get_name_xsd_string().unwrap().to_string(),
@ -342,14 +406,18 @@ impl FromApub for CommunityForm {
/// Return the community json over HTTP. /// Return the community json over HTTP.
pub async fn get_apub_community_http( pub async fn get_apub_community_http(
info: Path<CommunityQuery>, info: web::Path<CommunityQuery>,
db: DbPoolParam, db: DbPoolParam,
) -> Result<HttpResponse<Body>, Error> { ) -> Result<HttpResponse<Body>, LemmyError> {
let community = Community::read_from_name(&&db.get()?, &info.community_name)?; let community = blocking(&db, move |conn| {
Community::read_from_name(conn, &info.community_name)
})
.await??;
if !community.deleted { if !community.deleted {
Ok(create_apub_response( let apub = community.to_apub(&db).await?;
&community.to_apub(&db.get().unwrap())?,
)) Ok(create_apub_response(&apub))
} else { } else {
Ok(create_apub_tombstone_response(&community.to_tombstone()?)) Ok(create_apub_tombstone_response(&community.to_tombstone()?))
} }
@ -357,15 +425,19 @@ pub async fn get_apub_community_http(
/// Returns an empty followers collection, only populating the size (for privacy). /// Returns an empty followers collection, only populating the size (for privacy).
pub async fn get_apub_community_followers( pub async fn get_apub_community_followers(
info: Path<CommunityQuery>, info: web::Path<CommunityQuery>,
db: DbPoolParam, db: DbPoolParam,
) -> Result<HttpResponse<Body>, Error> { ) -> Result<HttpResponse<Body>, LemmyError> {
let community = Community::read_from_name(&&db.get()?, &info.community_name)?; let community = blocking(&db, move |conn| {
Community::read_from_name(&conn, &info.community_name)
})
.await??;
let conn = db.get()?; let community_id = community.id;
let community_followers = blocking(&db, move |conn| {
//As we are an object, we validated that the community id was valid CommunityFollowerView::for_community(&conn, community_id)
let community_followers = CommunityFollowerView::for_community(&conn, community.id).unwrap(); })
.await??;
let mut collection = UnorderedCollection::default(); let mut collection = UnorderedCollection::default();
let oprops: &mut ObjectProperties = collection.as_mut(); let oprops: &mut ObjectProperties = collection.as_mut();
@ -379,12 +451,13 @@ pub async fn get_apub_community_followers(
} }
impl Community { impl Community {
pub fn do_announce<A>( pub async fn do_announce<A>(
activity: A, activity: A,
community: &Community, community: &Community,
sender: &dyn ActorType, sender: &dyn ActorType,
conn: &PgConnection, client: &Client,
) -> Result<HttpResponse, Error> pool: &DbPool,
) -> Result<HttpResponse, LemmyError>
where where
A: Activity + Base + Serialize + Debug, A: Activity + Base + Serialize + Debug,
{ {
@ -399,15 +472,16 @@ impl Community {
.set_actor_xsd_any_uri(community.actor_id.to_owned())? .set_actor_xsd_any_uri(community.actor_id.to_owned())?
.set_object_base_box(BaseBox::from_concrete(activity)?)?; .set_object_base_box(BaseBox::from_concrete(activity)?)?;
insert_activity(&conn, community.creator_id, &announce, true)?; insert_activity(community.creator_id, announce.clone(), true, pool).await?;
// dont send to the instance where the activity originally came from, because that would result // dont send to the instance where the activity originally came from, because that would result
// in a database error (same data inserted twice) // in a database error (same data inserted twice)
let mut to = community.get_follower_inboxes(&conn)?; let mut to = community.get_follower_inboxes(pool).await?;
// this seems to be the "easiest" stable alternative for remove_item() // this seems to be the "easiest" stable alternative for remove_item()
to.retain(|x| *x != sender.get_shared_inbox_url()); to.retain(|x| *x != sender.get_shared_inbox_url());
send_activity(&announce, community, to)?; send_activity(client, &announce, community, to).await?;
Ok(HttpResponse::Ok().finish()) Ok(HttpResponse::Ok().finish())
} }

View file

@ -4,6 +4,7 @@ use crate::{
fetcher::{get_or_fetch_and_upsert_remote_community, get_or_fetch_and_upsert_remote_user}, fetcher::{get_or_fetch_and_upsert_remote_community, get_or_fetch_and_upsert_remote_user},
ActorType, ActorType,
}, },
blocking,
db::{ db::{
activity::insert_activity, activity::insert_activity,
community::{Community, CommunityFollower, CommunityFollowerForm}, community::{Community, CommunityFollower, CommunityFollowerForm},
@ -11,14 +12,14 @@ use crate::{
Followable, Followable,
}, },
routes::{ChatServerParam, DbPoolParam}, routes::{ChatServerParam, DbPoolParam},
LemmyError,
}; };
use activitystreams::activity::Undo; use activitystreams::activity::Undo;
use activitystreams_new::activity::Follow; use activitystreams_new::activity::Follow;
use actix_web::{web, HttpRequest, HttpResponse, Result}; use actix_web::{client::Client, web, HttpRequest, HttpResponse};
use diesel::PgConnection;
use failure::{Error, _core::fmt::Debug};
use log::debug; use log::debug;
use serde::Deserialize; use serde::Deserialize;
use std::fmt::Debug;
#[serde(untagged)] #[serde(untagged)]
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug)]
@ -28,7 +29,7 @@ pub enum CommunityAcceptedObjects {
} }
impl CommunityAcceptedObjects { impl CommunityAcceptedObjects {
fn follow(&self) -> Result<Follow, Error> { fn follow(&self) -> Result<Follow, LemmyError> {
match self { match self {
CommunityAcceptedObjects::Follow(f) => Ok(f.to_owned()), CommunityAcceptedObjects::Follow(f) => Ok(f.to_owned()),
CommunityAcceptedObjects::Undo(u) => Ok( CommunityAcceptedObjects::Undo(u) => Ok(
@ -49,16 +50,22 @@ pub async fn community_inbox(
input: web::Json<CommunityAcceptedObjects>, input: web::Json<CommunityAcceptedObjects>,
path: web::Path<String>, path: web::Path<String>,
db: DbPoolParam, db: DbPoolParam,
client: web::Data<Client>,
_chat_server: ChatServerParam, _chat_server: ChatServerParam,
) -> Result<HttpResponse, Error> { ) -> Result<HttpResponse, LemmyError> {
let input = input.into_inner(); let input = input.into_inner();
let conn = db.get()?;
let community = Community::read_from_name(&conn, &path.into_inner())?; let path = path.into_inner();
let community = blocking(&db, move |conn| Community::read_from_name(&conn, &path)).await??;
if !community.local { if !community.local {
return Err(format_err!( return Err(
format_err!(
"Received activity is addressed to remote community {}", "Received activity is addressed to remote community {}",
&community.actor_id &community.actor_id
)); )
.into(),
);
} }
debug!( debug!(
"Community {} received activity {:?}", "Community {} received activity {:?}",
@ -68,28 +75,27 @@ pub async fn community_inbox(
let user_uri = follow.actor.as_single_xsd_any_uri().unwrap().to_string(); let user_uri = follow.actor.as_single_xsd_any_uri().unwrap().to_string();
let community_uri = follow.object.as_single_xsd_any_uri().unwrap().to_string(); let community_uri = follow.object.as_single_xsd_any_uri().unwrap().to_string();
let conn = db.get()?; let user = get_or_fetch_and_upsert_remote_user(&user_uri, &client, &db).await?;
let community = get_or_fetch_and_upsert_remote_community(&community_uri, &client, &db).await?;
let user = get_or_fetch_and_upsert_remote_user(&user_uri, &conn)?;
let community = get_or_fetch_and_upsert_remote_community(&community_uri, &conn)?;
verify(&request, &user)?; verify(&request, &user)?;
match input { match input {
CommunityAcceptedObjects::Follow(f) => handle_follow(&f, &user, &community, &conn), CommunityAcceptedObjects::Follow(f) => handle_follow(f, user, community, &client, db).await,
CommunityAcceptedObjects::Undo(u) => handle_undo_follow(&u, &user, &community, &conn), CommunityAcceptedObjects::Undo(u) => handle_undo_follow(u, user, community, db).await,
} }
} }
/// Handle a follow request from a remote user, adding it to the local database and returning an /// Handle a follow request from a remote user, adding it to the local database and returning an
/// Accept activity. /// Accept activity.
fn handle_follow( async fn handle_follow(
follow: &Follow, follow: Follow,
user: &User_, user: User_,
community: &Community, community: Community,
conn: &PgConnection, client: &Client,
) -> Result<HttpResponse, Error> { db: DbPoolParam,
insert_activity(&conn, user.id, &follow, false)?; ) -> Result<HttpResponse, LemmyError> {
insert_activity(user.id, follow.clone(), false, &db).await?;
let community_follower_form = CommunityFollowerForm { let community_follower_form = CommunityFollowerForm {
community_id: community.id, community_id: community.id,
@ -97,27 +103,34 @@ fn handle_follow(
}; };
// This will fail if they're already a follower, but ignore the error. // This will fail if they're already a follower, but ignore the error.
CommunityFollower::follow(&conn, &community_follower_form).ok(); blocking(&db, move |conn| {
CommunityFollower::follow(&conn, &community_follower_form).ok()
})
.await?;
community.send_accept_follow(&follow, &conn)?; community.send_accept_follow(&follow, &client, &db).await?;
Ok(HttpResponse::Ok().finish()) Ok(HttpResponse::Ok().finish())
} }
fn handle_undo_follow( async fn handle_undo_follow(
undo: &Undo, undo: Undo,
user: &User_, user: User_,
community: &Community, community: Community,
conn: &PgConnection, db: DbPoolParam,
) -> Result<HttpResponse, Error> { ) -> Result<HttpResponse, LemmyError> {
insert_activity(&conn, user.id, &undo, false)?; insert_activity(user.id, undo, false, &db).await?;
let community_follower_form = CommunityFollowerForm { let community_follower_form = CommunityFollowerForm {
community_id: community.id, community_id: community.id,
user_id: user.id, user_id: user.id,
}; };
CommunityFollower::unfollow(&conn, &community_follower_form).ok(); // This will fail if they aren't a follower, but ignore the error.
blocking(&db, move |conn| {
CommunityFollower::unfollow(&conn, &community_follower_form).ok()
})
.await?;
Ok(HttpResponse::Ok().finish()) Ok(HttpResponse::Ok().finish())
} }

View file

@ -1,7 +1,9 @@
use crate::db::{category::Category, Crud}; use crate::{
db::{category::Category, Crud},
LemmyError,
};
use activitystreams::{ext::Extension, Actor}; use activitystreams::{ext::Extension, Actor};
use diesel::PgConnection; use diesel::PgConnection;
use failure::Error;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, Default, Deserialize, Serialize)] #[derive(Clone, Debug, Default, Deserialize, Serialize)]
@ -24,7 +26,7 @@ impl GroupExtension {
conn: &PgConnection, conn: &PgConnection,
category_id: i32, category_id: i32,
sensitive: bool, sensitive: bool,
) -> Result<GroupExtension, Error> { ) -> Result<GroupExtension, LemmyError> {
let category = Category::read(conn, category_id)?; let category = Category::read(conn, category_id)?;
let group_category = GroupCategory { let group_category = GroupCategory {
identifier: category_id.to_string(), identifier: category_id.to_string(),

View file

@ -1,9 +1,10 @@
use crate::apub::ActorType; use crate::{apub::ActorType, LemmyError};
use activitystreams::ext::Extension; use activitystreams::ext::Extension;
use actix_web::HttpRequest; use actix_web::{client::ClientRequest, HttpRequest};
use failure::Error; use http_signature_normalization_actix::{
use http::request::Builder; digest::{DigestClient, SignExt},
use http_signature_normalization::Config; Config,
};
use log::debug; use log::debug;
use openssl::{ use openssl::{
hash::MessageDigest, hash::MessageDigest,
@ -12,7 +13,7 @@ use openssl::{
sign::{Signer, Verifier}, sign::{Signer, Verifier},
}; };
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::collections::BTreeMap; use sha2::{Digest, Sha256};
lazy_static! { lazy_static! {
static ref HTTP_SIG_CONFIG: Config = Config::new(); static ref HTTP_SIG_CONFIG: Config = Config::new();
@ -24,7 +25,7 @@ pub struct Keypair {
} }
/// Generate the asymmetric keypair for ActivityPub HTTP signatures. /// Generate the asymmetric keypair for ActivityPub HTTP signatures.
pub fn generate_actor_keypair() -> Result<Keypair, Error> { pub fn generate_actor_keypair() -> Result<Keypair, LemmyError> {
let rsa = Rsa::generate(2048)?; let rsa = Rsa::generate(2048)?;
let pkey = PKey::from_rsa(rsa)?; let pkey = PKey::from_rsa(rsa)?;
let public_key = pkey.public_key_to_pem()?; let public_key = pkey.public_key_to_pem()?;
@ -36,56 +37,41 @@ pub fn generate_actor_keypair() -> Result<Keypair, Error> {
} }
/// Signs request headers with the given keypair. /// Signs request headers with the given keypair.
pub fn sign(request: &Builder, actor: &dyn ActorType) -> Result<String, Error> { pub async fn sign(
request: ClientRequest,
actor: &dyn ActorType,
activity: String,
) -> Result<DigestClient<String>, LemmyError> {
let signing_key_id = format!("{}#main-key", actor.actor_id()); let signing_key_id = format!("{}#main-key", actor.actor_id());
let private_key = actor.private_key();
let headers = request let digest_client = request
.headers_ref() .signature_with_digest(
.unwrap() HTTP_SIG_CONFIG.clone(),
.iter() signing_key_id,
.map(|h| -> Result<(String, String), Error> { Sha256::new(),
Ok((h.0.as_str().to_owned(), h.1.to_str()?.to_owned())) activity,
}) move |signing_string| {
.collect::<Result<BTreeMap<String, String>, Error>>()?; let private_key = PKey::private_key_from_pem(private_key.as_bytes())?;
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(actor.private_key().as_bytes())?;
let mut signer = Signer::new(MessageDigest::sha256(), &private_key).unwrap(); let mut signer = Signer::new(MessageDigest::sha256(), &private_key).unwrap();
signer.update(signing_string.as_bytes()).unwrap(); signer.update(signing_string.as_bytes()).unwrap();
Ok(base64::encode(signer.sign_to_vec()?)) as Result<_, Error>
})?
.signature_header();
Ok(signature_header_value) Ok(base64::encode(signer.sign_to_vec()?)) as Result<_, LemmyError>
},
)
.await?;
Ok(digest_client)
} }
pub fn verify(request: &HttpRequest, actor: &dyn ActorType) -> Result<(), Error> { pub fn verify(request: &HttpRequest, actor: &dyn ActorType) -> Result<(), LemmyError> {
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 let verified = HTTP_SIG_CONFIG
.begin_verify( .begin_verify(
request.method().as_str(), request.method(),
request.uri().path_and_query().unwrap().as_str(), request.uri().path_and_query(),
headers, request.headers().clone(),
)? )?
.verify(|signature, signing_string| -> Result<bool, Error> { .verify(|signature, signing_string| -> Result<bool, LemmyError> {
debug!( debug!(
"Verifying with key {}, message {}", "Verifying with key {}, message {}",
&actor.public_key(), &actor.public_key(),
@ -101,10 +87,7 @@ pub fn verify(request: &HttpRequest, actor: &dyn ActorType) -> Result<(), Error>
debug!("verified signature for {}", &request.uri()); debug!("verified signature for {}", &request.uri());
Ok(()) Ok(())
} else { } else {
Err(format_err!( Err(format_err!("Invalid signature on request: {}", &request.uri()).into())
"Invalid signature on request: {}",
&request.uri()
))
} }
} }

View file

@ -1,15 +1,14 @@
use activitystreams::object::Note; use activitystreams::object::Note;
use actix_web::Result; use actix_web::client::Client;
use diesel::{result::Error::NotFound, PgConnection}; use diesel::{result::Error::NotFound, PgConnection};
use failure::{Error, _core::fmt::Debug};
use isahc::prelude::*;
use log::debug; use log::debug;
use serde::Deserialize; use serde::Deserialize;
use std::time::Duration; use std::{fmt::Debug, time::Duration};
use url::Url; use url::Url;
use crate::{ use crate::{
api::site::SearchResponse, api::site::SearchResponse,
blocking,
db::{ db::{
comment::{Comment, CommentForm}, comment::{Comment, CommentForm},
comment_view::CommentView, comment_view::CommentView,
@ -23,7 +22,10 @@ use crate::{
SearchType, SearchType,
}, },
naive_now, naive_now,
request::{retry, RecvError},
routes::nodeinfo::{NodeInfo, NodeInfoWellKnown}, routes::nodeinfo::{NodeInfo, NodeInfoWellKnown},
DbPool,
LemmyError,
}; };
use crate::{ use crate::{
@ -43,36 +45,50 @@ use chrono::NaiveDateTime;
static ACTOR_REFETCH_INTERVAL_SECONDS: i64 = 24 * 60 * 60; static ACTOR_REFETCH_INTERVAL_SECONDS: i64 = 24 * 60 * 60;
// Fetch nodeinfo metadata from a remote instance. // Fetch nodeinfo metadata from a remote instance.
fn _fetch_node_info(domain: &str) -> Result<NodeInfo, Error> { async fn _fetch_node_info(client: &Client, domain: &str) -> Result<NodeInfo, LemmyError> {
let well_known_uri = Url::parse(&format!( let well_known_uri = Url::parse(&format!(
"{}://{}/.well-known/nodeinfo", "{}://{}/.well-known/nodeinfo",
get_apub_protocol_string(), get_apub_protocol_string(),
domain domain
))?; ))?;
let well_known = fetch_remote_object::<NodeInfoWellKnown>(&well_known_uri)?;
Ok(fetch_remote_object::<NodeInfo>(&well_known.links.href)?) let well_known = fetch_remote_object::<NodeInfoWellKnown>(client, &well_known_uri).await?;
let nodeinfo = fetch_remote_object::<NodeInfo>(client, &well_known.links.href).await?;
Ok(nodeinfo)
} }
/// Fetch any type of ActivityPub object, handling things like HTTP headers, deserialisation, /// Fetch any type of ActivityPub object, handling things like HTTP headers, deserialisation,
/// timeouts etc. /// timeouts etc.
pub fn fetch_remote_object<Response>(url: &Url) -> Result<Response, Error> pub async fn fetch_remote_object<Response>(
client: &Client,
url: &Url,
) -> Result<Response, LemmyError>
where where
Response: for<'de> Deserialize<'de>, Response: for<'de> Deserialize<'de>,
{ {
if !is_apub_id_valid(&url) { if !is_apub_id_valid(&url) {
return Err(format_err!("Activitypub uri invalid or blocked: {}", url)); return Err(format_err!("Activitypub uri invalid or blocked: {}", url).into());
} }
// TODO: this function should return a future
let timeout = Duration::from_secs(60); let timeout = Duration::from_secs(60);
let text = Request::get(url.as_str())
let json = retry(|| {
client
.get(url.as_str())
.header("Accept", APUB_JSON_CONTENT_TYPE) .header("Accept", APUB_JSON_CONTENT_TYPE)
.connect_timeout(timeout)
.timeout(timeout) .timeout(timeout)
.body(())? .send()
.send()? })
.text()?; .await?
let res: Response = serde_json::from_str(&text)?; .json()
Ok(res) .await
.map_err(|e| {
debug!("Receive error, {}", e);
RecvError(e.to_string())
})?;
Ok(json)
} }
/// The types of ActivityPub objects that can be fetched directly by searching for their ID. /// The types of ActivityPub objects that can be fetched directly by searching for their ID.
@ -92,7 +108,11 @@ pub enum SearchAcceptedObjects {
/// http://lemmy_alpha:8540/u/lemmy_alpha, or @lemmy_alpha@lemmy_alpha:8540 /// http://lemmy_alpha:8540/u/lemmy_alpha, or @lemmy_alpha@lemmy_alpha:8540
/// http://lemmy_alpha:8540/post/3 /// http://lemmy_alpha:8540/post/3
/// http://lemmy_alpha:8540/comment/2 /// http://lemmy_alpha:8540/comment/2
pub fn search_by_apub_id(query: &str, conn: &PgConnection) -> Result<SearchResponse, Error> { pub async fn search_by_apub_id(
query: &str,
client: &Client,
pool: &DbPool,
) -> Result<SearchResponse, LemmyError> {
// Parse the shorthand query url // Parse the shorthand query url
let query_url = if query.contains('@') { let query_url = if query.contains('@') {
debug!("{}", query); debug!("{}", query);
@ -107,10 +127,10 @@ pub fn search_by_apub_id(query: &str, conn: &PgConnection) -> Result<SearchRespo
let split2 = split[0].split('!').collect::<Vec<&str>>(); let split2 = split[0].split('!').collect::<Vec<&str>>();
(format!("/c/{}", split2[1]), split[1]) (format!("/c/{}", split2[1]), split[1])
} else { } else {
return Err(format_err!("Invalid search query: {}", query)); return Err(format_err!("Invalid search query: {}", query).into());
} }
} else { } else {
return Err(format_err!("Invalid search query: {}", query)); return Err(format_err!("Invalid search query: {}", query).into());
}; };
let url = format!("{}://{}{}", get_apub_protocol_string(), instance, name); let url = format!("{}://{}{}", get_apub_protocol_string(), instance, name);
@ -126,22 +146,41 @@ pub fn search_by_apub_id(query: &str, conn: &PgConnection) -> Result<SearchRespo
communities: vec![], communities: vec![],
users: vec![], users: vec![],
}; };
match fetch_remote_object::<SearchAcceptedObjects>(&query_url)? {
let response = match fetch_remote_object::<SearchAcceptedObjects>(client, &query_url).await? {
SearchAcceptedObjects::Person(p) => { SearchAcceptedObjects::Person(p) => {
let user_uri = p.inner.object_props.get_id().unwrap().to_string(); let user_uri = p.inner.object_props.get_id().unwrap().to_string();
let user = get_or_fetch_and_upsert_remote_user(&user_uri, &conn)?;
response.users = vec![UserView::read(conn, user.id)?]; let user = get_or_fetch_and_upsert_remote_user(&user_uri, client, pool).await?;
response.users = vec![blocking(pool, move |conn| UserView::read(conn, user.id)).await??];
response
} }
SearchAcceptedObjects::Group(g) => { SearchAcceptedObjects::Group(g) => {
let community_uri = g.inner.object_props.get_id().unwrap().to_string(); let community_uri = g.inner.object_props.get_id().unwrap().to_string();
let community = get_or_fetch_and_upsert_remote_community(&community_uri, &conn)?;
let community =
get_or_fetch_and_upsert_remote_community(&community_uri, client, pool).await?;
// TODO Maybe at some point in the future, fetch all the history of a community // TODO Maybe at some point in the future, fetch all the history of a community
// fetch_community_outbox(&c, conn)?; // fetch_community_outbox(&c, conn)?;
response.communities = vec![CommunityView::read(conn, community.id, None)?]; response.communities = vec![
blocking(pool, move |conn| {
CommunityView::read(conn, community.id, None)
})
.await??,
];
response
} }
SearchAcceptedObjects::Page(p) => { SearchAcceptedObjects::Page(p) => {
let p = upsert_post(&PostForm::from_apub(&p, conn)?, conn)?; let post_form = PostForm::from_apub(&p, client, pool).await?;
response.posts = vec![PostView::read(conn, p.id, None)?];
let p = blocking(pool, move |conn| upsert_post(&post_form, conn)).await??;
response.posts = vec![blocking(pool, move |conn| PostView::read(conn, p.id, None)).await??];
response
} }
SearchAcceptedObjects::Comment(c) => { SearchAcceptedObjects::Comment(c) => {
let post_url = c let post_url = c
@ -151,41 +190,59 @@ pub fn search_by_apub_id(query: &str, conn: &PgConnection) -> Result<SearchRespo
.next() .next()
.unwrap() .unwrap()
.to_string(); .to_string();
// TODO: also fetch parent comments if any // TODO: also fetch parent comments if any
let post = fetch_remote_object(&Url::parse(&post_url)?)?; let post = fetch_remote_object(client, &Url::parse(&post_url)?).await?;
upsert_post(&PostForm::from_apub(&post, conn)?, conn)?; let post_form = PostForm::from_apub(&post, client, pool).await?;
let c = upsert_comment(&CommentForm::from_apub(&c, conn)?, conn)?; let comment_form = CommentForm::from_apub(&c, client, pool).await?;
response.comments = vec![CommentView::read(conn, c.id, None)?];
} blocking(pool, move |conn| upsert_post(&post_form, conn)).await??;
let c = blocking(pool, move |conn| upsert_comment(&comment_form, conn)).await??;
response.comments =
vec![blocking(pool, move |conn| CommentView::read(conn, c.id, None)).await??];
response
} }
};
Ok(response) Ok(response)
} }
/// Check if a remote user exists, create if not found, if its too old update it.Fetch a user, insert/update it in the database and return the user. /// Check if a remote user exists, create if not found, if its too old update it.Fetch a user, insert/update it in the database and return the user.
pub fn get_or_fetch_and_upsert_remote_user( pub async fn get_or_fetch_and_upsert_remote_user(
apub_id: &str, apub_id: &str,
conn: &PgConnection, client: &Client,
) -> Result<User_, Error> { pool: &DbPool,
match User_::read_from_actor_id(&conn, &apub_id) { ) -> Result<User_, LemmyError> {
Ok(u) => { let apub_id_owned = apub_id.to_owned();
let user = blocking(pool, move |conn| {
User_::read_from_actor_id(conn, &apub_id_owned)
})
.await?;
match user {
// If its older than a day, re-fetch it // If its older than a day, re-fetch it
if !u.local && should_refetch_actor(u.last_refreshed_at) { Ok(u) if !u.local && should_refetch_actor(u.last_refreshed_at) => {
debug!("Fetching and updating from remote user: {}", apub_id); debug!("Fetching and updating from remote user: {}", apub_id);
let person = fetch_remote_object::<PersonExt>(&Url::parse(apub_id)?)?; let person = fetch_remote_object::<PersonExt>(client, &Url::parse(apub_id)?).await?;
let mut uf = UserForm::from_apub(&person, &conn)?;
let mut uf = UserForm::from_apub(&person, client, pool).await?;
uf.last_refreshed_at = Some(naive_now()); uf.last_refreshed_at = Some(naive_now());
Ok(User_::update(&conn, u.id, &uf)?) let user = blocking(pool, move |conn| User_::update(conn, u.id, &uf)).await??;
} else {
Ok(u) Ok(user)
}
} }
Ok(u) => Ok(u),
Err(NotFound {}) => { Err(NotFound {}) => {
debug!("Fetching and creating remote user: {}", apub_id); debug!("Fetching and creating remote user: {}", apub_id);
let person = fetch_remote_object::<PersonExt>(&Url::parse(apub_id)?)?; let person = fetch_remote_object::<PersonExt>(client, &Url::parse(apub_id)?).await?;
let uf = UserForm::from_apub(&person, &conn)?;
Ok(User_::create(conn, &uf)?) let uf = UserForm::from_apub(&person, client, pool).await?;
let user = blocking(pool, move |conn| User_::create(conn, &uf)).await??;
Ok(user)
} }
Err(e) => Err(Error::from(e)), Err(e) => Err(e.into()),
} }
} }
@ -204,27 +261,35 @@ fn should_refetch_actor(last_refreshed: NaiveDateTime) -> bool {
} }
/// Check if a remote community exists, create if not found, if its too old update it.Fetch a community, insert/update it in the database and return the community. /// Check if a remote community exists, create if not found, if its too old update it.Fetch a community, insert/update it in the database and return the community.
pub fn get_or_fetch_and_upsert_remote_community( pub async fn get_or_fetch_and_upsert_remote_community(
apub_id: &str, apub_id: &str,
conn: &PgConnection, client: &Client,
) -> Result<Community, Error> { pool: &DbPool,
match Community::read_from_actor_id(&conn, &apub_id) { ) -> Result<Community, LemmyError> {
Ok(c) => { let apub_id_owned = apub_id.to_owned();
if !c.local && should_refetch_actor(c.last_refreshed_at) { let community = blocking(pool, move |conn| {
Community::read_from_actor_id(conn, &apub_id_owned)
})
.await?;
match community {
Ok(c) if !c.local && should_refetch_actor(c.last_refreshed_at) => {
debug!("Fetching and updating from remote community: {}", apub_id); debug!("Fetching and updating from remote community: {}", apub_id);
let group = fetch_remote_object::<GroupExt>(&Url::parse(apub_id)?)?; let group = fetch_remote_object::<GroupExt>(client, &Url::parse(apub_id)?).await?;
let mut cf = CommunityForm::from_apub(&group, conn)?;
let mut cf = CommunityForm::from_apub(&group, client, pool).await?;
cf.last_refreshed_at = Some(naive_now()); cf.last_refreshed_at = Some(naive_now());
Ok(Community::update(&conn, c.id, &cf)?) let community = blocking(pool, move |conn| Community::update(conn, c.id, &cf)).await??;
} else {
Ok(c) Ok(community)
}
} }
Ok(c) => Ok(c),
Err(NotFound {}) => { Err(NotFound {}) => {
debug!("Fetching and creating remote community: {}", apub_id); debug!("Fetching and creating remote community: {}", apub_id);
let group = fetch_remote_object::<GroupExt>(&Url::parse(apub_id)?)?; let group = fetch_remote_object::<GroupExt>(client, &Url::parse(apub_id)?).await?;
let cf = CommunityForm::from_apub(&group, conn)?;
let community = Community::create(conn, &cf)?; let cf = CommunityForm::from_apub(&group, client, pool).await?;
let community = blocking(pool, move |conn| Community::create(conn, &cf)).await??;
// Also add the community moderators too // Also add the community moderators too
let creator_and_moderator_uris = group let creator_and_moderator_uris = group
@ -232,74 +297,105 @@ pub fn get_or_fetch_and_upsert_remote_community(
.object_props .object_props
.get_many_attributed_to_xsd_any_uris() .get_many_attributed_to_xsd_any_uris()
.unwrap(); .unwrap();
let creator_and_moderators = creator_and_moderator_uris
.map(|c| get_or_fetch_and_upsert_remote_user(&c.to_string(), &conn).unwrap())
.collect::<Vec<User_>>();
let mut creator_and_moderators = Vec::new();
for uri in creator_and_moderator_uris {
let c_or_m = get_or_fetch_and_upsert_remote_user(uri.as_str(), client, pool).await?;
creator_and_moderators.push(c_or_m);
}
let community_id = community.id;
blocking(pool, move |conn| {
for mod_ in creator_and_moderators { for mod_ in creator_and_moderators {
let community_moderator_form = CommunityModeratorForm { let community_moderator_form = CommunityModeratorForm {
community_id: community.id, community_id,
user_id: mod_.id, user_id: mod_.id,
}; };
CommunityModerator::join(&conn, &community_moderator_form)?;
CommunityModerator::join(conn, &community_moderator_form)?;
} }
Ok(()) as Result<(), LemmyError>
})
.await??;
Ok(community) Ok(community)
} }
Err(e) => Err(Error::from(e)), Err(e) => Err(e.into()),
} }
} }
fn upsert_post(post_form: &PostForm, conn: &PgConnection) -> Result<Post, Error> { fn upsert_post(post_form: &PostForm, conn: &PgConnection) -> Result<Post, LemmyError> {
let existing = Post::read_from_apub_id(conn, &post_form.ap_id); let existing = Post::read_from_apub_id(conn, &post_form.ap_id);
match existing { match existing {
Err(NotFound {}) => Ok(Post::create(conn, &post_form)?), Err(NotFound {}) => Ok(Post::create(conn, &post_form)?),
Ok(p) => Ok(Post::update(conn, p.id, &post_form)?), Ok(p) => Ok(Post::update(conn, p.id, &post_form)?),
Err(e) => Err(Error::from(e)), Err(e) => Err(e.into()),
} }
} }
pub fn get_or_fetch_and_insert_remote_post( pub async fn get_or_fetch_and_insert_remote_post(
post_ap_id: &str, post_ap_id: &str,
conn: &PgConnection, client: &Client,
) -> Result<Post, Error> { pool: &DbPool,
match Post::read_from_apub_id(conn, post_ap_id) { ) -> Result<Post, LemmyError> {
let post_ap_id_owned = post_ap_id.to_owned();
let post = blocking(pool, move |conn| {
Post::read_from_apub_id(conn, &post_ap_id_owned)
})
.await?;
match post {
Ok(p) => Ok(p), Ok(p) => Ok(p),
Err(NotFound {}) => { Err(NotFound {}) => {
debug!("Fetching and creating remote post: {}", post_ap_id); debug!("Fetching and creating remote post: {}", post_ap_id);
let post = fetch_remote_object::<PageExt>(&Url::parse(post_ap_id)?)?; let post = fetch_remote_object::<PageExt>(client, &Url::parse(post_ap_id)?).await?;
let post_form = PostForm::from_apub(&post, conn)?; let post_form = PostForm::from_apub(&post, client, pool).await?;
Ok(Post::create(conn, &post_form)?)
let post = blocking(pool, move |conn| Post::create(conn, &post_form)).await??;
Ok(post)
} }
Err(e) => Err(Error::from(e)), Err(e) => Err(e.into()),
} }
} }
fn upsert_comment(comment_form: &CommentForm, conn: &PgConnection) -> Result<Comment, Error> { fn upsert_comment(comment_form: &CommentForm, conn: &PgConnection) -> Result<Comment, LemmyError> {
let existing = Comment::read_from_apub_id(conn, &comment_form.ap_id); let existing = Comment::read_from_apub_id(conn, &comment_form.ap_id);
match existing { match existing {
Err(NotFound {}) => Ok(Comment::create(conn, &comment_form)?), Err(NotFound {}) => Ok(Comment::create(conn, &comment_form)?),
Ok(p) => Ok(Comment::update(conn, p.id, &comment_form)?), Ok(p) => Ok(Comment::update(conn, p.id, &comment_form)?),
Err(e) => Err(Error::from(e)), Err(e) => Err(e.into()),
} }
} }
pub fn get_or_fetch_and_insert_remote_comment( pub async fn get_or_fetch_and_insert_remote_comment(
comment_ap_id: &str, comment_ap_id: &str,
conn: &PgConnection, client: &Client,
) -> Result<Comment, Error> { pool: &DbPool,
match Comment::read_from_apub_id(conn, comment_ap_id) { ) -> Result<Comment, LemmyError> {
let comment_ap_id_owned = comment_ap_id.to_owned();
let comment = blocking(pool, move |conn| {
Comment::read_from_apub_id(conn, &comment_ap_id_owned)
})
.await?;
match comment {
Ok(p) => Ok(p), Ok(p) => Ok(p),
Err(NotFound {}) => { Err(NotFound {}) => {
debug!( debug!(
"Fetching and creating remote comment and its parents: {}", "Fetching and creating remote comment and its parents: {}",
comment_ap_id comment_ap_id
); );
let comment = fetch_remote_object::<Note>(&Url::parse(comment_ap_id)?)?; let comment = fetch_remote_object::<Note>(client, &Url::parse(comment_ap_id)?).await?;
let comment_form = CommentForm::from_apub(&comment, conn)?; let comment_form = CommentForm::from_apub(&comment, client, pool).await?;
Ok(Comment::create(conn, &comment_form)?)
let comment = blocking(pool, move |conn| Comment::create(conn, &comment_form)).await??;
Ok(comment)
} }
Err(e) => Err(Error::from(e)), Err(e) => Err(e.into()),
} }
} }
@ -309,7 +405,7 @@ pub fn get_or_fetch_and_insert_remote_comment(
// maybe), is community and user actors // maybe), is community and user actors
// and user actors // and user actors
// Fetch all posts in the outbox of the given user, and insert them into the database. // 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> { // fn fetch_community_outbox(community: &Community, conn: &PgConnection) -> Result<Vec<Post>, LemmyError> {
// let outbox_url = Url::parse(&community.get_outbox_url())?; // let outbox_url = Url::parse(&community.get_outbox_url())?;
// let outbox = fetch_remote_object::<OrderedCollection>(&outbox_url)?; // let outbox = fetch_remote_object::<OrderedCollection>(&outbox_url)?;
// let items = outbox.collection_props.get_many_items_base_boxes(); // let items = outbox.collection_props.get_many_items_base_boxes();
@ -317,11 +413,11 @@ pub fn get_or_fetch_and_insert_remote_comment(
// Ok( // Ok(
// items // items
// .unwrap() // .unwrap()
// .map(|obox: &BaseBox| -> Result<PostForm, Error> { // .map(|obox: &BaseBox| -> Result<PostForm, LemmyError> {
// let page = obox.clone().to_concrete::<Page>()?; // let page = obox.clone().to_concrete::<Page>()?;
// PostForm::from_page(&page, conn) // PostForm::from_page(&page, conn)
// }) // })
// .map(|pf| upsert_post(&pf?, conn)) // .map(|pf| upsert_post(&pf?, conn))
// .collect::<Result<Vec<Post>, Error>>()?, // .collect::<Result<Vec<Post>, LemmyError>>()?,
// ) // )
// } // }

View file

@ -18,7 +18,10 @@ use crate::{
}, },
convert_datetime, convert_datetime,
db::user::User_, db::user::User_,
request::{retry, RecvError},
routes::webfinger::WebFingerResponse, routes::webfinger::WebFingerResponse,
DbPool,
LemmyError,
MentionData, MentionData,
Settings, Settings,
}; };
@ -28,11 +31,8 @@ use activitystreams::{
}; };
use activitystreams_ext::{Ext1, Ext2, Ext3}; use activitystreams_ext::{Ext1, Ext2, Ext3};
use activitystreams_new::{activity::Follow, object::Tombstone, prelude::*}; use activitystreams_new::{activity::Follow, object::Tombstone, prelude::*};
use actix_web::{body::Body, HttpResponse, Result}; use actix_web::{body::Body, client::Client, HttpResponse};
use chrono::NaiveDateTime; use chrono::NaiveDateTime;
use diesel::PgConnection;
use failure::Error;
use isahc::prelude::*;
use log::debug; use log::debug;
use serde::Serialize; use serde::Serialize;
use url::Url; use url::Url;
@ -101,7 +101,9 @@ pub fn get_apub_protocol_string() -> &'static str {
// Checks if the ID has a valid format, correct scheme, and is in the allowed instance list. // Checks if the ID has a valid format, correct scheme, and is in the allowed instance list.
fn is_apub_id_valid(apub_id: &Url) -> bool { fn is_apub_id_valid(apub_id: &Url) -> bool {
debug!("Checking {}", apub_id);
if apub_id.scheme() != get_apub_protocol_string() { if apub_id.scheme() != get_apub_protocol_string() {
debug!("invalid scheme: {:?}", apub_id.scheme());
return false; return false;
} }
@ -112,15 +114,27 @@ fn is_apub_id_valid(apub_id: &Url) -> bool {
.map(|d| d.to_string()) .map(|d| d.to_string())
.collect(); .collect();
match apub_id.domain() { match apub_id.domain() {
Some(d) => allowed_instances.contains(&d.to_owned()), Some(d) => {
None => false, let contains = allowed_instances.contains(&d.to_owned());
if !contains {
debug!("{} not in {:?}", d, allowed_instances);
}
contains
}
None => {
debug!("missing domain");
false
}
} }
} }
#[async_trait::async_trait(?Send)]
pub trait ToApub { pub trait ToApub {
type Response; type Response;
fn to_apub(&self, conn: &PgConnection) -> Result<Self::Response, Error>; async fn to_apub(&self, pool: &DbPool) -> Result<Self::Response, LemmyError>;
fn to_tombstone(&self) -> Result<Tombstone, Error>; fn to_tombstone(&self) -> Result<Tombstone, LemmyError>;
} }
/// Updated is actually the deletion time /// Updated is actually the deletion time
@ -129,7 +143,7 @@ fn create_tombstone(
object_id: &str, object_id: &str,
updated: Option<NaiveDateTime>, updated: Option<NaiveDateTime>,
former_type: String, former_type: String,
) -> Result<Tombstone, Error> { ) -> Result<Tombstone, LemmyError> {
if deleted { if deleted {
if let Some(updated) = updated { if let Some(updated) = updated {
let mut tombstone = Tombstone::new(); let mut tombstone = Tombstone::new();
@ -138,37 +152,85 @@ fn create_tombstone(
tombstone.set_deleted(convert_datetime(updated).into()); tombstone.set_deleted(convert_datetime(updated).into());
Ok(tombstone) Ok(tombstone)
} else { } else {
Err(format_err!( Err(format_err!("Cant convert to tombstone because updated time was None.").into())
"Cant convert to tombstone because updated time was None."
))
} }
} else { } else {
Err(format_err!( Err(format_err!("Cant convert object to tombstone if it wasnt deleted").into())
"Cant convert object to tombstone if it wasnt deleted"
))
} }
} }
#[async_trait::async_trait(?Send)]
pub trait FromApub { pub trait FromApub {
type ApubType; type ApubType;
fn from_apub(apub: &Self::ApubType, conn: &PgConnection) -> Result<Self, Error> async fn from_apub(
apub: &Self::ApubType,
client: &Client,
pool: &DbPool,
) -> Result<Self, LemmyError>
where where
Self: Sized; Self: Sized;
} }
#[async_trait::async_trait(?Send)]
pub trait ApubObjectType { pub trait ApubObjectType {
fn send_create(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error>; async fn send_create(
fn send_update(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error>; &self,
fn send_delete(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error>; creator: &User_,
fn send_undo_delete(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error>; client: &Client,
fn send_remove(&self, mod_: &User_, conn: &PgConnection) -> Result<(), Error>; pool: &DbPool,
fn send_undo_remove(&self, mod_: &User_, conn: &PgConnection) -> Result<(), Error>; ) -> Result<(), LemmyError>;
async fn send_update(
&self,
creator: &User_,
client: &Client,
pool: &DbPool,
) -> Result<(), LemmyError>;
async fn send_delete(
&self,
creator: &User_,
client: &Client,
pool: &DbPool,
) -> Result<(), LemmyError>;
async fn send_undo_delete(
&self,
creator: &User_,
client: &Client,
pool: &DbPool,
) -> Result<(), LemmyError>;
async fn send_remove(
&self,
mod_: &User_,
client: &Client,
pool: &DbPool,
) -> Result<(), LemmyError>;
async fn send_undo_remove(
&self,
mod_: &User_,
client: &Client,
pool: &DbPool,
) -> Result<(), LemmyError>;
} }
#[async_trait::async_trait(?Send)]
pub trait ApubLikeableType { pub trait ApubLikeableType {
fn send_like(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error>; async fn send_like(
fn send_dislike(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error>; &self,
fn send_undo_like(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error>; creator: &User_,
client: &Client,
pool: &DbPool,
) -> Result<(), LemmyError>;
async fn send_dislike(
&self,
creator: &User_,
client: &Client,
pool: &DbPool,
) -> Result<(), LemmyError>;
async fn send_undo_like(
&self,
creator: &User_,
client: &Client,
pool: &DbPool,
) -> Result<(), LemmyError>;
} }
pub fn get_shared_inbox(actor_id: &str) -> String { pub fn get_shared_inbox(actor_id: &str) -> String {
@ -185,6 +247,7 @@ pub fn get_shared_inbox(actor_id: &str) -> String {
) )
} }
#[async_trait::async_trait(?Send)]
pub trait ActorType { pub trait ActorType {
fn actor_id(&self) -> String; fn actor_id(&self) -> String;
@ -194,20 +257,55 @@ pub trait ActorType {
// These two have default impls, since currently a community can't follow anything, // These two have default impls, since currently a community can't follow anything,
// and a user can't be followed (yet) // and a user can't be followed (yet)
#[allow(unused_variables)] #[allow(unused_variables)]
fn send_follow(&self, follow_actor_id: &str, conn: &PgConnection) -> Result<(), Error>; async fn send_follow(
fn send_unfollow(&self, follow_actor_id: &str, conn: &PgConnection) -> Result<(), Error>; &self,
follow_actor_id: &str,
client: &Client,
pool: &DbPool,
) -> Result<(), LemmyError>;
async fn send_unfollow(
&self,
follow_actor_id: &str,
client: &Client,
pool: &DbPool,
) -> Result<(), LemmyError>;
#[allow(unused_variables)] #[allow(unused_variables)]
fn send_accept_follow(&self, follow: &Follow, conn: &PgConnection) -> Result<(), Error>; async fn send_accept_follow(
&self,
follow: &Follow,
client: &Client,
pool: &DbPool,
) -> Result<(), LemmyError>;
fn send_delete(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error>; async fn send_delete(
fn send_undo_delete(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error>; &self,
creator: &User_,
client: &Client,
pool: &DbPool,
) -> Result<(), LemmyError>;
async fn send_undo_delete(
&self,
creator: &User_,
client: &Client,
pool: &DbPool,
) -> Result<(), LemmyError>;
fn send_remove(&self, mod_: &User_, conn: &PgConnection) -> Result<(), Error>; async fn send_remove(
fn send_undo_remove(&self, mod_: &User_, conn: &PgConnection) -> Result<(), Error>; &self,
mod_: &User_,
client: &Client,
pool: &DbPool,
) -> Result<(), LemmyError>;
async fn send_undo_remove(
&self,
mod_: &User_,
client: &Client,
pool: &DbPool,
) -> Result<(), LemmyError>;
/// For a given community, returns the inboxes of all followers. /// For a given community, returns the inboxes of all followers.
fn get_follower_inboxes(&self, conn: &PgConnection) -> Result<Vec<String>, Error>; async fn get_follower_inboxes(&self, pool: &DbPool) -> Result<Vec<String>, LemmyError>;
// TODO move these to the db rows // TODO move these to the db rows
fn get_inbox_url(&self) -> String { fn get_inbox_url(&self) -> String {
@ -244,7 +342,10 @@ pub trait ActorType {
} }
} }
pub fn fetch_webfinger_url(mention: &MentionData) -> Result<String, Error> { pub async fn fetch_webfinger_url(
mention: &MentionData,
client: &Client,
) -> Result<String, LemmyError> {
let fetch_url = format!( let fetch_url = format!(
"{}://{}/.well-known/webfinger?resource=acct:{}@{}", "{}://{}/.well-known/webfinger?resource=acct:{}@{}",
get_apub_protocol_string(), get_apub_protocol_string(),
@ -253,8 +354,14 @@ pub fn fetch_webfinger_url(mention: &MentionData) -> Result<String, Error> {
mention.domain mention.domain
); );
debug!("Fetching webfinger url: {}", &fetch_url); debug!("Fetching webfinger url: {}", &fetch_url);
let text = isahc::get(&fetch_url)?.text()?;
let res: WebFingerResponse = serde_json::from_str(&text)?; let mut response = retry(|| client.get(&fetch_url).send()).await?;
let res: WebFingerResponse = response
.json()
.await
.map_err(|e| RecvError(e.to_string()))?;
let link = res let link = res
.links .links
.iter() .iter()
@ -263,5 +370,5 @@ pub fn fetch_webfinger_url(mention: &MentionData) -> Result<String, Error> {
link link
.href .href
.to_owned() .to_owned()
.ok_or_else(|| format_err!("No href found.")) .ok_or_else(|| format_err!("No href found.").into())
} }

View file

@ -14,6 +14,7 @@ use crate::{
PageExt, PageExt,
ToApub, ToApub,
}, },
blocking,
convert_datetime, convert_datetime,
db::{ db::{
community::Community, community::Community,
@ -22,6 +23,8 @@ use crate::{
Crud, Crud,
}, },
routes::DbPoolParam, routes::DbPoolParam,
DbPool,
LemmyError,
Settings, Settings,
}; };
use activitystreams::{ use activitystreams::{
@ -32,9 +35,7 @@ use activitystreams::{
}; };
use activitystreams_ext::Ext1; use activitystreams_ext::Ext1;
use activitystreams_new::object::Tombstone; use activitystreams_new::object::Tombstone;
use actix_web::{body::Body, web::Path, HttpResponse, Result}; use actix_web::{body::Body, client::Client, web, HttpResponse};
use diesel::PgConnection;
use failure::Error;
use serde::Deserialize; use serde::Deserialize;
#[derive(Deserialize)] #[derive(Deserialize)]
@ -44,27 +45,33 @@ pub struct PostQuery {
/// Return the post json over HTTP. /// Return the post json over HTTP.
pub async fn get_apub_post( pub async fn get_apub_post(
info: Path<PostQuery>, info: web::Path<PostQuery>,
db: DbPoolParam, db: DbPoolParam,
) -> Result<HttpResponse<Body>, Error> { ) -> Result<HttpResponse<Body>, LemmyError> {
let id = info.post_id.parse::<i32>()?; let id = info.post_id.parse::<i32>()?;
let post = Post::read(&&db.get()?, id)?; let post = blocking(&db, move |conn| Post::read(conn, id)).await??;
if !post.deleted { if !post.deleted {
Ok(create_apub_response(&post.to_apub(&db.get().unwrap())?)) Ok(create_apub_response(&post.to_apub(&db).await?))
} else { } else {
Ok(create_apub_tombstone_response(&post.to_tombstone()?)) Ok(create_apub_tombstone_response(&post.to_tombstone()?))
} }
} }
#[async_trait::async_trait(?Send)]
impl ToApub for Post { impl ToApub for Post {
type Response = PageExt; type Response = PageExt;
// Turn a Lemmy post into an ActivityPub page that can be sent out over the network. // Turn a Lemmy post into an ActivityPub page that can be sent out over the network.
fn to_apub(&self, conn: &PgConnection) -> Result<PageExt, Error> { async fn to_apub(&self, pool: &DbPool) -> Result<PageExt, LemmyError> {
let mut page = Page::default(); let mut page = Page::default();
let oprops: &mut ObjectProperties = page.as_mut(); let oprops: &mut ObjectProperties = page.as_mut();
let creator = User_::read(conn, self.creator_id)?;
let community = Community::read(conn, self.community_id)?; let creator_id = self.creator_id;
let creator = blocking(pool, move |conn| User_::read(conn, creator_id)).await??;
let community_id = self.community_id;
let community = blocking(pool, move |conn| Community::read(conn, community_id)).await??;
oprops oprops
// Not needed when the Post is embedded in a collection (like for community outbox) // Not needed when the Post is embedded in a collection (like for community outbox)
@ -141,7 +148,7 @@ impl ToApub for Post {
Ok(Ext1::new(page, ext)) Ok(Ext1::new(page, ext))
} }
fn to_tombstone(&self) -> Result<Tombstone, Error> { fn to_tombstone(&self) -> Result<Tombstone, LemmyError> {
create_tombstone( create_tombstone(
self.deleted, self.deleted,
&self.ap_id, &self.ap_id,
@ -151,17 +158,26 @@ impl ToApub for Post {
} }
} }
#[async_trait::async_trait(?Send)]
impl FromApub for PostForm { impl FromApub for PostForm {
type ApubType = PageExt; type ApubType = PageExt;
/// Parse an ActivityPub page received from another instance into a Lemmy post. /// Parse an ActivityPub page received from another instance into a Lemmy post.
fn from_apub(page: &PageExt, conn: &PgConnection) -> Result<PostForm, Error> { async fn from_apub(
page: &PageExt,
client: &Client,
pool: &DbPool,
) -> Result<PostForm, LemmyError> {
let ext = &page.ext_one; let ext = &page.ext_one;
let oprops = &page.inner.object_props; let oprops = &page.inner.object_props;
let creator_actor_id = &oprops.get_attributed_to_xsd_any_uri().unwrap().to_string(); let creator_actor_id = &oprops.get_attributed_to_xsd_any_uri().unwrap().to_string();
let creator = get_or_fetch_and_upsert_remote_user(&creator_actor_id, &conn)?;
let creator = get_or_fetch_and_upsert_remote_user(&creator_actor_id, client, pool).await?;
let community_actor_id = &oprops.get_to_xsd_any_uri().unwrap().to_string(); let community_actor_id = &oprops.get_to_xsd_any_uri().unwrap().to_string();
let community = get_or_fetch_and_upsert_remote_community(&community_actor_id, &conn)?;
let community =
get_or_fetch_and_upsert_remote_community(&community_actor_id, client, pool).await?;
let thumbnail_url = match oprops.get_image_any_image() { let thumbnail_url = match oprops.get_image_any_image() {
Some(any_image) => any_image Some(any_image) => any_image
@ -221,11 +237,20 @@ impl FromApub for PostForm {
} }
} }
#[async_trait::async_trait(?Send)]
impl ApubObjectType for Post { impl ApubObjectType for Post {
/// Send out information about a newly created post, to the followers of the community. /// Send out information about a newly created post, to the followers of the community.
fn send_create(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error> { async fn send_create(
let page = self.to_apub(conn)?; &self,
let community = Community::read(conn, self.community_id)?; creator: &User_,
client: &Client,
pool: &DbPool,
) -> Result<(), LemmyError> {
let page = self.to_apub(pool).await?;
let community_id = self.community_id;
let community = blocking(pool, move |conn| Community::read(conn, community_id)).await??;
let id = format!("{}/create/{}", self.ap_id, uuid::Uuid::new_v4()); let id = format!("{}/create/{}", self.ap_id, uuid::Uuid::new_v4());
let mut create = Create::new(); let mut create = Create::new();
@ -241,18 +266,28 @@ impl ApubObjectType for Post {
send_activity_to_community( send_activity_to_community(
creator, creator,
conn,
&community, &community,
vec![community.get_shared_inbox_url()], vec![community.get_shared_inbox_url()],
create, create,
)?; client,
pool,
)
.await?;
Ok(()) Ok(())
} }
/// Send out information about an edited post, to the followers of the community. /// Send out information about an edited post, to the followers of the community.
fn send_update(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error> { async fn send_update(
let page = self.to_apub(conn)?; &self,
let community = Community::read(conn, self.community_id)?; creator: &User_,
client: &Client,
pool: &DbPool,
) -> Result<(), LemmyError> {
let page = self.to_apub(pool).await?;
let community_id = self.community_id;
let community = blocking(pool, move |conn| Community::read(conn, community_id)).await??;
let id = format!("{}/update/{}", self.ap_id, uuid::Uuid::new_v4()); let id = format!("{}/update/{}", self.ap_id, uuid::Uuid::new_v4());
let mut update = Update::new(); let mut update = Update::new();
@ -268,17 +303,27 @@ impl ApubObjectType for Post {
send_activity_to_community( send_activity_to_community(
creator, creator,
conn,
&community, &community,
vec![community.get_shared_inbox_url()], vec![community.get_shared_inbox_url()],
update, update,
)?; client,
pool,
)
.await?;
Ok(()) Ok(())
} }
fn send_delete(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error> { async fn send_delete(
let page = self.to_apub(conn)?; &self,
let community = Community::read(conn, self.community_id)?; creator: &User_,
client: &Client,
pool: &DbPool,
) -> Result<(), LemmyError> {
let page = self.to_apub(pool).await?;
let community_id = self.community_id;
let community = blocking(pool, move |conn| Community::read(conn, community_id)).await??;
let id = format!("{}/delete/{}", self.ap_id, uuid::Uuid::new_v4()); let id = format!("{}/delete/{}", self.ap_id, uuid::Uuid::new_v4());
let mut delete = Delete::default(); let mut delete = Delete::default();
@ -293,21 +338,29 @@ impl ApubObjectType for Post {
.set_actor_xsd_any_uri(creator.actor_id.to_owned())? .set_actor_xsd_any_uri(creator.actor_id.to_owned())?
.set_object_base_box(BaseBox::from_concrete(page)?)?; .set_object_base_box(BaseBox::from_concrete(page)?)?;
let community = Community::read(conn, self.community_id)?;
send_activity_to_community( send_activity_to_community(
creator, creator,
conn,
&community, &community,
vec![community.get_shared_inbox_url()], vec![community.get_shared_inbox_url()],
delete, delete,
)?; client,
pool,
)
.await?;
Ok(()) Ok(())
} }
fn send_undo_delete(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error> { async fn send_undo_delete(
let page = self.to_apub(conn)?; &self,
let community = Community::read(conn, self.community_id)?; creator: &User_,
client: &Client,
pool: &DbPool,
) -> Result<(), LemmyError> {
let page = self.to_apub(pool).await?;
let community_id = self.community_id;
let community = blocking(pool, move |conn| Community::read(conn, community_id)).await??;
let id = format!("{}/delete/{}", self.ap_id, uuid::Uuid::new_v4()); let id = format!("{}/delete/{}", self.ap_id, uuid::Uuid::new_v4());
let mut delete = Delete::default(); let mut delete = Delete::default();
@ -338,20 +391,29 @@ impl ApubObjectType for Post {
.set_actor_xsd_any_uri(creator.actor_id.to_owned())? .set_actor_xsd_any_uri(creator.actor_id.to_owned())?
.set_object_base_box(delete)?; .set_object_base_box(delete)?;
let community = Community::read(conn, self.community_id)?;
send_activity_to_community( send_activity_to_community(
creator, creator,
conn,
&community, &community,
vec![community.get_shared_inbox_url()], vec![community.get_shared_inbox_url()],
undo, undo,
)?; client,
pool,
)
.await?;
Ok(()) Ok(())
} }
fn send_remove(&self, mod_: &User_, conn: &PgConnection) -> Result<(), Error> { async fn send_remove(
let page = self.to_apub(conn)?; &self,
let community = Community::read(conn, self.community_id)?; mod_: &User_,
client: &Client,
pool: &DbPool,
) -> Result<(), LemmyError> {
let page = self.to_apub(pool).await?;
let community_id = self.community_id;
let community = blocking(pool, move |conn| Community::read(conn, community_id)).await??;
let id = format!("{}/remove/{}", self.ap_id, uuid::Uuid::new_v4()); let id = format!("{}/remove/{}", self.ap_id, uuid::Uuid::new_v4());
let mut remove = Remove::default(); let mut remove = Remove::default();
@ -366,20 +428,29 @@ impl ApubObjectType for Post {
.set_actor_xsd_any_uri(mod_.actor_id.to_owned())? .set_actor_xsd_any_uri(mod_.actor_id.to_owned())?
.set_object_base_box(BaseBox::from_concrete(page)?)?; .set_object_base_box(BaseBox::from_concrete(page)?)?;
let community = Community::read(conn, self.community_id)?;
send_activity_to_community( send_activity_to_community(
mod_, mod_,
conn,
&community, &community,
vec![community.get_shared_inbox_url()], vec![community.get_shared_inbox_url()],
remove, remove,
)?; client,
pool,
)
.await?;
Ok(()) Ok(())
} }
fn send_undo_remove(&self, mod_: &User_, conn: &PgConnection) -> Result<(), Error> {
let page = self.to_apub(conn)?; async fn send_undo_remove(
let community = Community::read(conn, self.community_id)?; &self,
mod_: &User_,
client: &Client,
pool: &DbPool,
) -> Result<(), LemmyError> {
let page = self.to_apub(pool).await?;
let community_id = self.community_id;
let community = blocking(pool, move |conn| Community::read(conn, community_id)).await??;
let id = format!("{}/remove/{}", self.ap_id, uuid::Uuid::new_v4()); let id = format!("{}/remove/{}", self.ap_id, uuid::Uuid::new_v4());
let mut remove = Remove::default(); let mut remove = Remove::default();
@ -409,22 +480,32 @@ impl ApubObjectType for Post {
.set_actor_xsd_any_uri(mod_.actor_id.to_owned())? .set_actor_xsd_any_uri(mod_.actor_id.to_owned())?
.set_object_base_box(remove)?; .set_object_base_box(remove)?;
let community = Community::read(conn, self.community_id)?;
send_activity_to_community( send_activity_to_community(
mod_, mod_,
conn,
&community, &community,
vec![community.get_shared_inbox_url()], vec![community.get_shared_inbox_url()],
undo, undo,
)?; client,
pool,
)
.await?;
Ok(()) Ok(())
} }
} }
#[async_trait::async_trait(?Send)]
impl ApubLikeableType for Post { impl ApubLikeableType for Post {
fn send_like(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error> { async fn send_like(
let page = self.to_apub(conn)?; &self,
let community = Community::read(conn, self.community_id)?; creator: &User_,
client: &Client,
pool: &DbPool,
) -> Result<(), LemmyError> {
let page = self.to_apub(pool).await?;
let community_id = self.community_id;
let community = blocking(pool, move |conn| Community::read(conn, community_id)).await??;
let id = format!("{}/like/{}", self.ap_id, uuid::Uuid::new_v4()); let id = format!("{}/like/{}", self.ap_id, uuid::Uuid::new_v4());
let mut like = Like::new(); let mut like = Like::new();
@ -440,17 +521,27 @@ impl ApubLikeableType for Post {
send_activity_to_community( send_activity_to_community(
&creator, &creator,
&conn,
&community, &community,
vec![community.get_shared_inbox_url()], vec![community.get_shared_inbox_url()],
like, like,
)?; client,
pool,
)
.await?;
Ok(()) Ok(())
} }
fn send_dislike(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error> { async fn send_dislike(
let page = self.to_apub(conn)?; &self,
let community = Community::read(conn, self.community_id)?; creator: &User_,
client: &Client,
pool: &DbPool,
) -> Result<(), LemmyError> {
let page = self.to_apub(pool).await?;
let community_id = self.community_id;
let community = blocking(pool, move |conn| Community::read(conn, community_id)).await??;
let id = format!("{}/dislike/{}", self.ap_id, uuid::Uuid::new_v4()); let id = format!("{}/dislike/{}", self.ap_id, uuid::Uuid::new_v4());
let mut dislike = Dislike::new(); let mut dislike = Dislike::new();
@ -466,17 +557,27 @@ impl ApubLikeableType for Post {
send_activity_to_community( send_activity_to_community(
&creator, &creator,
&conn,
&community, &community,
vec![community.get_shared_inbox_url()], vec![community.get_shared_inbox_url()],
dislike, dislike,
)?; client,
pool,
)
.await?;
Ok(()) Ok(())
} }
fn send_undo_like(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error> { async fn send_undo_like(
let page = self.to_apub(conn)?; &self,
let community = Community::read(conn, self.community_id)?; creator: &User_,
client: &Client,
pool: &DbPool,
) -> Result<(), LemmyError> {
let page = self.to_apub(pool).await?;
let community_id = self.community_id;
let community = blocking(pool, move |conn| Community::read(conn, community_id)).await??;
let id = format!("{}/like/{}", self.ap_id, uuid::Uuid::new_v4()); let id = format!("{}/like/{}", self.ap_id, uuid::Uuid::new_v4());
let mut like = Like::new(); let mut like = Like::new();
@ -508,11 +609,13 @@ impl ApubLikeableType for Post {
send_activity_to_community( send_activity_to_community(
&creator, &creator,
&conn,
&community, &community,
vec![community.get_shared_inbox_url()], vec![community.get_shared_inbox_url()],
undo, undo,
)?; client,
pool,
)
.await?;
Ok(()) Ok(())
} }
} }

View file

@ -7,6 +7,7 @@ use crate::{
FromApub, FromApub,
ToApub, ToApub,
}, },
blocking,
convert_datetime, convert_datetime,
db::{ db::{
activity::insert_activity, activity::insert_activity,
@ -14,6 +15,8 @@ use crate::{
user::User_, user::User_,
Crud, Crud,
}, },
DbPool,
LemmyError,
}; };
use activitystreams::{ use activitystreams::{
activity::{Create, Delete, Undo, Update}, activity::{Create, Delete, Undo, Update},
@ -21,18 +24,21 @@ use activitystreams::{
object::{kind::NoteType, properties::ObjectProperties, Note}, object::{kind::NoteType, properties::ObjectProperties, Note},
}; };
use activitystreams_new::object::Tombstone; use activitystreams_new::object::Tombstone;
use actix_web::Result; use actix_web::client::Client;
use diesel::PgConnection;
use failure::Error;
#[async_trait::async_trait(?Send)]
impl ToApub for PrivateMessage { impl ToApub for PrivateMessage {
type Response = Note; type Response = Note;
fn to_apub(&self, conn: &PgConnection) -> Result<Note, Error> { async fn to_apub(&self, pool: &DbPool) -> Result<Note, LemmyError> {
let mut private_message = Note::default(); let mut private_message = Note::default();
let oprops: &mut ObjectProperties = private_message.as_mut(); let oprops: &mut ObjectProperties = private_message.as_mut();
let creator = User_::read(&conn, self.creator_id)?;
let recipient = User_::read(&conn, self.recipient_id)?; let creator_id = self.creator_id;
let creator = blocking(pool, move |conn| User_::read(conn, creator_id)).await??;
let recipient_id = self.recipient_id;
let recipient = blocking(pool, move |conn| User_::read(conn, recipient_id)).await??;
oprops oprops
.set_context_xsd_any_uri(context())? .set_context_xsd_any_uri(context())?
@ -49,7 +55,7 @@ impl ToApub for PrivateMessage {
Ok(private_message) Ok(private_message)
} }
fn to_tombstone(&self) -> Result<Tombstone, Error> { fn to_tombstone(&self) -> Result<Tombstone, LemmyError> {
create_tombstone( create_tombstone(
self.deleted, self.deleted,
&self.ap_id, &self.ap_id,
@ -59,16 +65,24 @@ impl ToApub for PrivateMessage {
} }
} }
#[async_trait::async_trait(?Send)]
impl FromApub for PrivateMessageForm { impl FromApub for PrivateMessageForm {
type ApubType = Note; type ApubType = Note;
/// Parse an ActivityPub note received from another instance into a Lemmy Private message /// Parse an ActivityPub note received from another instance into a Lemmy Private message
fn from_apub(note: &Note, conn: &PgConnection) -> Result<PrivateMessageForm, Error> { async fn from_apub(
note: &Note,
client: &Client,
pool: &DbPool,
) -> Result<PrivateMessageForm, LemmyError> {
let oprops = &note.object_props; let oprops = &note.object_props;
let creator_actor_id = &oprops.get_attributed_to_xsd_any_uri().unwrap().to_string(); let creator_actor_id = &oprops.get_attributed_to_xsd_any_uri().unwrap().to_string();
let creator = get_or_fetch_and_upsert_remote_user(&creator_actor_id, &conn)?;
let creator = get_or_fetch_and_upsert_remote_user(&creator_actor_id, client, pool).await?;
let recipient_actor_id = &oprops.get_to_xsd_any_uri().unwrap().to_string(); let recipient_actor_id = &oprops.get_to_xsd_any_uri().unwrap().to_string();
let recipient = get_or_fetch_and_upsert_remote_user(&recipient_actor_id, &conn)?;
let recipient = get_or_fetch_and_upsert_remote_user(&recipient_actor_id, client, pool).await?;
Ok(PrivateMessageForm { Ok(PrivateMessageForm {
creator_id: creator.id, creator_id: creator.id,
@ -91,12 +105,20 @@ impl FromApub for PrivateMessageForm {
} }
} }
#[async_trait::async_trait(?Send)]
impl ApubObjectType for PrivateMessage { impl ApubObjectType for PrivateMessage {
/// Send out information about a newly created private message /// Send out information about a newly created private message
fn send_create(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error> { async fn send_create(
let note = self.to_apub(conn)?; &self,
creator: &User_,
client: &Client,
pool: &DbPool,
) -> Result<(), LemmyError> {
let note = self.to_apub(pool).await?;
let id = format!("{}/create/{}", self.ap_id, uuid::Uuid::new_v4()); let id = format!("{}/create/{}", self.ap_id, uuid::Uuid::new_v4());
let recipient = User_::read(&conn, self.recipient_id)?;
let recipient_id = self.recipient_id;
let recipient = blocking(pool, move |conn| User_::read(conn, recipient_id)).await??;
let mut create = Create::new(); let mut create = Create::new();
create create
@ -110,17 +132,24 @@ impl ApubObjectType for PrivateMessage {
.set_actor_xsd_any_uri(creator.actor_id.to_owned())? .set_actor_xsd_any_uri(creator.actor_id.to_owned())?
.set_object_base_box(note)?; .set_object_base_box(note)?;
insert_activity(&conn, creator.id, &create, true)?; insert_activity(creator.id, create.clone(), true, pool).await?;
send_activity(&create, creator, vec![to])?; send_activity(client, &create, creator, vec![to]).await?;
Ok(()) Ok(())
} }
/// Send out information about an edited post, to the followers of the community. /// Send out information about an edited post, to the followers of the community.
fn send_update(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error> { async fn send_update(
let note = self.to_apub(conn)?; &self,
creator: &User_,
client: &Client,
pool: &DbPool,
) -> Result<(), LemmyError> {
let note = self.to_apub(pool).await?;
let id = format!("{}/update/{}", self.ap_id, uuid::Uuid::new_v4()); let id = format!("{}/update/{}", self.ap_id, uuid::Uuid::new_v4());
let recipient = User_::read(&conn, self.recipient_id)?;
let recipient_id = self.recipient_id;
let recipient = blocking(pool, move |conn| User_::read(conn, recipient_id)).await??;
let mut update = Update::new(); let mut update = Update::new();
update update
@ -134,16 +163,23 @@ impl ApubObjectType for PrivateMessage {
.set_actor_xsd_any_uri(creator.actor_id.to_owned())? .set_actor_xsd_any_uri(creator.actor_id.to_owned())?
.set_object_base_box(note)?; .set_object_base_box(note)?;
insert_activity(&conn, creator.id, &update, true)?; insert_activity(creator.id, update.clone(), true, pool).await?;
send_activity(&update, creator, vec![to])?; send_activity(client, &update, creator, vec![to]).await?;
Ok(()) Ok(())
} }
fn send_delete(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error> { async fn send_delete(
let note = self.to_apub(conn)?; &self,
creator: &User_,
client: &Client,
pool: &DbPool,
) -> Result<(), LemmyError> {
let note = self.to_apub(pool).await?;
let id = format!("{}/delete/{}", self.ap_id, uuid::Uuid::new_v4()); let id = format!("{}/delete/{}", self.ap_id, uuid::Uuid::new_v4());
let recipient = User_::read(&conn, self.recipient_id)?;
let recipient_id = self.recipient_id;
let recipient = blocking(pool, move |conn| User_::read(conn, recipient_id)).await??;
let mut delete = Delete::new(); let mut delete = Delete::new();
delete delete
@ -157,16 +193,23 @@ impl ApubObjectType for PrivateMessage {
.set_actor_xsd_any_uri(creator.actor_id.to_owned())? .set_actor_xsd_any_uri(creator.actor_id.to_owned())?
.set_object_base_box(note)?; .set_object_base_box(note)?;
insert_activity(&conn, creator.id, &delete, true)?; insert_activity(creator.id, delete.clone(), true, pool).await?;
send_activity(&delete, creator, vec![to])?; send_activity(client, &delete, creator, vec![to]).await?;
Ok(()) Ok(())
} }
fn send_undo_delete(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error> { async fn send_undo_delete(
let note = self.to_apub(conn)?; &self,
creator: &User_,
client: &Client,
pool: &DbPool,
) -> Result<(), LemmyError> {
let note = self.to_apub(pool).await?;
let id = format!("{}/delete/{}", self.ap_id, uuid::Uuid::new_v4()); let id = format!("{}/delete/{}", self.ap_id, uuid::Uuid::new_v4());
let recipient = User_::read(&conn, self.recipient_id)?;
let recipient_id = self.recipient_id;
let recipient = blocking(pool, move |conn| User_::read(conn, recipient_id)).await??;
let mut delete = Delete::new(); let mut delete = Delete::new();
delete delete
@ -195,17 +238,27 @@ impl ApubObjectType for PrivateMessage {
.set_actor_xsd_any_uri(creator.actor_id.to_owned())? .set_actor_xsd_any_uri(creator.actor_id.to_owned())?
.set_object_base_box(delete)?; .set_object_base_box(delete)?;
insert_activity(&conn, creator.id, &undo, true)?; insert_activity(creator.id, undo.clone(), true, pool).await?;
send_activity(&undo, creator, vec![to])?; send_activity(client, &undo, creator, vec![to]).await?;
Ok(()) Ok(())
} }
fn send_remove(&self, _mod_: &User_, _conn: &PgConnection) -> Result<(), Error> { async fn send_remove(
&self,
_mod_: &User_,
_client: &Client,
_pool: &DbPool,
) -> Result<(), LemmyError> {
unimplemented!() unimplemented!()
} }
fn send_undo_remove(&self, _mod_: &User_, _conn: &PgConnection) -> Result<(), Error> { async fn send_undo_remove(
&self,
_mod_: &User_,
_client: &Client,
_pool: &DbPool,
) -> Result<(), LemmyError> {
unimplemented!() unimplemented!()
} }
} }

File diff suppressed because it is too large Load diff

View file

@ -8,6 +8,7 @@ use crate::{
PersonExt, PersonExt,
ToApub, ToApub,
}, },
blocking,
convert_datetime, convert_datetime,
db::{ db::{
activity::insert_activity, activity::insert_activity,
@ -15,6 +16,8 @@ use crate::{
}, },
naive_now, naive_now,
routes::DbPoolParam, routes::DbPoolParam,
DbPool,
LemmyError,
}; };
use activitystreams::{ use activitystreams::{
actor::{properties::ApActorProperties, Person}, actor::{properties::ApActorProperties, Person},
@ -29,9 +32,7 @@ use activitystreams_new::{
object::Tombstone, object::Tombstone,
prelude::*, prelude::*,
}; };
use actix_web::{body::Body, web::Path, HttpResponse, Result}; use actix_web::{body::Body, client::Client, web, HttpResponse};
use diesel::PgConnection;
use failure::Error;
use serde::Deserialize; use serde::Deserialize;
#[derive(Deserialize)] #[derive(Deserialize)]
@ -39,11 +40,12 @@ pub struct UserQuery {
user_name: String, user_name: String,
} }
#[async_trait::async_trait(?Send)]
impl ToApub for User_ { impl ToApub for User_ {
type Response = PersonExt; type Response = PersonExt;
// Turn a Lemmy Community into an ActivityPub group that can be sent out over the network. // Turn a Lemmy Community into an ActivityPub group that can be sent out over the network.
fn to_apub(&self, _conn: &PgConnection) -> Result<PersonExt, Error> { async fn to_apub(&self, _pool: &DbPool) -> Result<PersonExt, LemmyError> {
// TODO go through all these to_string and to_owned() // TODO go through all these to_string and to_owned()
let mut person = Person::default(); let mut person = Person::default();
let oprops: &mut ObjectProperties = person.as_mut(); let oprops: &mut ObjectProperties = person.as_mut();
@ -86,11 +88,12 @@ impl ToApub for User_ {
Ok(Ext2::new(person, actor_props, self.get_public_key_ext())) Ok(Ext2::new(person, actor_props, self.get_public_key_ext()))
} }
fn to_tombstone(&self) -> Result<Tombstone, Error> { fn to_tombstone(&self) -> Result<Tombstone, LemmyError> {
unimplemented!() unimplemented!()
} }
} }
#[async_trait::async_trait(?Send)]
impl ActorType for User_ { impl ActorType for User_ {
fn actor_id(&self) -> String { fn actor_id(&self) -> String {
self.actor_id.to_owned() self.actor_id.to_owned()
@ -105,19 +108,29 @@ impl ActorType for User_ {
} }
/// As a given local user, send out a follow request to a remote community. /// As a given local user, send out a follow request to a remote community.
fn send_follow(&self, follow_actor_id: &str, conn: &PgConnection) -> Result<(), Error> { async fn send_follow(
&self,
follow_actor_id: &str,
client: &Client,
pool: &DbPool,
) -> Result<(), LemmyError> {
let id = format!("{}/follow/{}", self.actor_id, uuid::Uuid::new_v4()); let id = format!("{}/follow/{}", self.actor_id, uuid::Uuid::new_v4());
let mut follow = Follow::new(self.actor_id.to_owned(), follow_actor_id); let mut follow = Follow::new(self.actor_id.to_owned(), follow_actor_id);
follow.set_context(context()).set_id(id.parse()?); follow.set_context(context()).set_id(id.parse()?);
let to = format!("{}/inbox", follow_actor_id); let to = format!("{}/inbox", follow_actor_id);
insert_activity(&conn, self.id, &follow, true)?; insert_activity(self.id, follow.clone(), true, pool).await?;
send_activity(&follow, self, vec![to])?; send_activity(client, &follow, self, vec![to]).await?;
Ok(()) Ok(())
} }
fn send_unfollow(&self, follow_actor_id: &str, conn: &PgConnection) -> Result<(), Error> { async fn send_unfollow(
&self,
follow_actor_id: &str,
client: &Client,
pool: &DbPool,
) -> Result<(), LemmyError> {
let id = format!("{}/follow/{}", self.actor_id, uuid::Uuid::new_v4()); let id = format!("{}/follow/{}", self.actor_id, uuid::Uuid::new_v4());
let mut follow = Follow::new(self.actor_id.to_owned(), follow_actor_id); let mut follow = Follow::new(self.actor_id.to_owned(), follow_actor_id);
follow.set_context(context()).set_id(id.parse()?); follow.set_context(context()).set_id(id.parse()?);
@ -130,41 +143,67 @@ impl ActorType for User_ {
let mut undo = Undo::new(self.actor_id.parse::<XsdAnyUri>()?, follow.into_any_base()?); let mut undo = Undo::new(self.actor_id.parse::<XsdAnyUri>()?, follow.into_any_base()?);
undo.set_context(context()).set_id(undo_id.parse()?); undo.set_context(context()).set_id(undo_id.parse()?);
insert_activity(&conn, self.id, &undo, true)?; insert_activity(self.id, undo.clone(), true, pool).await?;
send_activity(&undo, self, vec![to])?; send_activity(client, &undo, self, vec![to]).await?;
Ok(()) Ok(())
} }
fn send_delete(&self, _creator: &User_, _conn: &PgConnection) -> Result<(), Error> { async fn send_delete(
&self,
_creator: &User_,
_client: &Client,
_pool: &DbPool,
) -> Result<(), LemmyError> {
unimplemented!() unimplemented!()
} }
fn send_undo_delete(&self, _creator: &User_, _conn: &PgConnection) -> Result<(), Error> { async fn send_undo_delete(
&self,
_creator: &User_,
_client: &Client,
_pool: &DbPool,
) -> Result<(), LemmyError> {
unimplemented!() unimplemented!()
} }
fn send_remove(&self, _creator: &User_, _conn: &PgConnection) -> Result<(), Error> { async fn send_remove(
&self,
_creator: &User_,
_client: &Client,
_pool: &DbPool,
) -> Result<(), LemmyError> {
unimplemented!() unimplemented!()
} }
fn send_undo_remove(&self, _creator: &User_, _conn: &PgConnection) -> Result<(), Error> { async fn send_undo_remove(
&self,
_creator: &User_,
_client: &Client,
_pool: &DbPool,
) -> Result<(), LemmyError> {
unimplemented!() unimplemented!()
} }
fn send_accept_follow(&self, _follow: &Follow, _conn: &PgConnection) -> Result<(), Error> { async fn send_accept_follow(
&self,
_follow: &Follow,
_client: &Client,
_pool: &DbPool,
) -> Result<(), LemmyError> {
unimplemented!() unimplemented!()
} }
fn get_follower_inboxes(&self, _conn: &PgConnection) -> Result<Vec<String>, Error> { async fn get_follower_inboxes(&self, _pool: &DbPool) -> Result<Vec<String>, LemmyError> {
unimplemented!() unimplemented!()
} }
} }
#[async_trait::async_trait(?Send)]
impl FromApub for UserForm { impl FromApub for UserForm {
type ApubType = PersonExt; type ApubType = PersonExt;
/// Parse an ActivityPub person received from another instance into a Lemmy user. /// Parse an ActivityPub person received from another instance into a Lemmy user.
fn from_apub(person: &PersonExt, _conn: &PgConnection) -> Result<Self, Error> { async fn from_apub(person: &PersonExt, _: &Client, _: &DbPool) -> Result<Self, LemmyError> {
let oprops = &person.inner.object_props; let oprops = &person.inner.object_props;
let aprops = &person.ext_one; let aprops = &person.ext_one;
let public_key: &PublicKey = &person.ext_two.public_key; let public_key: &PublicKey = &person.ext_two.public_key;
@ -210,10 +249,14 @@ impl FromApub for UserForm {
/// Return the user json over HTTP. /// Return the user json over HTTP.
pub async fn get_apub_user_http( pub async fn get_apub_user_http(
info: Path<UserQuery>, info: web::Path<UserQuery>,
db: DbPoolParam, db: DbPoolParam,
) -> Result<HttpResponse<Body>, Error> { ) -> Result<HttpResponse<Body>, LemmyError> {
let user = User_::find_by_email_or_username(&&db.get()?, &info.user_name)?; let user_name = info.into_inner().user_name;
let u = user.to_apub(&db.get().unwrap())?; let user = blocking(&db, move |conn| {
User_::find_by_email_or_username(conn, &user_name)
})
.await??;
let u = user.to_apub(&db).await?;
Ok(create_apub_response(&u)) Ok(create_apub_response(&u))
} }

View file

@ -5,6 +5,7 @@ use crate::{
fetcher::{get_or_fetch_and_upsert_remote_community, get_or_fetch_and_upsert_remote_user}, fetcher::{get_or_fetch_and_upsert_remote_community, get_or_fetch_and_upsert_remote_user},
FromApub, FromApub,
}, },
blocking,
db::{ db::{
activity::insert_activity, activity::insert_activity,
community::{CommunityFollower, CommunityFollowerForm}, community::{CommunityFollower, CommunityFollowerForm},
@ -17,16 +18,17 @@ use crate::{
naive_now, naive_now,
routes::{ChatServerParam, DbPoolParam}, routes::{ChatServerParam, DbPoolParam},
websocket::{server::SendUserRoomMessage, UserOperation}, websocket::{server::SendUserRoomMessage, UserOperation},
DbPool,
LemmyError,
}; };
use activitystreams::{ use activitystreams::{
activity::{Accept, Create, Delete, Undo, Update}, activity::{Accept, Create, Delete, Undo, Update},
object::Note, object::Note,
}; };
use actix_web::{web, HttpRequest, HttpResponse, Result}; use actix_web::{client::Client, web, HttpRequest, HttpResponse};
use diesel::PgConnection;
use failure::{Error, _core::fmt::Debug};
use log::debug; use log::debug;
use serde::Deserialize; use serde::Deserialize;
use std::fmt::Debug;
#[serde(untagged)] #[serde(untagged)]
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug)]
@ -43,51 +45,53 @@ pub async fn user_inbox(
request: HttpRequest, request: HttpRequest,
input: web::Json<UserAcceptedObjects>, input: web::Json<UserAcceptedObjects>,
path: web::Path<String>, path: web::Path<String>,
client: web::Data<Client>,
db: DbPoolParam, db: DbPoolParam,
chat_server: ChatServerParam, chat_server: ChatServerParam,
) -> Result<HttpResponse, Error> { ) -> Result<HttpResponse, LemmyError> {
// TODO: would be nice if we could do the signature check here, but we cant access the actor property // 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 input = input.into_inner();
let conn = &db.get().unwrap();
let username = path.into_inner(); let username = path.into_inner();
debug!("User {} received activity: {:?}", &username, &input); debug!("User {} received activity: {:?}", &username, &input);
match input { match input {
UserAcceptedObjects::Accept(a) => receive_accept(&a, &request, &username, &conn), UserAcceptedObjects::Accept(a) => receive_accept(*a, &request, &username, &client, &db).await,
UserAcceptedObjects::Create(c) => { UserAcceptedObjects::Create(c) => {
receive_create_private_message(&c, &request, &conn, chat_server) receive_create_private_message(*c, &request, &client, &db, chat_server).await
} }
UserAcceptedObjects::Update(u) => { UserAcceptedObjects::Update(u) => {
receive_update_private_message(&u, &request, &conn, chat_server) receive_update_private_message(*u, &request, &client, &db, chat_server).await
} }
UserAcceptedObjects::Delete(d) => { UserAcceptedObjects::Delete(d) => {
receive_delete_private_message(&d, &request, &conn, chat_server) receive_delete_private_message(*d, &request, &client, &db, chat_server).await
} }
UserAcceptedObjects::Undo(u) => { UserAcceptedObjects::Undo(u) => {
receive_undo_delete_private_message(&u, &request, &conn, chat_server) receive_undo_delete_private_message(*u, &request, &client, &db, chat_server).await
} }
} }
} }
/// Handle accepted follows. /// Handle accepted follows.
fn receive_accept( async fn receive_accept(
accept: &Accept, accept: Accept,
request: &HttpRequest, request: &HttpRequest,
username: &str, username: &str,
conn: &PgConnection, client: &Client,
) -> Result<HttpResponse, Error> { pool: &DbPool,
) -> Result<HttpResponse, LemmyError> {
let community_uri = accept let community_uri = accept
.accept_props .accept_props
.get_actor_xsd_any_uri() .get_actor_xsd_any_uri()
.unwrap() .unwrap()
.to_string(); .to_string();
let community = get_or_fetch_and_upsert_remote_community(&community_uri, conn)?; let community = get_or_fetch_and_upsert_remote_community(&community_uri, client, pool).await?;
verify(request, &community)?; verify(request, &community)?;
let user = User_::read_from_name(&conn, username)?; let username = username.to_owned();
let user = blocking(pool, move |conn| User_::read_from_name(conn, &username)).await??;
insert_activity(&conn, community.creator_id, &accept, false)?; insert_activity(community.creator_id, accept, false, pool).await?;
// Now you need to add this to the community follower // Now you need to add this to the community follower
let community_follower_form = CommunityFollowerForm { let community_follower_form = CommunityFollowerForm {
@ -96,18 +100,22 @@ fn receive_accept(
}; };
// This will fail if they're already a follower // This will fail if they're already a follower
CommunityFollower::follow(&conn, &community_follower_form)?; blocking(pool, move |conn| {
CommunityFollower::follow(conn, &community_follower_form)
})
.await??;
// TODO: make sure that we actually requested a follow // TODO: make sure that we actually requested a follow
Ok(HttpResponse::Ok().finish()) Ok(HttpResponse::Ok().finish())
} }
fn receive_create_private_message( async fn receive_create_private_message(
create: &Create, create: Create,
request: &HttpRequest, request: &HttpRequest,
conn: &PgConnection, client: &Client,
pool: &DbPool,
chat_server: ChatServerParam, chat_server: ChatServerParam,
) -> Result<HttpResponse, Error> { ) -> Result<HttpResponse, LemmyError> {
let note = create let note = create
.create_props .create_props
.get_object_base_box() .get_object_base_box()
@ -122,36 +130,44 @@ fn receive_create_private_message(
.unwrap() .unwrap()
.to_string(); .to_string();
let user = get_or_fetch_and_upsert_remote_user(&user_uri, &conn)?; let user = get_or_fetch_and_upsert_remote_user(&user_uri, client, pool).await?;
verify(request, &user)?; verify(request, &user)?;
insert_activity(&conn, user.id, &create, false)?; insert_activity(user.id, create, false, pool).await?;
let private_message = PrivateMessageForm::from_apub(&note, &conn)?; let private_message = PrivateMessageForm::from_apub(&note, client, pool).await?;
let inserted_private_message = PrivateMessage::create(&conn, &private_message)?;
let message = PrivateMessageView::read(&conn, inserted_private_message.id)?; let inserted_private_message = blocking(pool, move |conn| {
PrivateMessage::create(conn, &private_message)
})
.await??;
let res = PrivateMessageResponse { let message = blocking(pool, move |conn| {
message: message.to_owned(), PrivateMessageView::read(conn, inserted_private_message.id)
}; })
.await??;
let res = PrivateMessageResponse { message };
let recipient_id = res.message.recipient_id;
chat_server.do_send(SendUserRoomMessage { chat_server.do_send(SendUserRoomMessage {
op: UserOperation::CreatePrivateMessage, op: UserOperation::CreatePrivateMessage,
response: res, response: res,
recipient_id: message.recipient_id, recipient_id,
my_id: None, my_id: None,
}); });
Ok(HttpResponse::Ok().finish()) Ok(HttpResponse::Ok().finish())
} }
fn receive_update_private_message( async fn receive_update_private_message(
update: &Update, update: Update,
request: &HttpRequest, request: &HttpRequest,
conn: &PgConnection, client: &Client,
pool: &DbPool,
chat_server: ChatServerParam, chat_server: ChatServerParam,
) -> Result<HttpResponse, Error> { ) -> Result<HttpResponse, LemmyError> {
let note = update let note = update
.update_props .update_props
.get_object_base_box() .get_object_base_box()
@ -166,37 +182,52 @@ fn receive_update_private_message(
.unwrap() .unwrap()
.to_string(); .to_string();
let user = get_or_fetch_and_upsert_remote_user(&user_uri, &conn)?; let user = get_or_fetch_and_upsert_remote_user(&user_uri, client, pool).await?;
verify(request, &user)?; verify(request, &user)?;
insert_activity(&conn, user.id, &update, false)?; insert_activity(user.id, update, false, pool).await?;
let private_message = PrivateMessageForm::from_apub(&note, &conn)?; let private_message_form = PrivateMessageForm::from_apub(&note, client, pool).await?;
let private_message_id = PrivateMessage::read_from_apub_id(&conn, &private_message.ap_id)?.id;
PrivateMessage::update(conn, private_message_id, &private_message)?;
let message = PrivateMessageView::read(&conn, private_message_id)?; let private_message_ap_id = private_message_form.ap_id.clone();
let private_message = blocking(pool, move |conn| {
PrivateMessage::read_from_apub_id(conn, &private_message_ap_id)
})
.await??;
let res = PrivateMessageResponse { let private_message_id = private_message.id;
message: message.to_owned(), blocking(pool, move |conn| {
}; PrivateMessage::update(conn, private_message_id, &private_message_form)
})
.await??;
let private_message_id = private_message.id;
let message = blocking(pool, move |conn| {
PrivateMessageView::read(conn, private_message_id)
})
.await??;
let res = PrivateMessageResponse { message };
let recipient_id = res.message.recipient_id;
chat_server.do_send(SendUserRoomMessage { chat_server.do_send(SendUserRoomMessage {
op: UserOperation::EditPrivateMessage, op: UserOperation::EditPrivateMessage,
response: res, response: res,
recipient_id: message.recipient_id, recipient_id,
my_id: None, my_id: None,
}); });
Ok(HttpResponse::Ok().finish()) Ok(HttpResponse::Ok().finish())
} }
fn receive_delete_private_message( async fn receive_delete_private_message(
delete: &Delete, delete: Delete,
request: &HttpRequest, request: &HttpRequest,
conn: &PgConnection, client: &Client,
pool: &DbPool,
chat_server: ChatServerParam, chat_server: ChatServerParam,
) -> Result<HttpResponse, Error> { ) -> Result<HttpResponse, LemmyError> {
let note = delete let note = delete
.delete_props .delete_props
.get_object_base_box() .get_object_base_box()
@ -211,15 +242,21 @@ fn receive_delete_private_message(
.unwrap() .unwrap()
.to_string(); .to_string();
let user = get_or_fetch_and_upsert_remote_user(&user_uri, &conn)?; let user = get_or_fetch_and_upsert_remote_user(&user_uri, client, pool).await?;
verify(request, &user)?; verify(request, &user)?;
insert_activity(&conn, user.id, &delete, false)?; insert_activity(user.id, delete, false, pool).await?;
let private_message_form = PrivateMessageForm::from_apub(&note, client, pool).await?;
let private_message_ap_id = private_message_form.ap_id;
let private_message = blocking(pool, move |conn| {
PrivateMessage::read_from_apub_id(conn, &private_message_ap_id)
})
.await??;
let private_message = PrivateMessageForm::from_apub(&note, &conn)?;
let private_message_id = PrivateMessage::read_from_apub_id(&conn, &private_message.ap_id)?.id;
let private_message_form = PrivateMessageForm { let private_message_form = PrivateMessageForm {
content: private_message.content, content: private_message_form.content,
recipient_id: private_message.recipient_id, recipient_id: private_message.recipient_id,
creator_id: private_message.creator_id, creator_id: private_message.creator_id,
deleted: Some(true), deleted: Some(true),
@ -229,30 +266,40 @@ fn receive_delete_private_message(
published: None, published: None,
updated: Some(naive_now()), updated: Some(naive_now()),
}; };
PrivateMessage::update(conn, private_message_id, &private_message_form)?;
let message = PrivateMessageView::read(&conn, private_message_id)?; let private_message_id = private_message.id;
blocking(pool, move |conn| {
PrivateMessage::update(conn, private_message_id, &private_message_form)
})
.await??;
let res = PrivateMessageResponse { let private_message_id = private_message.id;
message: message.to_owned(), let message = blocking(pool, move |conn| {
}; PrivateMessageView::read(&conn, private_message_id)
})
.await??;
let res = PrivateMessageResponse { message };
let recipient_id = res.message.recipient_id;
chat_server.do_send(SendUserRoomMessage { chat_server.do_send(SendUserRoomMessage {
op: UserOperation::EditPrivateMessage, op: UserOperation::EditPrivateMessage,
response: res, response: res,
recipient_id: message.recipient_id, recipient_id,
my_id: None, my_id: None,
}); });
Ok(HttpResponse::Ok().finish()) Ok(HttpResponse::Ok().finish())
} }
fn receive_undo_delete_private_message( async fn receive_undo_delete_private_message(
undo: &Undo, undo: Undo,
request: &HttpRequest, request: &HttpRequest,
conn: &PgConnection, client: &Client,
pool: &DbPool,
chat_server: ChatServerParam, chat_server: ChatServerParam,
) -> Result<HttpResponse, Error> { ) -> Result<HttpResponse, LemmyError> {
let delete = undo let delete = undo
.undo_props .undo_props
.get_object_base_box() .get_object_base_box()
@ -275,13 +322,19 @@ fn receive_undo_delete_private_message(
.unwrap() .unwrap()
.to_string(); .to_string();
let user = get_or_fetch_and_upsert_remote_user(&user_uri, &conn)?; let user = get_or_fetch_and_upsert_remote_user(&user_uri, client, pool).await?;
verify(request, &user)?; verify(request, &user)?;
insert_activity(&conn, user.id, &delete, false)?; insert_activity(user.id, delete, false, pool).await?;
let private_message = PrivateMessageForm::from_apub(&note, client, pool).await?;
let private_message_ap_id = private_message.ap_id.clone();
let private_message_id = blocking(pool, move |conn| {
PrivateMessage::read_from_apub_id(conn, &private_message_ap_id).map(|pm| pm.id)
})
.await??;
let private_message = PrivateMessageForm::from_apub(&note, &conn)?;
let private_message_id = PrivateMessage::read_from_apub_id(&conn, &private_message.ap_id)?.id;
let private_message_form = PrivateMessageForm { let private_message_form = PrivateMessageForm {
content: private_message.content, content: private_message.content,
recipient_id: private_message.recipient_id, recipient_id: private_message.recipient_id,
@ -293,18 +346,25 @@ fn receive_undo_delete_private_message(
published: None, published: None,
updated: Some(naive_now()), updated: Some(naive_now()),
}; };
PrivateMessage::update(conn, private_message_id, &private_message_form)?;
let message = PrivateMessageView::read(&conn, private_message_id)?; blocking(pool, move |conn| {
PrivateMessage::update(conn, private_message_id, &private_message_form)
})
.await??;
let res = PrivateMessageResponse { let message = blocking(pool, move |conn| {
message: message.to_owned(), PrivateMessageView::read(&conn, private_message_id)
}; })
.await??;
let res = PrivateMessageResponse { message };
let recipient_id = res.message.recipient_id;
chat_server.do_send(SendUserRoomMessage { chat_server.do_send(SendUserRoomMessage {
op: UserOperation::EditPrivateMessage, op: UserOperation::EditPrivateMessage,
response: res, response: res,
recipient_id: message.recipient_id, recipient_id,
my_id: None, my_id: None,
}); });

View file

@ -1,9 +1,9 @@
use crate::{db::Crud, schema::activity}; use crate::{blocking, db::Crud, schema::activity, DbPool, LemmyError};
use diesel::{dsl::*, result::Error, *}; use diesel::{dsl::*, result::Error, *};
use failure::_core::fmt::Debug;
use log::debug; use log::debug;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_json::Value; use serde_json::Value;
use std::fmt::Debug;
#[derive(Queryable, Identifiable, PartialEq, Debug, Serialize, Deserialize)] #[derive(Queryable, Identifiable, PartialEq, Debug, Serialize, Deserialize)]
#[table_name = "activity"] #[table_name = "activity"]
@ -55,12 +55,28 @@ impl Crud<ActivityForm> for Activity {
} }
} }
pub fn insert_activity<T>( pub async fn insert_activity<T>(
user_id: i32,
data: T,
local: bool,
pool: &DbPool,
) -> Result<(), LemmyError>
where
T: Serialize + Debug + Send + 'static,
{
blocking(pool, move |conn| {
do_insert_activity(conn, user_id, &data, local)
})
.await??;
Ok(())
}
fn do_insert_activity<T>(
conn: &PgConnection, conn: &PgConnection,
user_id: i32, user_id: i32,
data: &T, data: &T,
local: bool, local: bool,
) -> Result<(), failure::Error> ) -> Result<(), LemmyError>
where where
T: Serialize + Debug, T: Serialize + Debug,
{ {

View file

@ -10,21 +10,22 @@ use crate::{
apub::{extensions::signatures::generate_actor_keypair, make_apub_endpoint, EndpointType}, apub::{extensions::signatures::generate_actor_keypair, make_apub_endpoint, EndpointType},
db::Crud, db::Crud,
naive_now, naive_now,
LemmyError,
}; };
use diesel::*; use diesel::*;
use failure::Error;
use log::info; use log::info;
pub fn run_advanced_migrations(conn: &PgConnection) -> Result<(), Error> { pub fn run_advanced_migrations(conn: &PgConnection) -> Result<(), LemmyError> {
user_updates_2020_04_02(&conn)?; user_updates_2020_04_02(&conn)?;
community_updates_2020_04_02(&conn)?; community_updates_2020_04_02(&conn)?;
post_updates_2020_04_03(&conn)?; post_updates_2020_04_03(&conn)?;
comment_updates_2020_04_03(&conn)?; comment_updates_2020_04_03(&conn)?;
private_message_updates_2020_05_05(&conn)?; private_message_updates_2020_05_05(&conn)?;
Ok(()) Ok(())
} }
fn user_updates_2020_04_02(conn: &PgConnection) -> Result<(), Error> { fn user_updates_2020_04_02(conn: &PgConnection) -> Result<(), LemmyError> {
use crate::schema::user_::dsl::*; use crate::schema::user_::dsl::*;
info!("Running user_updates_2020_04_02"); info!("Running user_updates_2020_04_02");
@ -75,7 +76,7 @@ fn user_updates_2020_04_02(conn: &PgConnection) -> Result<(), Error> {
Ok(()) Ok(())
} }
fn community_updates_2020_04_02(conn: &PgConnection) -> Result<(), Error> { fn community_updates_2020_04_02(conn: &PgConnection) -> Result<(), LemmyError> {
use crate::schema::community::dsl::*; use crate::schema::community::dsl::*;
info!("Running community_updates_2020_04_02"); info!("Running community_updates_2020_04_02");
@ -119,7 +120,7 @@ fn community_updates_2020_04_02(conn: &PgConnection) -> Result<(), Error> {
Ok(()) Ok(())
} }
fn post_updates_2020_04_03(conn: &PgConnection) -> Result<(), Error> { fn post_updates_2020_04_03(conn: &PgConnection) -> Result<(), LemmyError> {
use crate::schema::post::dsl::*; use crate::schema::post::dsl::*;
info!("Running post_updates_2020_04_03"); info!("Running post_updates_2020_04_03");
@ -143,7 +144,7 @@ fn post_updates_2020_04_03(conn: &PgConnection) -> Result<(), Error> {
Ok(()) Ok(())
} }
fn comment_updates_2020_04_03(conn: &PgConnection) -> Result<(), Error> { fn comment_updates_2020_04_03(conn: &PgConnection) -> Result<(), LemmyError> {
use crate::schema::comment::dsl::*; use crate::schema::comment::dsl::*;
info!("Running comment_updates_2020_04_03"); info!("Running comment_updates_2020_04_03");
@ -167,7 +168,7 @@ fn comment_updates_2020_04_03(conn: &PgConnection) -> Result<(), Error> {
Ok(()) Ok(())
} }
fn private_message_updates_2020_05_05(conn: &PgConnection) -> Result<(), Error> { fn private_message_updates_2020_05_05(conn: &PgConnection) -> Result<(), LemmyError> {
use crate::schema::private_message::dsl::*; use crate::schema::private_message::dsl::*;
info!("Running private_message_updates_2020_05_05"); info!("Running private_message_updates_2020_05_05");

View file

@ -12,7 +12,7 @@ use crate::{
// ) // )
// SELECT * FROM MyTree; // SELECT * FROM MyTree;
#[derive(Queryable, Associations, Identifiable, PartialEq, Debug, Serialize, Deserialize)] #[derive(Clone, Queryable, Associations, Identifiable, PartialEq, Debug, Serialize, Deserialize)]
#[belongs_to(Post)] #[belongs_to(Post)]
#[table_name = "comment"] #[table_name = "comment"]
pub struct Comment { pub struct Comment {

View file

@ -5,7 +5,7 @@ use crate::{
use diesel::{dsl::*, result::Error, *}; use diesel::{dsl::*, result::Error, *};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
#[derive(Queryable, Identifiable, PartialEq, Debug, Serialize, Deserialize)] #[derive(Clone, Queryable, Identifiable, PartialEq, Debug, Serialize, Deserialize)]
#[table_name = "community"] #[table_name = "community"]
pub struct Community { pub struct Community {
pub id: i32, pub id: i32,

View file

@ -50,8 +50,8 @@ impl Crud<PasswordResetRequestForm> for PasswordResetRequest {
impl PasswordResetRequest { impl PasswordResetRequest {
pub fn create_token(conn: &PgConnection, from_user_id: i32, token: &str) -> Result<Self, Error> { pub fn create_token(conn: &PgConnection, from_user_id: i32, token: &str) -> Result<Self, Error> {
let mut hasher = Sha256::new(); let mut hasher = Sha256::new();
hasher.input(token); hasher.update(token);
let token_hash: String = PasswordResetRequest::bytes_to_hex(hasher.result().to_vec()); let token_hash: String = PasswordResetRequest::bytes_to_hex(hasher.finalize().to_vec());
let form = PasswordResetRequestForm { let form = PasswordResetRequestForm {
user_id: from_user_id, user_id: from_user_id,
@ -62,8 +62,8 @@ impl PasswordResetRequest {
} }
pub fn read_from_token(conn: &PgConnection, token: &str) -> Result<Self, Error> { pub fn read_from_token(conn: &PgConnection, token: &str) -> Result<Self, Error> {
let mut hasher = Sha256::new(); let mut hasher = Sha256::new();
hasher.input(token); hasher.update(token);
let token_hash: String = PasswordResetRequest::bytes_to_hex(hasher.result().to_vec()); let token_hash: String = PasswordResetRequest::bytes_to_hex(hasher.finalize().to_vec());
password_reset_request password_reset_request
.filter(token_encrypted.eq(token_hash)) .filter(token_encrypted.eq(token_hash))
.filter(published.gt(now - 1.days())) .filter(published.gt(now - 1.days()))

View file

@ -10,7 +10,7 @@ use diesel::{dsl::*, result::Error, *};
use jsonwebtoken::{decode, encode, DecodingKey, EncodingKey, Header, TokenData, Validation}; use jsonwebtoken::{decode, encode, DecodingKey, EncodingKey, Header, TokenData, Validation};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
#[derive(Queryable, Identifiable, PartialEq, Debug)] #[derive(Clone, Queryable, Identifiable, PartialEq, Debug)]
#[table_name = "user_"] #[table_name = "user_"]
pub struct User_ { pub struct User_ {
pub id: i32, pub id: i32,

View file

@ -26,20 +26,39 @@ pub extern crate serde_json;
pub extern crate sha2; pub extern crate sha2;
pub extern crate strum; pub extern crate strum;
pub async fn blocking<F, T>(pool: &DbPool, f: F) -> Result<T, LemmyError>
where
F: FnOnce(&diesel::PgConnection) -> T + Send + 'static,
T: Send + 'static,
{
let pool = pool.clone();
let res = actix_web::web::block(move || {
let conn = pool.get()?;
let res = (f)(&conn);
Ok(res) as Result<_, LemmyError>
})
.await?;
Ok(res)
}
pub mod api; pub mod api;
pub mod apub; pub mod apub;
pub mod db; pub mod db;
pub mod rate_limit; pub mod rate_limit;
pub mod request;
pub mod routes; pub mod routes;
pub mod schema; pub mod schema;
pub mod settings; pub mod settings;
pub mod version; pub mod version;
pub mod websocket; pub mod websocket;
use crate::settings::Settings; use crate::{
use actix_web::dev::ConnectionInfo; request::{retry, RecvError},
settings::Settings,
};
use actix_web::{client::Client, dev::ConnectionInfo};
use chrono::{DateTime, FixedOffset, Local, NaiveDateTime, Utc}; use chrono::{DateTime, FixedOffset, Local, NaiveDateTime, Utc};
use isahc::prelude::*;
use itertools::Itertools; use itertools::Itertools;
use lettre::{ use lettre::{
smtp::{ smtp::{
@ -58,12 +77,35 @@ use rand::{distributions::Alphanumeric, thread_rng, Rng};
use regex::{Regex, RegexBuilder}; use regex::{Regex, RegexBuilder};
use serde::Deserialize; use serde::Deserialize;
pub type DbPool = diesel::r2d2::Pool<diesel::r2d2::ConnectionManager<diesel::PgConnection>>;
pub type ConnectionId = usize; pub type ConnectionId = usize;
pub type PostId = i32; pub type PostId = i32;
pub type CommunityId = i32; pub type CommunityId = i32;
pub type UserId = i32; pub type UserId = i32;
pub type IPAddr = String; pub type IPAddr = String;
#[derive(Debug)]
pub struct LemmyError {
inner: failure::Error,
}
impl std::fmt::Display for LemmyError {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
self.inner.fmt(f)
}
}
impl actix_web::error::ResponseError for LemmyError {}
impl<T> From<T> for LemmyError
where
T: Into<failure::Error>,
{
fn from(t: T) -> Self {
LemmyError { inner: t.into() }
}
}
pub fn to_datetime_utc(ndt: NaiveDateTime) -> DateTime<Utc> { pub fn to_datetime_utc(ndt: NaiveDateTime) -> DateTime<Utc> {
DateTime::<Utc>::from_utc(ndt, Utc) DateTime::<Utc>::from_utc(ndt, Utc)
} }
@ -85,8 +127,10 @@ pub fn is_email_regex(test: &str) -> bool {
EMAIL_REGEX.is_match(test) EMAIL_REGEX.is_match(test)
} }
pub fn is_image_content_type(test: &str) -> Result<(), failure::Error> { pub async fn is_image_content_type(client: &Client, test: &str) -> Result<(), LemmyError> {
if isahc::get(test)? let response = retry(|| client.get(test).send()).await?;
if response
.headers() .headers()
.get("Content-Type") .get("Content-Type")
.ok_or_else(|| format_err!("No Content-Type header"))? .ok_or_else(|| format_err!("No Content-Type header"))?
@ -95,7 +139,7 @@ pub fn is_image_content_type(test: &str) -> Result<(), failure::Error> {
{ {
Ok(()) Ok(())
} else { } else {
Err(format_err!("Not an image type.")) Err(format_err!("Not an image type.").into())
} }
} }
@ -178,10 +222,15 @@ pub struct IframelyResponse {
html: Option<String>, html: Option<String>,
} }
pub fn fetch_iframely(url: &str) -> Result<IframelyResponse, failure::Error> { pub async fn fetch_iframely(client: &Client, url: &str) -> Result<IframelyResponse, LemmyError> {
let fetch_url = format!("http://iframely/oembed?url={}", url); let fetch_url = format!("http://iframely/oembed?url={}", url);
let text = isahc::get(&fetch_url)?.text()?;
let res: IframelyResponse = serde_json::from_str(&text)?; let mut response = retry(|| client.get(&fetch_url).send()).await?;
let res: IframelyResponse = response
.json()
.await
.map_err(|e| RecvError(e.to_string()))?;
Ok(res) Ok(res)
} }
@ -197,23 +246,30 @@ pub struct PictrsFile {
delete_token: String, delete_token: String,
} }
pub fn fetch_pictrs(image_url: &str) -> Result<PictrsResponse, failure::Error> { pub async fn fetch_pictrs(client: &Client, image_url: &str) -> Result<PictrsResponse, LemmyError> {
is_image_content_type(image_url)?; is_image_content_type(client, image_url).await?;
let fetch_url = format!( let fetch_url = format!(
"http://pictrs:8080/image/download?url={}", "http://pictrs:8080/image/download?url={}",
utf8_percent_encode(image_url, NON_ALPHANUMERIC) // TODO this might not be needed utf8_percent_encode(image_url, NON_ALPHANUMERIC) // TODO this might not be needed
); );
let text = isahc::get(&fetch_url)?.text()?;
let res: PictrsResponse = serde_json::from_str(&text)?; let mut response = retry(|| client.get(&fetch_url).send()).await?;
if res.msg == "ok" {
Ok(res) let response: PictrsResponse = response
.json()
.await
.map_err(|e| RecvError(e.to_string()))?;
if response.msg == "ok" {
Ok(response)
} else { } else {
Err(format_err!("{}", &res.msg)) Err(format_err!("{}", &response.msg).into())
} }
} }
fn fetch_iframely_and_pictrs_data( async fn fetch_iframely_and_pictrs_data(
client: &Client,
url: Option<String>, url: Option<String>,
) -> ( ) -> (
Option<String>, Option<String>,
@ -225,7 +281,7 @@ fn fetch_iframely_and_pictrs_data(
Some(url) => { Some(url) => {
// Fetch iframely data // Fetch iframely data
let (iframely_title, iframely_description, iframely_thumbnail_url, iframely_html) = let (iframely_title, iframely_description, iframely_thumbnail_url, iframely_html) =
match fetch_iframely(url) { match fetch_iframely(client, url).await {
Ok(res) => (res.title, res.description, res.thumbnail_url, res.html), Ok(res) => (res.title, res.description, res.thumbnail_url, res.html),
Err(e) => { Err(e) => {
error!("iframely err: {}", e); error!("iframely err: {}", e);
@ -235,7 +291,7 @@ fn fetch_iframely_and_pictrs_data(
// Fetch pictrs thumbnail // Fetch pictrs thumbnail
let pictrs_thumbnail = match iframely_thumbnail_url { let pictrs_thumbnail = match iframely_thumbnail_url {
Some(iframely_thumbnail_url) => match fetch_pictrs(&iframely_thumbnail_url) { Some(iframely_thumbnail_url) => match fetch_pictrs(client, &iframely_thumbnail_url).await {
Ok(res) => Some(res.files[0].file.to_owned()), Ok(res) => Some(res.files[0].file.to_owned()),
Err(e) => { Err(e) => {
error!("pictrs err: {}", e); error!("pictrs err: {}", e);
@ -243,7 +299,7 @@ fn fetch_iframely_and_pictrs_data(
} }
}, },
// Try to generate a small thumbnail if iframely is not supported // Try to generate a small thumbnail if iframely is not supported
None => match fetch_pictrs(&url) { None => match fetch_pictrs(client, &url).await {
Ok(res) => Some(res.files[0].file.to_owned()), Ok(res) => Some(res.files[0].file.to_owned()),
Err(e) => { Err(e) => {
error!("pictrs err: {}", e); error!("pictrs err: {}", e);
@ -269,7 +325,7 @@ pub fn markdown_to_html(text: &str) -> String {
pub fn get_ip(conn_info: &ConnectionInfo) -> String { pub fn get_ip(conn_info: &ConnectionInfo) -> String {
conn_info conn_info
.remote() .remote_addr()
.unwrap_or("127.0.0.1:12345") .unwrap_or("127.0.0.1:12345")
.split(':') .split(':')
.next() .next()
@ -327,21 +383,25 @@ mod tests {
#[test] #[test]
fn test_mentions_regex() { fn test_mentions_regex() {
let text = "Just read a great blog post by [@tedu@honk.teduangst.com](/u/test). And another by !test_community@fish.teduangst.com . Another [@lemmy@lemmy_alpha:8540](/u/fish)"; let text = "Just read a great blog post by [@tedu@honk.teduangst.com](/u/test). And another by !test_community@fish.teduangst.com . Another [@lemmy@lemmy-alpha:8540](/u/fish)";
let mentions = scrape_text_for_mentions(text); let mentions = scrape_text_for_mentions(text);
assert_eq!(mentions[0].name, "tedu".to_string()); assert_eq!(mentions[0].name, "tedu".to_string());
assert_eq!(mentions[0].domain, "honk.teduangst.com".to_string()); assert_eq!(mentions[0].domain, "honk.teduangst.com".to_string());
assert_eq!(mentions[1].domain, "lemmy_alpha:8540".to_string()); assert_eq!(mentions[1].domain, "lemmy-alpha:8540".to_string());
} }
#[test] #[test]
fn test_image() { fn test_image() {
assert!(is_image_content_type("https://1734811051.rsc.cdn77.org/data/images/full/365645/as-virus-kills-navajos-in-their-homes-tribal-women-provide-lifeline.jpg?w=600?w=650").is_ok()); actix_rt::System::new("tset_image").block_on(async move {
assert!(is_image_content_type( let client = actix_web::client::Client::default();
assert!(is_image_content_type(&client, "https://1734811051.rsc.cdn77.org/data/images/full/365645/as-virus-kills-navajos-in-their-homes-tribal-women-provide-lifeline.jpg?w=600?w=650").await.is_ok());
assert!(is_image_content_type(&client,
"https://twitter.com/BenjaminNorton/status/1259922424272957440?s=20" "https://twitter.com/BenjaminNorton/status/1259922424272957440?s=20"
) )
.is_err()); .await.is_err()
);
});
} }
#[test] #[test]
@ -399,7 +459,7 @@ mod tests {
// These helped with testing // These helped with testing
// #[test] // #[test]
// fn test_iframely() { // fn test_iframely() {
// let res = fetch_iframely("https://www.redspark.nu/?p=15341"); // let res = fetch_iframely(client, "https://www.redspark.nu/?p=15341").await;
// assert!(res.is_ok()); // assert!(res.is_ok());
// } // }
@ -420,7 +480,7 @@ mod tests {
lazy_static! { lazy_static! {
static ref EMAIL_REGEX: Regex = Regex::new(r"^[a-zA-Z0-9.!#$%&*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$").unwrap(); static ref EMAIL_REGEX: Regex = Regex::new(r"^[a-zA-Z0-9.!#$%&*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$").unwrap();
static ref SLUR_REGEX: Regex = RegexBuilder::new(r"(fag(g|got|tard)?|maricos?|cock\s?sucker(s|ing)?|n(i|1)g(\b|g?(a|er)?(s|z)?)\b|dindu(s?)|mudslime?s?|kikes?|mongoloids?|towel\s*heads?|\bspi(c|k)s?\b|\bchinks?|niglets?|beaners?|\bnips?\b|\bcoons?\b|jungle\s*bunn(y|ies?)|jigg?aboo?s?|\bpakis?\b|rag\s*heads?|gooks?|cunts?|bitch(es|ing|y)?|puss(y|ies?)|twats?|feminazis?|whor(es?|ing)|\bslut(s|t?y)?|\btr(a|@)nn?(y|ies?)|ladyboy(s?)|\b(b|re|r)tard(ed)?s?)").case_insensitive(true).build().unwrap(); static ref SLUR_REGEX: Regex = RegexBuilder::new(r"(fag(g|got|tard)?|maricos?|cock\s?sucker(s|ing)?|\bn(i|1)g(\b|g?(a|er)?(s|z)?)\b|dindu(s?)|mudslime?s?|kikes?|mongoloids?|towel\s*heads?|\bspi(c|k)s?\b|\bchinks?|niglets?|beaners?|\bnips?\b|\bcoons?\b|jungle\s*bunn(y|ies?)|jigg?aboo?s?|\bpakis?\b|rag\s*heads?|gooks?|cunts?|bitch(es|ing|y)?|puss(y|ies?)|twats?|feminazis?|whor(es?|ing)|\bslut(s|t?y)?|\btr(a|@)nn?(y|ies?)|ladyboy(s?)|\b(b|re|r)tard(ed)?s?)").case_insensitive(true).build().unwrap();
static ref USERNAME_MATCHES_REGEX: Regex = Regex::new(r"/u/[a-zA-Z][0-9a-zA-Z_]*").unwrap(); static ref USERNAME_MATCHES_REGEX: Regex = Regex::new(r"/u/[a-zA-Z][0-9a-zA-Z_]*").unwrap();
// TODO keep this old one, it didn't work with port well tho // TODO keep this old one, it didn't work with port well tho
// static ref WEBFINGER_USER_REGEX: Regex = Regex::new(r"@(?P<name>[\w.]+)@(?P<domain>[a-zA-Z0-9._-]+\.[a-zA-Z0-9_-]+)").unwrap(); // static ref WEBFINGER_USER_REGEX: Regex = Regex::new(r"@(?P<name>[\w.]+)@(?P<domain>[a-zA-Z0-9._-]+\.[a-zA-Z0-9_-]+)").unwrap();

View file

@ -4,10 +4,13 @@ extern crate diesel_migrations;
#[macro_use] #[macro_use]
pub extern crate lazy_static; pub extern crate lazy_static;
pub type DbPool = Pool<ConnectionManager<PgConnection>>;
use crate::lemmy_server::actix_web::dev::Service; use crate::lemmy_server::actix_web::dev::Service;
use actix::prelude::*; use actix::prelude::*;
use actix_web::{ use actix_web::{
body::Body, body::Body,
client::Client,
dev::{ServiceRequest, ServiceResponse}, dev::{ServiceRequest, ServiceResponse},
http::{ http::{
header::{CACHE_CONTROL, CONTENT_TYPE}, header::{CACHE_CONTROL, CONTENT_TYPE},
@ -20,14 +23,16 @@ use diesel::{
PgConnection, PgConnection,
}; };
use lemmy_server::{ use lemmy_server::{
blocking,
db::code_migrations::run_advanced_migrations, db::code_migrations::run_advanced_migrations,
rate_limit::{rate_limiter::RateLimiter, RateLimit}, rate_limit::{rate_limiter::RateLimiter, RateLimit},
routes::{api, federation, feeds, index, nodeinfo, webfinger}, routes::{api, federation, feeds, index, nodeinfo, webfinger},
settings::Settings, settings::Settings,
websocket::server::*, websocket::server::*,
LemmyError,
}; };
use regex::Regex; use regex::Regex;
use std::{io, sync::Arc}; use std::sync::Arc;
use tokio::sync::Mutex; use tokio::sync::Mutex;
lazy_static! { lazy_static! {
@ -41,7 +46,7 @@ lazy_static! {
embed_migrations!(); embed_migrations!();
#[actix_rt::main] #[actix_rt::main]
async fn main() -> io::Result<()> { async fn main() -> Result<(), LemmyError> {
env_logger::init(); env_logger::init();
let settings = Settings::get(); let settings = Settings::get();
@ -53,9 +58,12 @@ async fn main() -> io::Result<()> {
.unwrap_or_else(|_| panic!("Error connecting to {}", settings.get_database_url())); .unwrap_or_else(|_| panic!("Error connecting to {}", settings.get_database_url()));
// Run the migrations from code // Run the migrations from code
let conn = pool.get().unwrap(); blocking(&pool, move |conn| {
embedded_migrations::run(&conn).unwrap(); embedded_migrations::run(conn)?;
run_advanced_migrations(&conn).unwrap(); run_advanced_migrations(conn)?;
Ok(()) as Result<(), LemmyError>
})
.await??;
// Set up the rate limiter // Set up the rate limiter
let rate_limiter = RateLimit { let rate_limiter = RateLimit {
@ -63,7 +71,7 @@ async fn main() -> io::Result<()> {
}; };
// Set up websocket server // Set up websocket server
let server = ChatServer::startup(pool.clone(), rate_limiter.clone()).start(); let server = ChatServer::startup(pool.clone(), rate_limiter.clone(), Client::default()).start();
println!( println!(
"Starting http server at {}:{}", "Starting http server at {}:{}",
@ -79,6 +87,7 @@ async fn main() -> io::Result<()> {
.wrap(middleware::Logger::default()) .wrap(middleware::Logger::default())
.data(pool.clone()) .data(pool.clone())
.data(server.clone()) .data(server.clone())
.data(Client::default())
// The routes // The routes
.configure(move |cfg| api::config(cfg, &rate_limiter)) .configure(move |cfg| api::config(cfg, &rate_limiter))
.configure(federation::config) .configure(federation::config)
@ -98,7 +107,9 @@ async fn main() -> io::Result<()> {
}) })
.bind((settings.bind, settings.port))? .bind((settings.bind, settings.port))?
.run() .run()
.await .await?;
Ok(())
} }
fn add_cache_headers<S>( fn add_cache_headers<S>(

View file

@ -1,5 +1,5 @@
use super::{IPAddr, Settings}; use super::{IPAddr, Settings};
use crate::{api::APIError, get_ip, settings::RateLimitConfig}; use crate::{get_ip, settings::RateLimitConfig, LemmyError};
use actix_web::dev::{Service, ServiceRequest, ServiceResponse, Transform}; use actix_web::dev::{Service, ServiceRequest, ServiceResponse, Transform};
use futures::future::{ok, Ready}; use futures::future::{ok, Ready};
use rate_limiter::{RateLimitType, RateLimiter}; use rate_limiter::{RateLimitType, RateLimiter};
@ -15,6 +15,8 @@ pub mod rate_limiter;
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct RateLimit { pub struct RateLimit {
// it might be reasonable to use a std::sync::Mutex here, since we don't need to lock this
// across await points
pub rate_limiter: Arc<Mutex<RateLimiter>>, pub rate_limiter: Arc<Mutex<RateLimiter>>,
} }
@ -57,17 +59,11 @@ impl RateLimited {
fut: impl Future<Output = Result<T, E>>, fut: impl Future<Output = Result<T, E>>,
) -> Result<T, E> ) -> Result<T, E>
where where
E: From<failure::Error>, E: From<LemmyError>,
{ {
let rate_limit: RateLimitConfig = actix_web::web::block(move || { // Does not need to be blocking because the RwLock in settings never held across await points,
// needs to be in a web::block because the RwLock in settings is from stdlib // and the operation here locks only long enough to clone
Ok(Settings::get().rate_limit) as Result<_, failure::Error> let rate_limit: RateLimitConfig = Settings::get().rate_limit;
})
.await
.map_err(|e| match e {
actix_web::error::BlockingError::Error(e) => e,
_ => APIError::err("Operation canceled").into(),
})?;
// before // before
{ {
@ -83,6 +79,7 @@ impl RateLimited {
false, false,
)?; )?;
drop(limiter);
return fut.await; return fut.await;
} }
RateLimitType::Post => { RateLimitType::Post => {

View file

@ -1,6 +1,5 @@
use super::IPAddr; use super::IPAddr;
use crate::api::APIError; use crate::{api::APIError, LemmyError};
use failure::Error;
use log::debug; use log::debug;
use std::{collections::HashMap, time::SystemTime}; use std::{collections::HashMap, time::SystemTime};
use strum::IntoEnumIterator; use strum::IntoEnumIterator;
@ -11,7 +10,7 @@ pub struct RateLimitBucket {
allowance: f64, allowance: f64,
} }
#[derive(Eq, PartialEq, Hash, Debug, EnumIter, Copy, Clone)] #[derive(Eq, PartialEq, Hash, Debug, EnumIter, Copy, Clone, AsRefStr)]
pub enum RateLimitType { pub enum RateLimitType {
Message, Message,
Register, Register,
@ -61,7 +60,7 @@ impl RateLimiter {
rate: i32, rate: i32,
per: i32, per: i32,
check_only: bool, check_only: bool,
) -> Result<(), Error> { ) -> Result<(), LemmyError> {
self.insert_ip(ip); self.insert_ip(ip);
if let Some(bucket) = self.buckets.get_mut(&type_) { if let Some(bucket) = self.buckets.get_mut(&type_) {
if let Some(rate_limit) = bucket.get_mut(ip) { if let Some(rate_limit) = bucket.get_mut(ip) {
@ -81,12 +80,21 @@ impl RateLimiter {
if rate_limit.allowance < 1.0 { if rate_limit.allowance < 1.0 {
debug!( debug!(
"Rate limited IP: {}, time_passed: {}, allowance: {}", "Rate limited type: {}, IP: {}, time_passed: {}, allowance: {}",
ip, time_passed, rate_limit.allowance type_.as_ref(),
ip,
time_passed,
rate_limit.allowance
); );
Err( Err(
APIError { APIError {
message: format!("Too many requests. {} per {} seconds", rate, per), message: format!(
"Too many requests. type: {}, IP: {}, {} per {} seconds",
type_.as_ref(),
ip,
rate,
per
),
} }
.into(), .into(),
) )

51
server/src/request.rs Normal file
View file

@ -0,0 +1,51 @@
use crate::LemmyError;
use std::future::Future;
#[derive(Clone, Debug, Fail)]
#[fail(display = "Error sending request, {}", _0)]
struct SendError(pub String);
#[derive(Clone, Debug, Fail)]
#[fail(display = "Error receiving response, {}", _0)]
pub struct RecvError(pub String);
pub async fn retry<F, Fut, T>(f: F) -> Result<T, LemmyError>
where
F: Fn() -> Fut,
Fut: Future<Output = Result<T, actix_web::client::SendRequestError>>,
{
retry_custom(|| async { Ok((f)().await) }).await
}
pub async fn retry_custom<F, Fut, T>(f: F) -> Result<T, LemmyError>
where
F: Fn() -> Fut,
Fut: Future<Output = Result<Result<T, actix_web::client::SendRequestError>, LemmyError>>,
{
let mut response = Err(format_err!("connect timeout").into());
for _ in 0u8..3 {
match (f)().await? {
Ok(t) => return Ok(t),
Err(e) => {
if is_connect_timeout(&e) {
response = Err(SendError(e.to_string()).into());
continue;
}
return Err(SendError(e.to_string()).into());
}
}
}
response
}
fn is_connect_timeout(e: &actix_web::client::SendRequestError) -> bool {
if let actix_web::client::SendRequestError::Connect(e) = e {
if let actix_web::client::ConnectError::Timeout = e {
return true;
}
}
false
}

View file

@ -4,7 +4,7 @@ use crate::{
routes::{ChatServerParam, DbPoolParam}, routes::{ChatServerParam, DbPoolParam},
websocket::WebsocketInfo, websocket::WebsocketInfo,
}; };
use actix_web::{error::ErrorBadRequest, *}; use actix_web::{client::Client, error::ErrorBadRequest, *};
use serde::Serialize; use serde::Serialize;
pub fn config(cfg: &mut web::ServiceConfig, rate_limit: &RateLimit) { pub fn config(cfg: &mut web::ServiceConfig, rate_limit: &RateLimit) {
@ -150,6 +150,7 @@ pub fn config(cfg: &mut web::ServiceConfig, rate_limit: &RateLimit) {
async fn perform<Request>( async fn perform<Request>(
data: Request, data: Request,
client: &Client,
db: DbPoolParam, db: DbPoolParam,
chat_server: ChatServerParam, chat_server: ChatServerParam,
) -> Result<HttpResponse, Error> ) -> Result<HttpResponse, Error>
@ -162,9 +163,10 @@ where
id: None, id: None,
}; };
let oper: Oper<Request> = Oper::new(data); let oper: Oper<Request> = Oper::new(data, client.clone());
let res = web::block(move || oper.perform(db.get_ref().to_owned(), Some(ws_info))) let res = oper
.perform(&db, Some(ws_info))
.await .await
.map(|json| HttpResponse::Ok().json(json)) .map(|json| HttpResponse::Ok().json(json))
.map_err(ErrorBadRequest)?; .map_err(ErrorBadRequest)?;
@ -173,6 +175,7 @@ where
async fn route_get<Data>( async fn route_get<Data>(
data: web::Query<Data>, data: web::Query<Data>,
client: web::Data<Client>,
db: DbPoolParam, db: DbPoolParam,
chat_server: ChatServerParam, chat_server: ChatServerParam,
) -> Result<HttpResponse, Error> ) -> Result<HttpResponse, Error>
@ -180,11 +183,12 @@ where
Data: Serialize + Send + 'static, Data: Serialize + Send + 'static,
Oper<Data>: Perform, Oper<Data>: Perform,
{ {
perform::<Data>(data.0, db, chat_server).await perform::<Data>(data.0, &client, db, chat_server).await
} }
async fn route_post<Data>( async fn route_post<Data>(
data: web::Json<Data>, data: web::Json<Data>,
client: web::Data<Client>,
db: DbPoolParam, db: DbPoolParam,
chat_server: ChatServerParam, chat_server: ChatServerParam,
) -> Result<HttpResponse, Error> ) -> Result<HttpResponse, Error>
@ -192,5 +196,5 @@ where
Data: Serialize + Send + 'static, Data: Serialize + Send + 'static,
Oper<Data>: Perform, Oper<Data>: Perform,
{ {
perform::<Data>(data.0, db, chat_server).await perform::<Data>(data.0, &client, db, chat_server).await
} }

View file

@ -12,6 +12,8 @@ use crate::{
settings::Settings, settings::Settings,
}; };
use actix_web::*; use actix_web::*;
use http_signature_normalization_actix::digest::middleware::VerifyDigest;
use sha2::{Digest, Sha256};
pub fn config(cfg: &mut web::ServiceConfig) { pub fn config(cfg: &mut web::ServiceConfig) {
if Settings::get().federation.enabled { if Settings::get().federation.enabled {
@ -38,8 +40,12 @@ pub fn config(cfg: &mut web::ServiceConfig) {
.route("/comment/{comment_id}", web::get().to(get_apub_comment)), .route("/comment/{comment_id}", web::get().to(get_apub_comment)),
) )
// Inboxes dont work with the header guard for some reason. // Inboxes dont work with the header guard for some reason.
.service(
web::scope("/")
.wrap(VerifyDigest::new(Sha256::new()))
.route("/c/{community_name}/inbox", web::post().to(community_inbox)) .route("/c/{community_name}/inbox", web::post().to(community_inbox))
.route("/u/{user_name}/inbox", web::post().to(user_inbox)) .route("/u/{user_name}/inbox", web::post().to(user_inbox))
.route("/inbox", web::post().to(shared_inbox)); .route("/inbox", web::post().to(shared_inbox)),
);
} }
} }

View file

@ -1,4 +1,5 @@
use crate::{ use crate::{
blocking,
db::{ db::{
comment_view::{ReplyQueryBuilder, ReplyView}, comment_view::{ReplyQueryBuilder, ReplyView},
community::Community, community::Community,
@ -12,6 +13,7 @@ use crate::{
markdown_to_html, markdown_to_html,
routes::DbPoolParam, routes::DbPoolParam,
settings::Settings, settings::Settings,
LemmyError,
}; };
use actix_web::{error::ErrorBadRequest, *}; use actix_web::{error::ErrorBadRequest, *};
use chrono::{DateTime, NaiveDateTime, Utc}; use chrono::{DateTime, NaiveDateTime, Utc};
@ -43,21 +45,20 @@ pub fn config(cfg: &mut web::ServiceConfig) {
} }
async fn get_all_feed(info: web::Query<Params>, db: DbPoolParam) -> Result<HttpResponse, Error> { async fn get_all_feed(info: web::Query<Params>, db: DbPoolParam) -> Result<HttpResponse, Error> {
let res = web::block(move || { let sort_type = get_sort_type(info).map_err(ErrorBadRequest)?;
let conn = db.get()?;
get_feed_all_data(&conn, &get_sort_type(info)?) let rss = blocking(&db, move |conn| get_feed_all_data(conn, &sort_type))
}) .await?
.await .map_err(ErrorBadRequest)?;
.map(|rss| {
Ok(
HttpResponse::Ok() HttpResponse::Ok()
.content_type("application/rss+xml") .content_type("application/rss+xml")
.body(rss) .body(rss),
}) )
.map_err(ErrorBadRequest)?;
Ok(res)
} }
fn get_feed_all_data(conn: &PgConnection, sort_type: &SortType) -> Result<String, failure::Error> { fn get_feed_all_data(conn: &PgConnection, sort_type: &SortType) -> Result<String, LemmyError> {
let site_view = SiteView::read(&conn)?; let site_view = SiteView::read(&conn)?;
let posts = PostQueryBuilder::create(&conn) let posts = PostQueryBuilder::create(&conn)
@ -85,37 +86,34 @@ async fn get_feed(
info: web::Query<Params>, info: web::Query<Params>,
db: web::Data<Pool<ConnectionManager<PgConnection>>>, db: web::Data<Pool<ConnectionManager<PgConnection>>>,
) -> Result<HttpResponse, Error> { ) -> Result<HttpResponse, Error> {
let res = web::block(move || { let sort_type = get_sort_type(info).map_err(ErrorBadRequest)?;
let conn = db.get()?;
let sort_type = get_sort_type(info)?;
let request_type = match path.0.as_ref() { let request_type = match path.0.as_ref() {
"u" => RequestType::User, "u" => RequestType::User,
"c" => RequestType::Community, "c" => RequestType::Community,
"front" => RequestType::Front, "front" => RequestType::Front,
"inbox" => RequestType::Inbox, "inbox" => RequestType::Inbox,
_ => return Err(format_err!("wrong_type")), _ => return Err(ErrorBadRequest(LemmyError::from(format_err!("wrong_type")))),
}; };
let param = path.1.to_owned(); let param = path.1.to_owned();
match request_type { let builder = blocking(&db, move |conn| match request_type {
RequestType::User => get_feed_user(&conn, &sort_type, param), RequestType::User => get_feed_user(conn, &sort_type, param),
RequestType::Community => get_feed_community(&conn, &sort_type, param), RequestType::Community => get_feed_community(conn, &sort_type, param),
RequestType::Front => get_feed_front(&conn, &sort_type, param), RequestType::Front => get_feed_front(conn, &sort_type, param),
RequestType::Inbox => get_feed_inbox(&conn, param), RequestType::Inbox => get_feed_inbox(conn, param),
}
}) })
.await .await?
.map(|builder| builder.build().unwrap().to_string()) .map_err(ErrorBadRequest)?;
.map(|rss| {
let rss = builder.build().map_err(ErrorBadRequest)?.to_string();
Ok(
HttpResponse::Ok() HttpResponse::Ok()
.content_type("application/rss+xml") .content_type("application/rss+xml")
.body(rss) .body(rss),
}) )
.map_err(ErrorBadRequest)?;
Ok(res)
} }
fn get_sort_type(info: web::Query<Params>) -> Result<SortType, ParseError> { fn get_sort_type(info: web::Query<Params>) -> Result<SortType, ParseError> {
@ -130,7 +128,7 @@ fn get_feed_user(
conn: &PgConnection, conn: &PgConnection,
sort_type: &SortType, sort_type: &SortType,
user_name: String, user_name: String,
) -> Result<ChannelBuilder, failure::Error> { ) -> Result<ChannelBuilder, LemmyError> {
let site_view = SiteView::read(&conn)?; let site_view = SiteView::read(&conn)?;
let user = User_::find_by_username(&conn, &user_name)?; let user = User_::find_by_username(&conn, &user_name)?;
let user_url = user.get_profile_url(); let user_url = user.get_profile_url();
@ -156,7 +154,7 @@ fn get_feed_community(
conn: &PgConnection, conn: &PgConnection,
sort_type: &SortType, sort_type: &SortType,
community_name: String, community_name: String,
) -> Result<ChannelBuilder, failure::Error> { ) -> Result<ChannelBuilder, LemmyError> {
let site_view = SiteView::read(&conn)?; let site_view = SiteView::read(&conn)?;
let community = Community::read_from_name(&conn, &community_name)?; let community = Community::read_from_name(&conn, &community_name)?;
@ -185,7 +183,7 @@ fn get_feed_front(
conn: &PgConnection, conn: &PgConnection,
sort_type: &SortType, sort_type: &SortType,
jwt: String, jwt: String,
) -> Result<ChannelBuilder, failure::Error> { ) -> Result<ChannelBuilder, LemmyError> {
let site_view = SiteView::read(&conn)?; let site_view = SiteView::read(&conn)?;
let user_id = Claims::decode(&jwt)?.claims.id; let user_id = Claims::decode(&jwt)?.claims.id;
@ -210,7 +208,7 @@ fn get_feed_front(
Ok(channel_builder) Ok(channel_builder)
} }
fn get_feed_inbox(conn: &PgConnection, jwt: String) -> Result<ChannelBuilder, failure::Error> { fn get_feed_inbox(conn: &PgConnection, jwt: String) -> Result<ChannelBuilder, LemmyError> {
let site_view = SiteView::read(&conn)?; let site_view = SiteView::read(&conn)?;
let user_id = Claims::decode(&jwt)?.claims.id; let user_id = Claims::decode(&jwt)?.claims.id;

View file

@ -1,8 +1,10 @@
use crate::{ use crate::{
apub::get_apub_protocol_string, apub::get_apub_protocol_string,
blocking,
db::site_view::SiteView, db::site_view::SiteView,
routes::DbPoolParam, routes::DbPoolParam,
version, version,
LemmyError,
Settings, Settings,
}; };
use actix_web::{body::Body, error::ErrorBadRequest, *}; use actix_web::{body::Body, error::ErrorBadRequest, *};
@ -15,7 +17,7 @@ pub fn config(cfg: &mut web::ServiceConfig) {
.route("/.well-known/nodeinfo", web::get().to(node_info_well_known)); .route("/.well-known/nodeinfo", web::get().to(node_info_well_known));
} }
async fn node_info_well_known() -> Result<HttpResponse<Body>, failure::Error> { async fn node_info_well_known() -> Result<HttpResponse<Body>, LemmyError> {
let node_info = NodeInfoWellKnown { let node_info = NodeInfoWellKnown {
links: NodeInfoWellKnownLinks { links: NodeInfoWellKnownLinks {
rel: Url::parse("http://nodeinfo.diaspora.software/ns/schema/2.0")?, rel: Url::parse("http://nodeinfo.diaspora.software/ns/schema/2.0")?,
@ -30,18 +32,17 @@ async fn node_info_well_known() -> Result<HttpResponse<Body>, failure::Error> {
} }
async fn node_info(db: DbPoolParam) -> Result<HttpResponse, Error> { async fn node_info(db: DbPoolParam) -> Result<HttpResponse, Error> {
let res = web::block(move || { let site_view = blocking(&db, SiteView::read)
let conn = db.get()?; .await?
let site_view = match SiteView::read(&conn) { .map_err(|_| ErrorBadRequest(LemmyError::from(format_err!("not_found"))))?;
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()] vec!["activitypub".to_string()]
} else { } else {
vec![] vec![]
}; };
Ok(NodeInfo {
let json = NodeInfo {
version: "2.0".to_string(), version: "2.0".to_string(),
software: NodeInfoSoftware { software: NodeInfoSoftware {
name: "lemmy".to_string(), name: "lemmy".to_string(),
@ -56,12 +57,9 @@ async fn node_info(db: DbPoolParam) -> Result<HttpResponse, Error> {
local_comments: site_view.number_of_comments, local_comments: site_view.number_of_comments,
open_registrations: site_view.open_registration, open_registrations: site_view.open_registration,
}, },
}) };
})
.await Ok(HttpResponse::Ok().json(json))
.map(|json| HttpResponse::Ok().json(json))
.map_err(ErrorBadRequest)?;
Ok(res)
} }
#[derive(Serialize, Deserialize, Debug)] #[derive(Serialize, Deserialize, Debug)]

View file

@ -1,6 +1,8 @@
use crate::{ use crate::{
blocking,
db::{community::Community, user::User_}, db::{community::Community, user::User_},
routes::DbPoolParam, routes::DbPoolParam,
LemmyError,
Settings, Settings,
}; };
use actix_web::{error::ErrorBadRequest, web::Query, *}; use actix_web::{error::ErrorBadRequest, web::Query, *};
@ -61,9 +63,6 @@ async fn get_webfinger_response(
info: Query<Params>, info: Query<Params>,
db: DbPoolParam, db: DbPoolParam,
) -> Result<HttpResponse, Error> { ) -> Result<HttpResponse, Error> {
let res = web::block(move || {
let conn = db.get()?;
let community_regex_parsed = WEBFINGER_COMMUNITY_REGEX let community_regex_parsed = WEBFINGER_COMMUNITY_REGEX
.captures(&info.resource) .captures(&info.resource)
.map(|c| c.get(1)) .map(|c| c.get(1))
@ -75,24 +74,26 @@ async fn get_webfinger_response(
.flatten(); .flatten();
let url = if let Some(community_name) = community_regex_parsed { let url = if let Some(community_name) = community_regex_parsed {
let community_name = community_name.as_str().to_owned();
// Make sure the requested community exists. // Make sure the requested community exists.
let community = match Community::read_from_name(&conn, &community_name.as_str()) { blocking(&db, move |conn| {
Ok(o) => o, Community::read_from_name(conn, &community_name)
Err(_) => return Err(format_err!("not_found")), })
}; .await?
community.actor_id .map_err(|_| ErrorBadRequest(LemmyError::from(format_err!("not_found"))))?
.actor_id
} else if let Some(user_name) = user_regex_parsed { } else if let Some(user_name) = user_regex_parsed {
let user_name = user_name.as_str().to_owned();
// Make sure the requested user exists. // Make sure the requested user exists.
let user = match User_::read_from_name(&conn, &user_name.as_str()) { blocking(&db, move |conn| User_::read_from_name(conn, &user_name))
Ok(o) => o, .await?
Err(_) => return Err(format_err!("not_found")), .map_err(|_| ErrorBadRequest(LemmyError::from(format_err!("not_found"))))?
}; .actor_id
user.actor_id
} else { } else {
return Err(format_err!("not_found")); return Err(ErrorBadRequest(LemmyError::from(format_err!("not_found"))));
}; };
let wf_res = WebFingerResponse { let json = WebFingerResponse {
subject: info.resource.to_owned(), subject: info.resource.to_owned(),
aliases: vec![url.to_owned()], aliases: vec![url.to_owned()],
links: vec![ links: vec![
@ -115,10 +116,5 @@ async fn get_webfinger_response(
], ],
}; };
Ok(wf_res) Ok(HttpResponse::Ok().json(json))
})
.await
.map(|json| HttpResponse::Ok().json(json))
.map_err(ErrorBadRequest)?;
Ok(res)
} }

View file

@ -1,5 +1,5 @@
use crate::LemmyError;
use config::{Config, ConfigError, Environment, File}; use config::{Config, ConfigError, Environment, File};
use failure::Error;
use serde::Deserialize; use serde::Deserialize;
use std::{env, fs, net::IpAddr, sync::RwLock}; use std::{env, fs, net::IpAddr, sync::RwLock};
@ -118,11 +118,11 @@ impl Settings {
format!("{}/api/v1", self.hostname) format!("{}/api/v1", self.hostname)
} }
pub fn read_config_file() -> Result<String, Error> { pub fn read_config_file() -> Result<String, LemmyError> {
Ok(fs::read_to_string(CONFIG_FILE)?) Ok(fs::read_to_string(CONFIG_FILE)?)
} }
pub fn save_config_file(data: &str) -> Result<String, Error> { pub fn save_config_file(data: &str) -> Result<String, LemmyError> {
fs::write(CONFIG_FILE, data)?; fs::write(CONFIG_FILE, data)?;
// Reload the new settings // Reload the new settings

View file

@ -1 +1 @@
pub const VERSION: &str = "v0.7.5"; pub const VERSION: &str = "v0.7.8";

View file

@ -6,7 +6,6 @@ use diesel::{
r2d2::{ConnectionManager, Pool}, r2d2::{ConnectionManager, Pool},
PgConnection, PgConnection,
}; };
use failure::Error;
use log::{error, info}; use log::{error, info};
use rand::{rngs::ThreadRng, Rng}; use rand::{rngs::ThreadRng, Rng};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};

View file

@ -9,10 +9,13 @@ use crate::{
websocket::UserOperation, websocket::UserOperation,
CommunityId, CommunityId,
ConnectionId, ConnectionId,
DbPool,
IPAddr, IPAddr,
LemmyError,
PostId, PostId,
UserId, UserId,
}; };
use actix_web::client::Client;
/// Chat server sends this messages to session /// Chat server sends this messages to session
#[derive(Message)] #[derive(Message)]
@ -154,12 +157,16 @@ pub struct ChatServer {
/// Rate limiting based on rate type and IP addr /// Rate limiting based on rate type and IP addr
rate_limiter: RateLimit, rate_limiter: RateLimit,
/// An HTTP Client
client: Client,
} }
impl ChatServer { impl ChatServer {
pub fn startup( pub fn startup(
pool: Pool<ConnectionManager<PgConnection>>, pool: Pool<ConnectionManager<PgConnection>>,
rate_limiter: RateLimit, rate_limiter: RateLimit,
client: Client,
) -> ChatServer { ) -> ChatServer {
ChatServer { ChatServer {
sessions: HashMap::new(), sessions: HashMap::new(),
@ -169,6 +176,7 @@ impl ChatServer {
rng: rand::thread_rng(), rng: rand::thread_rng(),
pool, pool,
rate_limiter, rate_limiter,
client,
} }
} }
@ -236,7 +244,7 @@ impl ChatServer {
response: &Response, response: &Response,
post_id: PostId, post_id: PostId,
my_id: Option<ConnectionId>, my_id: Option<ConnectionId>,
) -> Result<(), Error> ) -> Result<(), LemmyError>
where where
Response: Serialize, Response: Serialize,
{ {
@ -260,7 +268,7 @@ impl ChatServer {
response: &Response, response: &Response,
community_id: CommunityId, community_id: CommunityId,
my_id: Option<ConnectionId>, my_id: Option<ConnectionId>,
) -> Result<(), Error> ) -> Result<(), LemmyError>
where where
Response: Serialize, Response: Serialize,
{ {
@ -283,7 +291,7 @@ impl ChatServer {
op: &UserOperation, op: &UserOperation,
response: &Response, response: &Response,
my_id: Option<ConnectionId>, my_id: Option<ConnectionId>,
) -> Result<(), Error> ) -> Result<(), LemmyError>
where where
Response: Serialize, Response: Serialize,
{ {
@ -305,7 +313,7 @@ impl ChatServer {
response: &Response, response: &Response,
recipient_id: UserId, recipient_id: UserId,
my_id: Option<ConnectionId>, my_id: Option<ConnectionId>,
) -> Result<(), Error> ) -> Result<(), LemmyError>
where where
Response: Serialize, Response: Serialize,
{ {
@ -328,7 +336,7 @@ impl ChatServer {
user_operation: &UserOperation, user_operation: &UserOperation,
comment: &CommentResponse, comment: &CommentResponse,
my_id: Option<ConnectionId>, my_id: Option<ConnectionId>,
) -> Result<(), Error> { ) -> Result<(), LemmyError> {
let mut comment_reply_sent = comment.clone(); let mut comment_reply_sent = comment.clone();
comment_reply_sent.comment.my_vote = None; comment_reply_sent.comment.my_vote = None;
comment_reply_sent.comment.user_id = None; comment_reply_sent.comment.user_id = None;
@ -366,7 +374,7 @@ impl ChatServer {
user_operation: &UserOperation, user_operation: &UserOperation,
post: &PostResponse, post: &PostResponse,
my_id: Option<ConnectionId>, my_id: Option<ConnectionId>,
) -> Result<(), Error> { ) -> Result<(), LemmyError> {
let community_id = post.post.community_id; let community_id = post.post.community_id;
// Don't send my data with it // Don't send my data with it
@ -394,7 +402,7 @@ impl ChatServer {
&mut self, &mut self,
msg: StandardMessage, msg: StandardMessage,
ctx: &mut Context<Self>, ctx: &mut Context<Self>,
) -> impl Future<Output = Result<String, Error>> { ) -> impl Future<Output = Result<String, LemmyError>> {
let addr = ctx.address(); let addr = ctx.address();
let pool = self.pool.clone(); let pool = self.pool.clone();
let rate_limiter = self.rate_limiter.clone(); let rate_limiter = self.rate_limiter.clone();
@ -404,6 +412,7 @@ impl ChatServer {
None => "blank_ip".to_string(), None => "blank_ip".to_string(),
}; };
let client = self.client.clone();
async move { async move {
let msg = msg; let msg = msg;
let json: Value = serde_json::from_str(&msg.msg)?; let json: Value = serde_json::from_str(&msg.msg)?;
@ -414,482 +423,109 @@ impl ChatServer {
let user_operation: UserOperation = UserOperation::from_str(&op)?; let user_operation: UserOperation = UserOperation::from_str(&op)?;
let args = Args {
client,
pool,
rate_limiter,
chatserver: addr,
id: msg.id,
ip,
op: user_operation.clone(),
data,
};
match user_operation { match user_operation {
// User ops // User ops
UserOperation::Login => { UserOperation::Login => do_user_operation::<Login>(args).await,
do_user_operation::<Login>(pool, rate_limiter, addr, msg.id, ip, user_operation, data) UserOperation::Register => do_user_operation::<Register>(args).await,
.await UserOperation::GetUserDetails => do_user_operation::<GetUserDetails>(args).await,
} UserOperation::GetReplies => do_user_operation::<GetReplies>(args).await,
UserOperation::Register => { UserOperation::AddAdmin => do_user_operation::<AddAdmin>(args).await,
do_user_operation::<Register>(pool, rate_limiter, addr, msg.id, ip, user_operation, data) UserOperation::BanUser => do_user_operation::<BanUser>(args).await,
.await UserOperation::GetUserMentions => do_user_operation::<GetUserMentions>(args).await,
} UserOperation::EditUserMention => do_user_operation::<EditUserMention>(args).await,
UserOperation::GetUserDetails => { UserOperation::MarkAllAsRead => do_user_operation::<MarkAllAsRead>(args).await,
do_user_operation::<GetUserDetails>( UserOperation::DeleteAccount => do_user_operation::<DeleteAccount>(args).await,
pool, UserOperation::PasswordReset => do_user_operation::<PasswordReset>(args).await,
rate_limiter, UserOperation::PasswordChange => do_user_operation::<PasswordChange>(args).await,
addr,
msg.id,
ip,
user_operation,
data,
)
.await
}
UserOperation::GetReplies => {
do_user_operation::<GetReplies>(
pool,
rate_limiter,
addr,
msg.id,
ip,
user_operation,
data,
)
.await
}
UserOperation::AddAdmin => {
do_user_operation::<AddAdmin>(pool, rate_limiter, addr, msg.id, ip, user_operation, data)
.await
}
UserOperation::BanUser => {
do_user_operation::<BanUser>(pool, rate_limiter, addr, msg.id, ip, user_operation, data)
.await
}
UserOperation::GetUserMentions => {
do_user_operation::<GetUserMentions>(
pool,
rate_limiter,
addr,
msg.id,
ip,
user_operation,
data,
)
.await
}
UserOperation::EditUserMention => {
do_user_operation::<EditUserMention>(
pool,
rate_limiter,
addr,
msg.id,
ip,
user_operation,
data,
)
.await
}
UserOperation::MarkAllAsRead => {
do_user_operation::<MarkAllAsRead>(
pool,
rate_limiter,
addr,
msg.id,
ip,
user_operation,
data,
)
.await
}
UserOperation::DeleteAccount => {
do_user_operation::<DeleteAccount>(
pool,
rate_limiter,
addr,
msg.id,
ip,
user_operation,
data,
)
.await
}
UserOperation::PasswordReset => {
do_user_operation::<PasswordReset>(
pool,
rate_limiter,
addr,
msg.id,
ip,
user_operation,
data,
)
.await
}
UserOperation::PasswordChange => {
do_user_operation::<PasswordChange>(
pool,
rate_limiter,
addr,
msg.id,
ip,
user_operation,
data,
)
.await
}
UserOperation::CreatePrivateMessage => { UserOperation::CreatePrivateMessage => {
do_user_operation::<CreatePrivateMessage>( do_user_operation::<CreatePrivateMessage>(args).await
pool,
rate_limiter,
addr,
msg.id,
ip,
user_operation,
data,
)
.await
}
UserOperation::EditPrivateMessage => {
do_user_operation::<EditPrivateMessage>(
pool,
rate_limiter,
addr,
msg.id,
ip,
user_operation,
data,
)
.await
}
UserOperation::GetPrivateMessages => {
do_user_operation::<GetPrivateMessages>(
pool,
rate_limiter,
addr,
msg.id,
ip,
user_operation,
data,
)
.await
}
UserOperation::UserJoin => {
do_user_operation::<UserJoin>(pool, rate_limiter, addr, msg.id, ip, user_operation, data)
.await
}
UserOperation::SaveUserSettings => {
do_user_operation::<SaveUserSettings>(
pool,
rate_limiter,
addr,
msg.id,
ip,
user_operation,
data,
)
.await
} }
UserOperation::EditPrivateMessage => do_user_operation::<EditPrivateMessage>(args).await,
UserOperation::GetPrivateMessages => do_user_operation::<GetPrivateMessages>(args).await,
UserOperation::UserJoin => do_user_operation::<UserJoin>(args).await,
UserOperation::SaveUserSettings => do_user_operation::<SaveUserSettings>(args).await,
// Site ops // Site ops
UserOperation::GetModlog => { UserOperation::GetModlog => do_user_operation::<GetModlog>(args).await,
do_user_operation::<GetModlog>(pool, rate_limiter, addr, msg.id, ip, user_operation, data) UserOperation::CreateSite => do_user_operation::<CreateSite>(args).await,
.await UserOperation::EditSite => do_user_operation::<EditSite>(args).await,
} UserOperation::GetSite => do_user_operation::<GetSite>(args).await,
UserOperation::CreateSite => { UserOperation::GetSiteConfig => do_user_operation::<GetSiteConfig>(args).await,
do_user_operation::<CreateSite>( UserOperation::SaveSiteConfig => do_user_operation::<SaveSiteConfig>(args).await,
pool, UserOperation::Search => do_user_operation::<Search>(args).await,
rate_limiter, UserOperation::TransferCommunity => do_user_operation::<TransferCommunity>(args).await,
addr, UserOperation::TransferSite => do_user_operation::<TransferSite>(args).await,
msg.id, UserOperation::ListCategories => do_user_operation::<ListCategories>(args).await,
ip,
user_operation,
data,
)
.await
}
UserOperation::EditSite => {
do_user_operation::<EditSite>(pool, rate_limiter, addr, msg.id, ip, user_operation, data)
.await
}
UserOperation::GetSite => {
do_user_operation::<GetSite>(pool, rate_limiter, addr, msg.id, ip, user_operation, data)
.await
}
UserOperation::GetSiteConfig => {
do_user_operation::<GetSiteConfig>(
pool,
rate_limiter,
addr,
msg.id,
ip,
user_operation,
data,
)
.await
}
UserOperation::SaveSiteConfig => {
do_user_operation::<SaveSiteConfig>(
pool,
rate_limiter,
addr,
msg.id,
ip,
user_operation,
data,
)
.await
}
UserOperation::Search => {
do_user_operation::<Search>(pool, rate_limiter, addr, msg.id, ip, user_operation, data)
.await
}
UserOperation::TransferCommunity => {
do_user_operation::<TransferCommunity>(
pool,
rate_limiter,
addr,
msg.id,
ip,
user_operation,
data,
)
.await
}
UserOperation::TransferSite => {
do_user_operation::<TransferSite>(
pool,
rate_limiter,
addr,
msg.id,
ip,
user_operation,
data,
)
.await
}
UserOperation::ListCategories => {
do_user_operation::<ListCategories>(
pool,
rate_limiter,
addr,
msg.id,
ip,
user_operation,
data,
)
.await
}
// Community ops // Community ops
UserOperation::GetCommunity => { UserOperation::GetCommunity => do_user_operation::<GetCommunity>(args).await,
do_user_operation::<GetCommunity>( UserOperation::ListCommunities => do_user_operation::<ListCommunities>(args).await,
pool, UserOperation::CreateCommunity => do_user_operation::<CreateCommunity>(args).await,
rate_limiter, UserOperation::EditCommunity => do_user_operation::<EditCommunity>(args).await,
addr, UserOperation::FollowCommunity => do_user_operation::<FollowCommunity>(args).await,
msg.id,
ip,
user_operation,
data,
)
.await
}
UserOperation::ListCommunities => {
do_user_operation::<ListCommunities>(
pool,
rate_limiter,
addr,
msg.id,
ip,
user_operation,
data,
)
.await
}
UserOperation::CreateCommunity => {
do_user_operation::<CreateCommunity>(
pool,
rate_limiter,
addr,
msg.id,
ip,
user_operation,
data,
)
.await
}
UserOperation::EditCommunity => {
do_user_operation::<EditCommunity>(
pool,
rate_limiter,
addr,
msg.id,
ip,
user_operation,
data,
)
.await
}
UserOperation::FollowCommunity => {
do_user_operation::<FollowCommunity>(
pool,
rate_limiter,
addr,
msg.id,
ip,
user_operation,
data,
)
.await
}
UserOperation::GetFollowedCommunities => { UserOperation::GetFollowedCommunities => {
do_user_operation::<GetFollowedCommunities>( do_user_operation::<GetFollowedCommunities>(args).await
pool,
rate_limiter,
addr,
msg.id,
ip,
user_operation,
data,
)
.await
}
UserOperation::BanFromCommunity => {
do_user_operation::<BanFromCommunity>(
pool,
rate_limiter,
addr,
msg.id,
ip,
user_operation,
data,
)
.await
}
UserOperation::AddModToCommunity => {
do_user_operation::<AddModToCommunity>(
pool,
rate_limiter,
addr,
msg.id,
ip,
user_operation,
data,
)
.await
} }
UserOperation::BanFromCommunity => do_user_operation::<BanFromCommunity>(args).await,
UserOperation::AddModToCommunity => do_user_operation::<AddModToCommunity>(args).await,
// Post ops // Post ops
UserOperation::CreatePost => { UserOperation::CreatePost => do_user_operation::<CreatePost>(args).await,
do_user_operation::<CreatePost>( UserOperation::GetPost => do_user_operation::<GetPost>(args).await,
pool, UserOperation::GetPosts => do_user_operation::<GetPosts>(args).await,
rate_limiter, UserOperation::EditPost => do_user_operation::<EditPost>(args).await,
addr, UserOperation::CreatePostLike => do_user_operation::<CreatePostLike>(args).await,
msg.id, UserOperation::SavePost => do_user_operation::<SavePost>(args).await,
ip,
user_operation,
data,
)
.await
}
UserOperation::GetPost => {
do_user_operation::<GetPost>(pool, rate_limiter, addr, msg.id, ip, user_operation, data)
.await
}
UserOperation::GetPosts => {
do_user_operation::<GetPosts>(pool, rate_limiter, addr, msg.id, ip, user_operation, data)
.await
}
UserOperation::EditPost => {
do_user_operation::<EditPost>(pool, rate_limiter, addr, msg.id, ip, user_operation, data)
.await
}
UserOperation::CreatePostLike => {
do_user_operation::<CreatePostLike>(
pool,
rate_limiter,
addr,
msg.id,
ip,
user_operation,
data,
)
.await
}
UserOperation::SavePost => {
do_user_operation::<SavePost>(pool, rate_limiter, addr, msg.id, ip, user_operation, data)
.await
}
// Comment ops // Comment ops
UserOperation::CreateComment => { UserOperation::CreateComment => do_user_operation::<CreateComment>(args).await,
do_user_operation::<CreateComment>( UserOperation::EditComment => do_user_operation::<EditComment>(args).await,
pool, UserOperation::SaveComment => do_user_operation::<SaveComment>(args).await,
rate_limiter, UserOperation::GetComments => do_user_operation::<GetComments>(args).await,
addr, UserOperation::CreateCommentLike => do_user_operation::<CreateCommentLike>(args).await,
msg.id,
ip,
user_operation,
data,
)
.await
}
UserOperation::EditComment => {
do_user_operation::<EditComment>(
pool,
rate_limiter,
addr,
msg.id,
ip,
user_operation,
data,
)
.await
}
UserOperation::SaveComment => {
do_user_operation::<SaveComment>(
pool,
rate_limiter,
addr,
msg.id,
ip,
user_operation,
data,
)
.await
}
UserOperation::GetComments => {
do_user_operation::<GetComments>(
pool,
rate_limiter,
addr,
msg.id,
ip,
user_operation,
data,
)
.await
}
UserOperation::CreateCommentLike => {
do_user_operation::<CreateCommentLike>(
pool,
rate_limiter,
addr,
msg.id,
ip,
user_operation,
data,
)
.await
}
} }
} }
} }
} }
async fn do_user_operation<'a, Data>( struct Args<'a> {
pool: Pool<ConnectionManager<PgConnection>>, client: Client,
pool: DbPool,
rate_limiter: RateLimit, rate_limiter: RateLimit,
chatserver: Addr<ChatServer>, chatserver: Addr<ChatServer>,
id: ConnectionId, id: ConnectionId,
ip: IPAddr, ip: IPAddr,
op: UserOperation, op: UserOperation,
data: &str, data: &'a str,
) -> Result<String, Error> }
async fn do_user_operation<'a, 'b, Data>(args: Args<'b>) -> Result<String, LemmyError>
where where
for<'de> Data: Deserialize<'de> + 'a, for<'de> Data: Deserialize<'de> + 'a,
Oper<Data>: Perform, Oper<Data>: Perform,
{ {
let Args {
client,
pool,
rate_limiter,
chatserver,
id,
ip,
op,
data,
} = args;
let ws_info = WebsocketInfo { let ws_info = WebsocketInfo {
chatserver, chatserver,
id: Some(id), id: Some(id),
@ -898,17 +534,14 @@ where
let data = data.to_string(); let data = data.to_string();
let op2 = op.clone(); let op2 = op.clone();
let client = client.clone();
let fut = async move { let fut = async move {
actix_web::web::block(move || { let pool = pool.clone();
let parsed_data: Data = serde_json::from_str(&data)?; let parsed_data: Data = serde_json::from_str(&data)?;
let res = Oper::new(parsed_data).perform(pool, Some(ws_info))?; let res = Oper::new(parsed_data, client)
.perform(&pool, Some(ws_info))
.await?;
to_json_string(&op, &res) to_json_string(&op, &res)
})
.await
.map_err(|e| match e {
actix_web::error::BlockingError::Error(e) => e,
_ => APIError::err("Operation canceled").into(),
})
}; };
match op2 { match op2 {
@ -1109,7 +742,7 @@ struct WebsocketResponse<T> {
data: T, data: T,
} }
fn to_json_string<Response>(op: &UserOperation, data: &Response) -> Result<String, Error> fn to_json_string<Response>(op: &UserOperation, data: &Response) -> Result<String, LemmyError>
where where
Response: Serialize, Response: Serialize,
{ {

View file

@ -124,10 +124,10 @@ describe('main', () => {
}); });
describe('follow_accept', () => { describe('follow_accept', () => {
test('/u/lemmy_alpha follows and accepts lemmy_beta/c/main', async () => { test('/u/lemmy_alpha follows and accepts lemmy-beta/c/main', async () => {
// Make sure lemmy_beta/c/main is cached on lemmy_alpha // Make sure lemmy-beta/c/main is cached on lemmy_alpha
// Use short-hand search url // Use short-hand search url
let searchUrl = `${lemmyAlphaApiUrl}/search?q=!main@lemmy_beta:8550&type_=All&sort=TopAll`; let searchUrl = `${lemmyAlphaApiUrl}/search?q=!main@lemmy-beta:8550&type_=All&sort=TopAll`;
let searchResponse: SearchResponse = await fetch(searchUrl, { let searchResponse: SearchResponse = await fetch(searchUrl, {
method: 'GET', method: 'GET',
@ -215,7 +215,7 @@ describe('main', () => {
// Also make G follow B // Also make G follow B
// Use short-hand search url // Use short-hand search url
let searchUrlG = `${lemmyGammaApiUrl}/search?q=!main@lemmy_beta:8550&type_=All&sort=TopAll`; let searchUrlG = `${lemmyGammaApiUrl}/search?q=!main@lemmy-beta:8550&type_=All&sort=TopAll`;
let searchResponseG: SearchResponse = await fetch(searchUrlG, { let searchResponseG: SearchResponse = await fetch(searchUrlG, {
method: 'GET', method: 'GET',
@ -449,7 +449,7 @@ describe('main', () => {
// Lemmy alpha responds to their own comment, but mentions lemmy beta. // Lemmy alpha responds to their own comment, but mentions lemmy beta.
// Make sure lemmy beta gets that in their inbox. // Make sure lemmy beta gets that in their inbox.
let mentionContent = 'A test mention of @lemmy_beta@lemmy_beta:8550'; let mentionContent = 'A test mention of @lemmy_beta@lemmy-beta:8550';
let mentionCommentForm: CommentForm = { let mentionCommentForm: CommentForm = {
content: mentionContent, content: mentionContent,
post_id: 2, post_id: 2,
@ -550,7 +550,7 @@ describe('main', () => {
expect(createCommunityRes.community.name).toBe(communityName); expect(createCommunityRes.community.name).toBe(communityName);
// Cache it on lemmy_alpha // Cache it on lemmy_alpha
let searchUrl = `${lemmyAlphaApiUrl}/search?q=http://lemmy_beta:8550/c/${communityName}&type_=All&sort=TopAll`; let searchUrl = `${lemmyAlphaApiUrl}/search?q=http://lemmy-beta:8550/c/${communityName}&type_=All&sort=TopAll`;
let searchResponse: SearchResponse = await fetch(searchUrl, { let searchResponse: SearchResponse = await fetch(searchUrl, {
method: 'GET', method: 'GET',
}).then(d => d.json()); }).then(d => d.json());
@ -826,7 +826,7 @@ describe('main', () => {
expect(createCommunityRes.community.name).toBe(communityName); expect(createCommunityRes.community.name).toBe(communityName);
// Cache it on lemmy_alpha // Cache it on lemmy_alpha
let searchUrl = `${lemmyAlphaApiUrl}/search?q=http://lemmy_beta:8550/c/${communityName}&type_=All&sort=TopAll`; let searchUrl = `${lemmyAlphaApiUrl}/search?q=http://lemmy-beta:8550/c/${communityName}&type_=All&sort=TopAll`;
let searchResponse: SearchResponse = await fetch(searchUrl, { let searchResponse: SearchResponse = await fetch(searchUrl, {
method: 'GET', method: 'GET',
}).then(d => d.json()); }).then(d => d.json());
@ -1278,7 +1278,7 @@ describe('main', () => {
// Create a test comment on Gamma, make sure it gets announced to alpha // Create a test comment on Gamma, make sure it gets announced to alpha
let commentContent = let commentContent =
'A jest test federated comment announce, lets mention @lemmy_beta@lemmy_beta:8550'; 'A jest test federated comment announce, lets mention @lemmy_beta@lemmy-beta:8550';
let commentForm: CommentForm = { let commentForm: CommentForm = {
content: commentContent, content: commentContent,
@ -1417,7 +1417,7 @@ describe('main', () => {
expect(createChildCommentRes.comment.content).toBe(childCommentContent); expect(createChildCommentRes.comment.content).toBe(childCommentContent);
// Follow again, for other tests // Follow again, for other tests
let searchUrl = `${lemmyAlphaApiUrl}/search?q=!main@lemmy_beta:8550&type_=All&sort=TopAll`; let searchUrl = `${lemmyAlphaApiUrl}/search?q=!main@lemmy-beta:8550&type_=All&sort=TopAll`;
let searchResponse: SearchResponse = await fetch(searchUrl, { let searchResponse: SearchResponse = await fetch(searchUrl, {
method: 'GET', method: 'GET',

View file

@ -263,7 +263,9 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
// If its a comment edit, only check that its from your user, and that its a // If its a comment edit, only check that its from your user, and that its a
// text edit only // text edit only
(op == UserOperation.EditComment && data.comment.content) (data.comment.creator_id == UserService.Instance.user.id &&
op == UserOperation.EditComment &&
data.comment.content)
) { ) {
this.state.previewMode = false; this.state.previewMode = false;
this.state.loading = false; this.state.loading = false;

View file

@ -20,6 +20,11 @@ interface State {
loginLoading: boolean; loginLoading: boolean;
registerLoading: boolean; registerLoading: boolean;
enable_nsfw: boolean; enable_nsfw: boolean;
mathQuestion: {
a: number;
b: number;
answer: number;
};
} }
export class Login extends Component<any, State> { export class Login extends Component<any, State> {
@ -40,6 +45,11 @@ export class Login extends Component<any, State> {
loginLoading: false, loginLoading: false,
registerLoading: false, registerLoading: false,
enable_nsfw: undefined, enable_nsfw: undefined,
mathQuestion: {
a: Math.floor(Math.random() * 10) + 1,
b: Math.floor(Math.random() * 10) + 1,
answer: undefined,
},
}; };
constructor(props: any, context: any) { constructor(props: any, context: any) {
@ -215,6 +225,23 @@ export class Login extends Component<any, State> {
/> />
</div> </div>
</div> </div>
<div class="form-group row">
<label class="col-sm-10 col-form-label" htmlFor="register-math">
{i18n.t('what_is')}{' '}
{`${this.state.mathQuestion.a} + ${this.state.mathQuestion.b}?`}
</label>
<div class="col-sm-2">
<input
type="number"
id="register-math"
class="form-control"
value={this.state.mathQuestion.answer}
onInput={linkEvent(this, this.handleMathAnswerChange)}
required
/>
</div>
</div>
{this.state.enable_nsfw && ( {this.state.enable_nsfw && (
<div class="form-group row"> <div class="form-group row">
<div class="col-sm-10"> <div class="col-sm-10">
@ -235,7 +262,11 @@ export class Login extends Component<any, State> {
)} )}
<div class="form-group row"> <div class="form-group row">
<div class="col-sm-10"> <div class="col-sm-10">
<button type="submit" class="btn btn-secondary"> <button
type="submit"
class="btn btn-secondary"
disabled={this.mathCheck}
>
{this.state.registerLoading ? ( {this.state.registerLoading ? (
<svg class="icon icon-spinner spin"> <svg class="icon icon-spinner spin">
<use xlinkHref="#icon-spinner"></use> <use xlinkHref="#icon-spinner"></use>
@ -272,8 +303,10 @@ export class Login extends Component<any, State> {
i.state.registerLoading = true; i.state.registerLoading = true;
i.setState(i.state); i.setState(i.state);
if (!i.mathCheck) {
WebSocketService.Instance.register(i.state.registerForm); WebSocketService.Instance.register(i.state.registerForm);
} }
}
handleRegisterUsernameChange(i: Login, event: any) { handleRegisterUsernameChange(i: Login, event: any) {
i.state.registerForm.username = event.target.value; i.state.registerForm.username = event.target.value;
@ -303,6 +336,11 @@ export class Login extends Component<any, State> {
i.setState(i.state); i.setState(i.state);
} }
handleMathAnswerChange(i: Login, event: any) {
i.state.mathQuestion.answer = event.target.value;
i.setState(i.state);
}
handlePasswordReset(i: Login) { handlePasswordReset(i: Login) {
event.preventDefault(); event.preventDefault();
let resetForm: PasswordResetForm = { let resetForm: PasswordResetForm = {
@ -311,6 +349,13 @@ export class Login extends Component<any, State> {
WebSocketService.Instance.passwordReset(resetForm); WebSocketService.Instance.passwordReset(resetForm);
} }
get mathCheck(): boolean {
return (
this.state.mathQuestion.answer !=
this.state.mathQuestion.a + this.state.mathQuestion.b
);
}
parseMessage(msg: WebSocketJsonResponse) { parseMessage(msg: WebSocketJsonResponse) {
let res = wsJsonToRes(msg); let res = wsJsonToRes(msg);
if (msg.error) { if (msg.error) {

View file

@ -6,10 +6,12 @@ import { repoUrl } from '../utils';
interface SilverUser { interface SilverUser {
name: string; name: string;
link: string; link?: string;
} }
let general = [ let general = [
'dude in phx',
'twilight loki',
'Andrew Plaza', 'Andrew Plaza',
'Jonathan Cremin', 'Jonathan Cremin',
'Arthur Nieuwland', 'Arthur Nieuwland',
@ -19,7 +21,7 @@ let general = [
'Andre Vallestero', 'Andre Vallestero',
'NotTooHighToHack', 'NotTooHighToHack',
]; ];
let highlighted = ['Oskenso Kashi', 'Alex Benishek']; let highlighted = ['DiscountFuneral', 'Oskenso Kashi', 'Alex Benishek'];
let silver: Array<SilverUser> = [ let silver: Array<SilverUser> = [
{ {
name: 'Redjoker', name: 'Redjoker',
@ -89,9 +91,13 @@ export class Sponsors extends Component<any, any> {
{silver.map(s => ( {silver.map(s => (
<div class="card col-12 col-md-2"> <div class="card col-12 col-md-2">
<div> <div>
{s.link ? (
<a href={s.link} target="_blank" rel="noopener"> <a href={s.link} target="_blank" rel="noopener">
💎 {s.name} 💎 {s.name}
</a> </a>
) : (
<div>💎 {s.name}</div>
)}
</div> </div>
</div> </div>
))} ))}

2
ui/src/i18next.ts vendored
View file

@ -24,6 +24,7 @@ import { gl } from './translations/gl';
import { tr } from './translations/tr'; import { tr } from './translations/tr';
import { hu } from './translations/hu'; import { hu } from './translations/hu';
import { uk } from './translations/uk'; import { uk } from './translations/uk';
import { sq } from './translations/sq';
// https://github.com/nimbusec-oss/inferno-i18next/blob/master/tests/T.test.js#L66 // https://github.com/nimbusec-oss/inferno-i18next/blob/master/tests/T.test.js#L66
const resources = { const resources = {
@ -51,6 +52,7 @@ const resources = {
tr, tr,
hu, hu,
uk, uk,
sq,
}; };
function format(value: any, format: any, lng: any): any { function format(value: any, format: any, lng: any): any {

4
ui/src/utils.ts vendored
View file

@ -21,6 +21,7 @@ import 'moment/locale/gl';
import 'moment/locale/tr'; import 'moment/locale/tr';
import 'moment/locale/hu'; import 'moment/locale/hu';
import 'moment/locale/uk'; import 'moment/locale/uk';
import 'moment/locale/sq';
import { import {
UserOperation, UserOperation,
@ -83,6 +84,7 @@ export const languages = [
{ code: 'fi', name: 'Suomi' }, { code: 'fi', name: 'Suomi' },
{ code: 'fr', name: 'Français' }, { code: 'fr', name: 'Français' },
{ code: 'sv', name: 'Svenska' }, { code: 'sv', name: 'Svenska' },
{ code: 'sq', name: 'Shqip' },
{ code: 'tr', name: 'Türkçe' }, { code: 'tr', name: 'Türkçe' },
{ code: 'uk', name: 'українська мова' }, { code: 'uk', name: 'українська мова' },
{ code: 'ru', name: 'Русский' }, { code: 'ru', name: 'Русский' },
@ -414,6 +416,8 @@ export function getMomentLanguage(): string {
lang = 'hu'; lang = 'hu';
} else if (lang.startsWith('uk')) { } else if (lang.startsWith('uk')) {
lang = 'uk'; lang = 'uk';
} else if (lang.startsWith('sq')) {
lang = 'sq';
} else { } else {
lang = 'en'; lang = 'en';
} }

2
ui/src/version.ts vendored
View file

@ -1 +1 @@
export const version: string = 'v0.7.5'; export const version: string = 'v0.7.8';

View file

@ -50,14 +50,14 @@
"remove_as_admin": "Als Administrator entfernen", "remove_as_admin": "Als Administrator entfernen",
"appoint_as_admin": "Zum Administrator ernennen", "appoint_as_admin": "Zum Administrator ernennen",
"remove": "entfernen", "remove": "entfernen",
"removed": "entfernt", "removed": "entfernt durch die Moderation",
"locked": "gesperrt", "locked": "gesperrt",
"stickied": "angeheftet", "stickied": "angeheftet",
"reason": "Grund", "reason": "Grund",
"mark_as_read": "als gelesen markieren", "mark_as_read": "als gelesen markieren",
"mark_as_unread": "als ungelesen markieren", "mark_as_unread": "als ungelesen markieren",
"delete": "löschen", "delete": "löschen",
"deleted": "gelöscht", "deleted": "vom Ersteller gelöscht",
"delete_account": "Konto löschen", "delete_account": "Konto löschen",
"delete_account_confirm": "Achtung: Dadurch werden alle Ihre Daten dauerhaft gelöscht. Geben Sie zur Bestätigung Ihr Passwort ein.", "delete_account_confirm": "Achtung: Dadurch werden alle Ihre Daten dauerhaft gelöscht. Geben Sie zur Bestätigung Ihr Passwort ein.",
"restore": "wiederherstellen", "restore": "wiederherstellen",
@ -150,7 +150,7 @@
"theme": "Aussehen", "theme": "Aussehen",
"sponsors": "Sponsoren", "sponsors": "Sponsoren",
"sponsors_of_lemmy": "Sponsoren von Lemmy", "sponsors_of_lemmy": "Sponsoren von Lemmy",
"sponsor_message": "Lemmy ist freie <1>Open-Source</1> Software, also ohne Werbung, Monetarisierung oder Venturekapital, Punkt. Deine Spenden gehen direkt an die Vollzeit Entwicklung des Projekts. Vielen Dank an die folgenden Personen:", "sponsor_message": "Lemmy ist freie <1>Open-Source</1> Software, ohne Werbung, Monetarisierung oder Venturekapital, Punkt. Deine Spenden gehen direkt an die Vollzeit Entwicklung des Projekts. Vielen Dank an die folgenden Personen:",
"support_on_patreon": "Auf Patreon unterstützen", "support_on_patreon": "Auf Patreon unterstützen",
"support_on_liberapay": "Auf Liberapay unterstützen", "support_on_liberapay": "Auf Liberapay unterstützen",
"general_sponsors": "Allgemeine Sponsoren sind die, die zwischen $10 und $39 zu Lemmy beitragen.", "general_sponsors": "Allgemeine Sponsoren sind die, die zwischen $10 und $39 zu Lemmy beitragen.",
@ -251,5 +251,10 @@
"number_of_upvotes": "{{count}} Stimme", "number_of_upvotes": "{{count}} Stimme",
"number_of_upvotes_plural": "{{count}} Stimmen", "number_of_upvotes_plural": "{{count}} Stimmen",
"number_of_downvotes": "{{count}} Gegenstimme", "number_of_downvotes": "{{count}} Gegenstimme",
"number_of_downvotes_plural": "{{count}} Gegenstimmen" "number_of_downvotes_plural": "{{count}} Gegenstimmen",
"invalid_community_name": "Ungültiger Name.",
"click_to_delete_picture": "Klicke, um das Bild zu löschen.",
"picture_deleted": "Bild gelöscht.",
"select_a_community": "Wähle eine Community aus",
"invalid_username": "Ungültiger Benutzername."
} }

View file

@ -102,7 +102,7 @@
"category": "Κατηγορία", "category": "Κατηγορία",
"subscribers": "Εγγεγραμμένοι", "subscribers": "Εγγεγραμμένοι",
"both": "Και οι δύο", "both": "Και οι δύο",
"saved": "Αποθηκεύτηκε", "saved": "Αποθηκευμένα",
"prev": "Προηγούμενο", "prev": "Προηγούμενο",
"next": "Επόμενο", "next": "Επόμενο",
"sidebar": "Πλευρικό μενού", "sidebar": "Πλευρικό μενού",
@ -116,7 +116,7 @@
"mark_all_as_read": "επισήμανση όλων ως διαβασμένα", "mark_all_as_read": "επισήμανση όλων ως διαβασμένα",
"type": "Είδος", "type": "Είδος",
"unread": "Μη διαβασμένα", "unread": "Μη διαβασμένα",
"url": "Ενιαίος Εντοπιστής Πόρων (URL)", "url": "URL",
"subscribed": "Εγγεγραμμένος", "subscribed": "Εγγεγραμμένος",
"week": "Εβδομάδα", "week": "Εβδομάδα",
"month": "Μήνας", "month": "Μήνας",
@ -151,8 +151,8 @@
"reset_password_mail_sent": "Μόλις στάλθηκε ένα μήνυμα ηλεκτρονικού ταχυδρομείου για την επαναφορά του κωδικού σας.", "reset_password_mail_sent": "Μόλις στάλθηκε ένα μήνυμα ηλεκτρονικού ταχυδρομείου για την επαναφορά του κωδικού σας.",
"password_change": "Αλλαγή κωδικού", "password_change": "Αλλαγή κωδικού",
"new_password": "Νέος κωδικός", "new_password": "Νέος κωδικός",
"no_email_setup": "Αυτός ο διακομιστής δεν έχει εγκαταστήσει σωστά το ηλεκτρονικό ταχυδρομείο.", "no_email_setup": "Αυτός ο διακομιστής δεν έχει εγκαταστήσει σωστά το email.",
"email": "Ηλεκτρονικό ταχυδρομείο", "email": "Email",
"matrix_user_id": "Χρήστης Matrix", "matrix_user_id": "Χρήστης Matrix",
"private_message_disclaimer": "Προσοχή: τα προσωπικά μηνύματα στο Lemmy δεν είναι ασφαλή. Παρακαλούμε δημιουργήστε έναν λογαριασμό στο <1>Riot.im</1> για ασφαλή επικοινωνία.", "private_message_disclaimer": "Προσοχή: τα προσωπικά μηνύματα στο Lemmy δεν είναι ασφαλή. Παρακαλούμε δημιουργήστε έναν λογαριασμό στο <1>Riot.im</1> για ασφαλή επικοινωνία.",
"send_notifications_to_email": "Αποστολή ειδοποιήσεων στη διεύθυνση ηλεκτρονικού ταχυδρομείου", "send_notifications_to_email": "Αποστολή ειδοποιήσεων στη διεύθυνση ηλεκτρονικού ταχυδρομείου",
@ -200,7 +200,7 @@
"monero": "Monero", "monero": "Monero",
"code": "Κώδικας", "code": "Κώδικας",
"by": "από", "by": "από",
"to": "μέχρι", "to": "προς",
"from": "από", "from": "από",
"transfer_community": "μεταφορά κοινότητας", "transfer_community": "μεταφορά κοινότητας",
"transfer_site": "μεταφορά ιστότοπου", "transfer_site": "μεταφορά ιστότοπου",

View file

@ -264,5 +264,6 @@
"time": "Time", "time": "Time",
"action": "Action", "action": "Action",
"emoji_picker": "Emoji Picker", "emoji_picker": "Emoji Picker",
"block_leaving": "Are you sure you want to leave?" "block_leaving": "Are you sure you want to leave?",
"what_is": "What is"
} }

View file

@ -1,33 +1,34 @@
{ {
"post": "Poŝti", "post": "Afiŝi",
"remove_post": "Fortiri Poŝton", "remove_post": "Forigi afiŝon",
"no_posts": "Ne Poŝtoj.", "no_posts": "Neniuj afiŝoj.",
"create_a_post": "Verki Poŝton", "create_a_post": "Verki afiŝon",
"create_post": "Verki Poŝton", "create_post": "Verki afiŝon",
"number_of_posts": "{{count}} Poŝtoj", "number_of_posts": "{{count}} afiŝo",
"posts": "Poŝtoj", "number_of_posts_plural": "{{count}} afiŝoj",
"related_posts": "Tiuj poŝtoj eble rilatas", "posts": "Afiŝoj",
"cross_posts": "Tiuj ligilo ankaŭ estas poŝtinta al:", "related_posts": "Ĉi tiuj afiŝoj eble rilatas",
"cross_post": "laŭapoŝto", "cross_posts": "Tiu ligilo ankaŭ estas afiŝita al:",
"cross_post": "transafiŝo",
"comments": "Komentoj", "comments": "Komentoj",
"number_of_comments": "{{count}} Komento", "number_of_comments": "{{count}} komento",
"number_of_comments_plural": "{{count}} Komentoj", "number_of_comments_plural": "{{count}} komentoj",
"remove_comment": "Fortiri Komentojn", "remove_comment": "Forigi komenton",
"communities": "Komunumoj", "communities": "Komunumoj",
"users": "Uzantoj", "users": "Uzantoj",
"create_a_community": "Krei komunumon", "create_a_community": "Krei komunumon",
"create_community": "Krei Komunumon", "create_community": "Krei komunumon",
"remove_community": "Forigi Komunumon", "remove_community": "Forigi komunumon",
"subscribed_to_communities": "Abonita al <1>komunumoj</1>", "subscribed_to_communities": "Abonanta <1>komunumojn</1>",
"trending_communities": "Furora <1>komunumoj</1>", "trending_communities": "Furoraj <1>komunumoj</1>",
"list_of_communities": "Listo de komunumoj", "list_of_communities": "Listo de komunumoj",
"community_reqs": "minusklaj leteroj, substrekoj, kaj ne spacetoj.", "community_reqs": "minusklaj literoj, substrekoj, kaj neniuj spacetoj.",
"edit": "redakti", "edit": "redakti",
"reply": "repliki", "reply": "respondi",
"cancel": "nuligi", "cancel": "Nuligi",
"unlock": "malŝlosi", "unlock": "malŝlosi",
"lock": "ŝlosi", "lock": "ŝlosi",
"link": "ligi", "link": "ligilo",
"mod": "moderanto", "mod": "moderanto",
"mods": "moderantoj", "mods": "moderantoj",
"moderates": "Moderigas", "moderates": "Moderigas",
@ -37,17 +38,17 @@
"modlog": "Moderlogo", "modlog": "Moderlogo",
"admin": "administranto", "admin": "administranto",
"admins": "administrantoj", "admins": "administrantoj",
"remove_as_admin": "forigi per administranto", "remove_as_admin": "forigi kiel administranto",
"appoint_as_admin": "nomumi per administranto", "appoint_as_admin": "nomumi administranto",
"remove": "fortiri", "remove": "forigi",
"removed": "fortirita", "removed": "fortirita",
"locked": "ŝlosita", "locked": "ŝlosita",
"reason": "Kialo", "reason": "Kialo",
"mark_as_read": "marki kiel legita", "mark_as_read": "marki legita",
"mark_as_unread": "marki kiel nelegita", "mark_as_unread": "marki nelegita",
"delete": "forigi", "delete": "forigi",
"deleted": "forigita", "deleted": "forigita de la kreinto",
"restore": "restaŭri", "restore": "revenigi",
"ban": "forbari", "ban": "forbari",
"ban_from_site": "forbari de retejo", "ban_from_site": "forbari de retejo",
"unban": "malforbari", "unban": "malforbari",
@ -55,11 +56,14 @@
"save": "konservi", "save": "konservi",
"unsave": "malkonservi", "unsave": "malkonservi",
"create": "krei", "create": "krei",
"username": "Uzantnomo", "username": "Uzantonomo",
"email_or_username": "Retadreso aŭ Uzantnomo", "email_or_username": "Retpoŝtadreso aŭ uzantonomo",
"number_of_users": "{{count}} Uzantoj", "number_of_users": "{{count}} uzanto",
"number_of_subscribers": "{{count}} Abonantoj", "number_of_users_plural": "{{count}} uzantoj",
"number_of_points": "{{count}} Voĉdonoj", "number_of_subscribers": "{{count}} abonanto",
"number_of_subscribers_plural": "{{count}} abonantoj",
"number_of_points": "{{count}} voĉdono",
"number_of_points_plural": "{{count}} voĉdonoj",
"name": "Nomo", "name": "Nomo",
"title": "Titolo", "title": "Titolo",
"category": "Kategorio", "category": "Kategorio",
@ -69,10 +73,10 @@
"unsubscribe": "Malaboni", "unsubscribe": "Malaboni",
"subscribe": "Aboni", "subscribe": "Aboni",
"subscribed": "Abonita", "subscribed": "Abonita",
"prev": "Antaŭe", "prev": "Malpluen",
"next": "Poste", "next": "Pluen",
"sidebar": "Flankstango", "sidebar": "Flankobreto",
"sort_type": "Klasi per kia", "sort_type": "Ordigilo",
"hot": "Varmaj", "hot": "Varmaj",
"new": "Novaj", "new": "Novaj",
"top_day": "Supraj tagaj", "top_day": "Supraj tagaj",
@ -84,46 +88,46 @@
"api": "API", "api": "API",
"inbox": "Ricevujo", "inbox": "Ricevujo",
"inbox_for": "Ricevujo de <1>{{user}}</1>", "inbox_for": "Ricevujo de <1>{{user}}</1>",
"mark_all_as_read": "marki ĉiujn kiel legitaj", "mark_all_as_read": "marki ĉiujn legitaj",
"type": "Tipo", "type": "Tipo",
"unread": "Nelegitaj", "unread": "Nelegitaj",
"reply_sent": "Repliko sendis", "reply_sent": "Respondo sendiĝis",
"search": "Serĉi", "search": "Serĉi",
"overview": "Resumo", "overview": "Resumo",
"view": "Rigardi", "view": "Rigardi",
"logout": "Elsaluti", "logout": "Adiaŭi",
"login_sign_up": "Ensaluti / Registriĝi", "login_sign_up": "Saluti / Registriĝi",
"login": "Ensaluti", "login": "Saluti",
"sign_up": "Registriĝi", "sign_up": "Registriĝi",
"notifications_error": "Labortablaj avizoj estas nehavebla en via retumilo. Provu Firefox-on aŭ Chrome-on.", "notifications_error": "Labortablaj avizoj estas nehaveblaj en via foliumilo. Provu foliumilojn Firefox aŭ Chrome.",
"unread_messages": "Nelegitaj Mesaĝoj", "unread_messages": "Nelegitaj mesaĝoj",
"password": "Pasvorto", "password": "Pasvorto",
"verify_password": "Konfirmu Vian Pasvorton", "verify_password": "Konfirmu vian pasvorton",
"email": "Retadreso", "email": "Retpoŝtadreso",
"optional": "Fakultativa", "optional": "Malnepra",
"expires": "Finiĝos", "expires": "Finiĝos",
"url": "URL", "url": "URL",
"body": "Ĉefparto", "body": "Ĉefparto",
"copy_suggested_title": "kopii la sugestiitan titolon: {{title}}", "copy_suggested_title": "kopii la proponitan titolon: {{title}}",
"community": "Komunumo", "community": "Komunumo",
"expand_here": "Ekspansii ĉi tie", "expand_here": "Etendi ĉi tie",
"subscribe_to_communities": "Aboni al iuj <1>komunumoj</1>.", "subscribe_to_communities": "Aboni al iuj <1>komunumoj</1>.",
"chat": "Babilo", "chat": "Babilo",
"recent_comments": "Freŝaj Komentoj", "recent_comments": "Freŝaj komentoj",
"no_results": "Ne rezultoj.", "no_results": "Neniuj rezultoj.",
"setup": "Agordi", "setup": "Agordi",
"lemmy_instance_setup": "Agordi Instancon de Lemmy", "lemmy_instance_setup": "Agordi nodon de Lemmy",
"setup_admin": "Agordi Retejan Administranton", "setup_admin": "Agordi administranton de retejo",
"your_site": "via retejo", "your_site": "via retejo",
"modified": "modifita", "modified": "modifita",
"nsfw": "NSFW", "nsfw": "Konsterna",
"show_nsfw": "Vidigi NSFW-an enhavon", "show_nsfw": "Montri konsternan enhavon",
"sponsors": "Subtenantoj", "sponsors": "Subtenantoj",
"sponsors_of_lemmy": "Subtenantoj de Lemmy", "sponsors_of_lemmy": "Subtenantoj de Lemmy",
"sponsor_message": "Lemmy estas senpaga, <1>liberkoda</1> programaro. Tio signifas ne reklami, pagigi, aŭ riska kapitalo, ĉiam. Viaj donacoj rekte subtenas plentempan evoluon de la projekto. Dankon al tiuj homoj:", "sponsor_message": "Lemmy estas senpaga, <1>liberkoda</1> programaro, sen reklamoj, pagigado, aŭ riska kapitalo, ĉiam ajn. Viaj donacoj rekte subtenas plentempan evoluigadon de la projekto. Dankon al tiuj homoj:",
"support_on_patreon": "Subteni per Patreon", "support_on_patreon": "Subteni per Patreon",
"general_sponsors": "Ĝeneralaj Subtenantoj estas tiuj ke donacis inter $10 kaj $39 al Lemmy.", "general_sponsors": "Ĝeneralaj subtenantoj estas tiuj, kiuj donacis inter $10 kaj $39 al Lemmy.",
"crypto": "Crypto", "crypto": "Ĉifroteĥnikaro",
"bitcoin": "Bitcoin", "bitcoin": "Bitcoin",
"ethereum": "Ethereum", "ethereum": "Ethereum",
"monero": "Monero", "monero": "Monero",
@ -133,45 +137,124 @@
"to": "al", "to": "al",
"transfer_community": "transdoni la komunumon", "transfer_community": "transdoni la komunumon",
"transfer_site": "transdoni la retejon", "transfer_site": "transdoni la retejon",
"powered_by": "Konstruis per", "powered_by": "Konstruita per",
"landing_0": "Lemmy estas <1>ligila agregatilo</1> / Reddit anstataŭo ke intenciĝas funkci en la <2>federacio-universo</2>.<3></3>ĝi estas mem-gastigebla, havas nuna-ĝisdatigajn komentarojn, kaj estas malgrandega (<4>~80kB</4>). Federacio en la ActivityPub-an reton estas planizita. <5></5>Estas <6>fruega beta versio</6>, kaj multaj trajtoj estas nune difektaj aŭ mankaj. <7></7>Sugestias novajn trajtojn aŭ raportas cimojn <8>ĉi tie.</8><9></9>Faris per <10>Rust</10>, <11>Actix</11>, <12>Inferno</12>, <13>Typescript</13>.", "landing": "Lemmy estas <1>amasigilo de ligiloj</1> / alternativo de Reddit, intencita funkcii en la <2>federuniverso</2>.<3></3>ĝi estas mem-gastigebla, havas tuj-ĝisdatigojn de komentaroj, kaj estas malgrandega (<4>~80kB</4>). Federado en la reto de ActivityPub estas planita. <5></5>Ĉi tio estas <6>tre frua beta-versio</6>, kaj multaj funkcioj estas nune difektaj aŭ mankaj. <7></7>Proponu novajn funkciojn aŭ raportu erarojn <8>ĉi tie.</8><9></9>Konstruita per <10>Rust</10>, <11>Actix</11>, <12>Inferno</12>, <13>Typescript</13>.",
"not_logged_in": "Ne estas ensalutinta.", "not_logged_in": "Nesalutinta.",
"community_ban": "Vi estas forbarita de la komunumo.", "community_ban": "Vi estas forbarita de la komunumo.",
"site_ban": "Vi estas forbarita de la retejo", "site_ban": "Vi estas forbarita de la retejo",
"couldnt_create_comment": "Ne povis krei la komenton.", "couldnt_create_comment": "Ne povis krei la komenton.",
"couldnt_like_comment": "Ne povis ŝati la komenton.", "couldnt_like_comment": "Ne povis ŝati la komenton.",
"couldnt_update_comment": "Ne povis ĝisdatigi komenton.", "couldnt_update_comment": "Ne povis ĝisdatigi la komenton.",
"couldnt_save_comment": "Ne povis konservi komenton.", "couldnt_save_comment": "Ne povis konservi la komenton.",
"no_comment_edit_allowed": "Ne rajtas redakti la komenton.", "no_comment_edit_allowed": "Ne rajtas redakti la komenton.",
"no_post_edit_allowed": "Ne rajtas redakti la poŝton.", "no_post_edit_allowed": "Ne rajtas redakti la afiŝon.",
"no_community_edit_allowed": "Ne rajtas redakti la komunumon.", "no_community_edit_allowed": "Ne rajtas redakti la komunumon.",
"couldnt_find_community": "Ne povis trovi la komunumon.", "couldnt_find_community": "Ne povis trovi la komunumon.",
"couldnt_update_community": "Ne povis ĝisdatigi la komunumon.", "couldnt_update_community": "Ne povis ĝisdatigi la komunumon.",
"community_already_exists": "Komunumo jam ekzistas.", "community_already_exists": "Komunumo jam ekzistas.",
"community_moderator_already_exists": "Komunuma moderanto jam ekzistas.", "community_moderator_already_exists": "Komunuma moderanto jam ekzistas.",
"community_follower_already_exists": "Komunuma sekvanto.", "community_follower_already_exists": "Abonanto de komunumo jam ekzistas.",
"community_user_already_banned": "Komunuma uzanto jam estas forbarita.", "community_user_already_banned": "Uzanto de komunumo jam estas forbarita.",
"couldnt_create_post": "Ne povis krei la poŝton.", "couldnt_create_post": "Ne povis krei la afiŝon.",
"couldnt_like_post": "Ne povis ŝati la poŝton.", "couldnt_like_post": "Ne povis ŝati la afiŝon.",
"couldnt_find_post": "Ne povis trovi la poŝton.", "couldnt_find_post": "Ne povis trovi la afiŝon.",
"couldnt_get_posts": "Ne povis irpreni poŝtojn", "couldnt_get_posts": "Ne povis akiri afiŝojn",
"couldnt_update_post": "Ne povis ĝisdatigi la poŝton", "couldnt_update_post": "Ne povis ĝisdatigi la afiŝon",
"couldnt_save_post": "Ne povis konservi la poŝton.", "couldnt_save_post": "Ne povis konservi la afiŝon.",
"no_slurs": "Ne bigotaj vortoj.", "no_slurs": "Neniuj fivortoj.",
"not_an_admin": "Ne estas administranto.", "not_an_admin": "Ne estas administranto.",
"site_already_exists": "Retejo jam ekzistas.", "site_already_exists": "Retejo jam ekzistas.",
"couldnt_update_site": "Ne povis ĝisdatigi la retejon.", "couldnt_update_site": "Ne povis ĝisdatigi la retejon.",
"couldnt_find_that_username_or_email": "Ne povis trovi tiun uzantnomon aŭ retadreson.", "couldnt_find_that_username_or_email": "Ne povis trovi tiun uzantonomon aŭ retpoŝtadreson.",
"password_incorrect": "Pasvorto malĝustas.", "password_incorrect": "Pasvorto malĝustas.",
"passwords_dont_match": "Pasvortoj ne samas.", "passwords_dont_match": "Pasvortoj ne samas.",
"admin_already_created": "Pardonu, jam estas administranto.", "admin_already_created": "Pardonu, jam estas administranto.",
"user_already_exists": "Uzanto jam ekzistas.", "user_already_exists": "Uzanto jam ekzistas.",
"couldnt_update_user": "Ne povis ĝisdatigi la uzanton.", "couldnt_update_user": "Ne povis ĝisdatigi la uzanton.",
"system_err_login": "Sistema eraro. Provu elsaluti kaj ensaluti.", "system_err_login": "Sistema eraro. Provu adiaŭi kaj resaluti.",
"send_message": "Sendi mesaĝon", "send_message": "Sendi mesaĝon",
"message": "Mesaĝo", "message": "Mesaĝo",
"number_of_communities": "{{count}} Komunumo", "number_of_communities": "{{count}} komunumo",
"number_of_communities_plural": "{{count}} Komunumoj", "number_of_communities_plural": "{{count}} komunumoj",
"more": "pli", "more": "pli",
"select_a_community": "Elekti komunumon" "select_a_community": "Elekti komunumon",
"click_to_delete_picture": "Klaku por forigi bildon.",
"cross_posted_to": "transafiŝita al: ",
"invalid_community_name": "Nevalida nomo.",
"picture_deleted": "Bildo foriĝis.",
"create_private_message": "Krei privatan mesaĝon",
"send_secure_message": "Sendi sekuran mesaĝon",
"avatar": "Profilbildo",
"show_avatars": "Montri profilbildojn",
"formatting_help": "helpo pri formatado",
"sorting_help": "helpo pri ordigado",
"sticky": "pingli",
"unsticky": "malpingli",
"stickied": "pinglita",
"delete_account": "Forigi konton",
"delete_account_confirm": "Averto: ĉi tio por ĉiam forigos ĉiujn viajn datumojn. Enigu pasvorton por konfirmi.",
"preview": "Antaŭrigardo",
"upload_image": "alŝuti bildon",
"upload_avatar": "Alŝuti profilbildon",
"banned": "forbarita",
"creator": "kreinto",
"number_online": "{{count}} uzanto enreta",
"number_online_plural": "{{count}} uzantoj enretaj",
"old": "Malnovaj",
"docs": "Dokumentaĵo",
"view_source": "montri fonton",
"show_context": "Montri kuntekston",
"admin_settings": "Agordoj de agministranto",
"site_config": "Agordaro de retejo",
"banned_users": "Forbaritaj uzantoj",
"donate": "Donaci",
"archive_link": "arĥiva ligilo",
"replies": "Respondoj",
"mentions": "Mencioj",
"message_sent": "Mesaĝo sendiĝis",
"post_title_too_long": "Titolo de afiŝo estas tro longa.",
"messages": "Mesaĝoj",
"old_password": "Malnova pasvorto",
"forgot_password": "forgesita pasvorto",
"reset_password_mail_sent": "Retletero sendiĝis por restarigi vian pasvorton.",
"password_change": "Ŝanĝo de pasvorto",
"new_password": "Nova pasvorto",
"no_email_setup": "Ĉi tiu servilo ne agordis ĝuste retpoŝton.",
"matrix_user_id": "Uzanto de Matrix",
"private_message_disclaimer": "Averto: Privataj mesaĝoj en Lemmy ne estas sekuraj. Bonvolu krei konton je <1>Riot.im</1> por sekura mesaĝado.",
"send_notifications_to_email": "Sendi sciigojn al retpoŝtadreso",
"language": "Lingvo",
"browser_default": "Laŭ foliumilo",
"downvotes_disabled": "Kontraŭvoĉoj malŝaltiĝis",
"enable_downvotes": "Ŝalti kontraŭvoĉojn",
"open_registration": "Ebligi registradon",
"registration_closed": "Registrado malebliĝis",
"enable_nsfw": "Ŝalti konsternajn",
"support_on_open_collective": "Subteni per OpenCollective",
"theme": "Haŭto",
"support_on_liberapay": "Subteni per Liberapay",
"donate_to_lemmy": "Donaci al Lemmy",
"silver_sponsors": "Arĝentaj subtenantoj estas tiuj, kiuj donacis $40 al Lemmy.",
"are_you_sure": "ĉu vi certas?",
"yes": "jes",
"no": "ne",
"logged_in": "Salutinta.",
"site_saved": "Retejo konserviĝis.",
"couldnt_get_comments": "Ne povis akiri la komentojn.",
"email_already_exists": "Retpoŝtadreso jam ekzistas.",
"couldnt_create_private_message": "Ne povis krei privatan mesaĝon.",
"no_private_message_edit_allowed": "Ne rajtas redakti la privatan mesaĝon.",
"couldnt_update_private_message": "Ne povis ĝisdatigi la privatan mesaĝon.",
"time": "Tempo",
"action": "Ago",
"emoji_picker": "Elektilo de bildsignoj",
"block_leaving": "Ĉu vi certe volas foriri?",
"from": "de",
"invalid_username": "Nevalida uzantonomo.",
"upvote": "Porvoĉi",
"number_of_upvotes": "{{count}} porvoĉo",
"number_of_upvotes_plural": "{{count}} porvoĉoj",
"downvote": "Kontraŭvoĉi",
"number_of_downvotes": "{{count}} kontraŭvoĉo",
"number_of_downvotes_plural": "{{count}} kontraŭvoĉoj"
} }

View file

@ -56,7 +56,7 @@
"mark_as_read": "segna come letto", "mark_as_read": "segna come letto",
"mark_as_unread": "segna come non letto", "mark_as_unread": "segna come non letto",
"delete": "cancella", "delete": "cancella",
"deleted": "eliminato dall'autore del commento", "deleted": "eliminato dal creatore",
"delete_account": "Cancella Account", "delete_account": "Cancella Account",
"delete_account_confirm": "Attenzione: stai per cancellare permanentemente tutti i tuoi dati. Inserisci la tua password per confermare questa azione.", "delete_account_confirm": "Attenzione: stai per cancellare permanentemente tutti i tuoi dati. Inserisci la tua password per confermare questa azione.",
"restore": "ripristina", "restore": "ripristina",
@ -151,7 +151,7 @@
"ethereum": "Ethereum", "ethereum": "Ethereum",
"monero": "Monero", "monero": "Monero",
"code": "Codice", "code": "Codice",
"joined": "Iscritto da", "joined": "Iscritto",
"by": "di", "by": "di",
"to": "su", "to": "su",
"transfer_community": "trasferisci comunità", "transfer_community": "trasferisci comunità",
@ -175,7 +175,7 @@
"couldnt_update_community": "Impossibile aggiornare la comunità.", "couldnt_update_community": "Impossibile aggiornare la comunità.",
"community_already_exists": "La comunità esiste già.", "community_already_exists": "La comunità esiste già.",
"community_moderator_already_exists": "Questo utente è già moderatore della comunità.", "community_moderator_already_exists": "Questo utente è già moderatore della comunità.",
"community_follower_already_exists": "Questo utente è già moderatore della comunità.", "community_follower_already_exists": "Questo utente è già membro della comunità.",
"community_user_already_banned": "L'utente della comunità è già stato espulso.", "community_user_already_banned": "L'utente della comunità è già stato espulso.",
"couldnt_create_post": "Impossibile creare la pubblicazione.", "couldnt_create_post": "Impossibile creare la pubblicazione.",
"couldnt_like_post": "Impossibile apprezzare la pubblicazione.", "couldnt_like_post": "Impossibile apprezzare la pubblicazione.",

108
ui/translations/sq.json vendored Normal file
View file

@ -0,0 +1,108 @@
{
"remove_post": "Hiqe Postimin",
"no_posts": "Nuk ka Postime.",
"create_a_post": "Krijo një postim",
"create_post": "Krijo Postimin",
"posts": "Postime",
"related_posts": "Këto postime mund të jenë të lidhura",
"cross_posts": "Ky link është postuar edhe te:",
"cross_post": "shumë-postim",
"cross_posted_to": "shumë-postuar në: ",
"comments": "Komentet",
"remove_comment": "Fshije Komentin",
"communities": "Komunitetet",
"users": "Përdoruesit",
"create_a_community": "Krijo një komunitet",
"select_a_community": "Përzgjedh një komunitet",
"create_community": "Krijo komunitetin",
"remove_community": "Fshije Komunitetin",
"subscribed_to_communities": "I abonuar në",
"trending_communities": "Trendi",
"list_of_communities": "Lista e komuniteteve",
"community_reqs": "gërma të vogla, nënvizim, dhe pa hapësira.",
"invalid_community_name": "Emër invalid.",
"create_private_message": "Krijo Mesazh Privat",
"send_secure_message": "Dërgo Mesazh të Sigurtë",
"send_message": "Dërgo Mesazh",
"message": "Mesazh",
"edit": "redakto",
"reply": "përgjigju",
"more": "më shumë",
"cancel": "Anulo",
"preview": "Shiko paraprakisht",
"upload_image": "ngarko imazhin",
"upload_avatar": "Ngarko foton e profilit",
"show_avatars": "Shfaq fotot e profilit",
"show_context": "Shfaq kontekstin",
"formatting_help": "ndihmë me formatimin",
"sorting_help": "ndihmë me radhitjen",
"view_source": "shiko origjinën",
"unlock": "hape",
"lock": "mbyll",
"unsticky": "çngjit",
"link": "link",
"archive_link": "link i arkivuar",
"mod": "moderator",
"mods": "moderatorët",
"settings": "Konfigurimet",
"site_config": "Konfigurimet e faqes",
"remove_as_mod": "Largoje si moderator",
"appoint_as_mod": "emëro si moderator",
"modlog": "Ditari i moderimit",
"admin": "administrator",
"admins": "administratorët",
"appoint_as_admin": "emëro si administrator",
"remove": "fshije",
"removed": "është fshirë nga një moderator",
"locked": "mbyllur",
"stickied": "ngjitur",
"reason": "Arsye",
"mark_as_read": "shëno si të lexuar",
"mark_as_unread": "shëno si të palexuar",
"delete": "fshije",
"delete_account": "Fshije Account-in",
"click_to_delete_picture": "Shtyp për të fshirë imazhin.",
"picture_deleted": "Imazhi është fshirë.",
"restore": "rikthe",
"ban": "",
"ban_from_site": "",
"save": "ruaj",
"unsave": "anulo ruajtjen",
"create": "krijo",
"creator": "krijuesi",
"username": "Emri virtual",
"email_or_username": "Email-i ose Emri virtual",
"number_of_users": "{{count}} Përdorues",
"number_of_users_plural": "{{count}} Përdoruesa",
"number_of_subscribers": "{{count}} i abonuar",
"number_of_subscribers_plural": "{{count}} të abonuar",
"number_of_points": "{{count}} Pikë",
"number_of_points_plural": "{{count}} Pikë",
"number_online": "{{count}} Përdorues Online",
"number_online_plural": "{{count}} Përdoruesa Online",
"name": "Emri",
"title": "Titulli",
"category": "Kategoria",
"subscribers": "Të abonuarit",
"both": "Të dy",
"saved": "E ruajtur",
"subscribe": "Abonohu",
"subscribed": "Jeni abonuar",
"next": "Tjetra",
"post": "postim",
"number_of_posts": "{{count}} Postim",
"number_of_posts_plural": "{{count}} Postime",
"number_of_comments": "{{count}} Koment",
"number_of_comments_plural": "{{count}} Komente",
"number_of_communities": "{{count}} Komunitet",
"number_of_communities_plural": "{{count}} Komunitete",
"avatar": "Fotoja e profilit",
"sticky": "ngjite",
"moderates": "Moderon",
"admin_settings": "Konfigurimet administrative",
"remove_as_admin": "largoje si administrator",
"deleted": "është fshirë nga krijuesi",
"delete_account_confirm": "Paralajmërim: kjo do të fshij të gjitha të dhënat e juaja përgjithmonë. Shtyp fjalëkalimin tënd për ta konfirmuar.",
"unsubscribe": "Çabonohu",
"prev": "E mëparshme"
}