diff --git a/.github/ISSUE_TEMPLATE/BUG_REPORT.md b/.github/ISSUE_TEMPLATE/BUG_REPORT.md new file mode 100644 index 000000000..b57e0e125 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/BUG_REPORT.md @@ -0,0 +1,27 @@ +--- +name: "\U0001F41E Bug Report" +about: Create a report to help us improve Lemmy +title: '' +labels: bug +assignees: '' + +--- + +Found a bug? Please fill out the sections below. 👍 + +### Issue Summary + +A summary of the bug. + + +### Steps to Reproduce + +1. (for example) I clicked login, and an endless spinner show up. +2. I tried to install lemmy via this guide, and I'm getting this error. +3. ... + +### Technical details + +* Please post your log: `sudo docker-compose logs > lemmy_log.out`. +* What OS are you trying to install lemmy on? +* Any browser console errors? diff --git a/.github/ISSUE_TEMPLATE/FEATURE_REQUEST.md b/.github/ISSUE_TEMPLATE/FEATURE_REQUEST.md new file mode 100644 index 000000000..957f4cdfc --- /dev/null +++ b/.github/ISSUE_TEMPLATE/FEATURE_REQUEST.md @@ -0,0 +1,42 @@ +--- +name: "\U0001F680 Feature request" +about: Suggest an idea for improving Lemmy +title: '' +labels: enhancement +assignees: '' + +--- + +### Is your proposal related to a problem? + + + +(Write your answer here.) + +### Describe the solution you'd like + + + +(Describe your proposed solution here.) + +### Describe alternatives you've considered + + + +(Write your answer here.) + +### Additional context + + + +(Write your answer here.) diff --git a/.github/ISSUE_TEMPLATE/QUESTION.md b/.github/ISSUE_TEMPLATE/QUESTION.md new file mode 100644 index 000000000..b45f8f1e5 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/QUESTION.md @@ -0,0 +1,10 @@ +--- +name: "? Question" +about: General questions about Lemmy +title: '' +labels: question +assignees: '' + +--- + +What's the question you have about lemmy? diff --git a/ansible/VERSION b/ansible/VERSION index aed5a7df6..076cd4b2b 100644 --- a/ansible/VERSION +++ b/ansible/VERSION @@ -1 +1 @@ -v0.7.5 +v0.7.11 diff --git a/ansible/ansible.cfg b/ansible/ansible.cfg index 74b6ab2f1..696466297 100644 --- a/ansible/ansible.cfg +++ b/ansible/ansible.cfg @@ -1,6 +1,6 @@ [defaults] -inventory=inventory -interpreter_python=/usr/bin/python3 +inventory = inventory +interpreter_python = /usr/bin/python3 [ssh_connection] pipelining = True diff --git a/ansible/inventory.example b/ansible/inventory.example index 52b45d3c3..c5f98653d 100644 --- a/ansible/inventory.example +++ b/ansible/inventory.example @@ -1,6 +1,12 @@ [lemmy] -# define the username and hostname that you use for ssh connection, and specify the domain -myuser@example.com domain=example.com letsencrypt_contact_email=your@email.com +# to get started, copy this file to `inventory` and adjust the values below. +# - `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] ansible_connection=ssh diff --git a/ansible/lemmy.yml b/ansible/lemmy.yml index 7b78ab8d3..3520c4042 100644 --- a/ansible/lemmy.yml +++ b/ansible/lemmy.yml @@ -5,18 +5,41 @@ # https://www.josharcher.uk/code/ansible-python-connection-failure-ubuntu-server-1604/ gather_facts: False 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 + # 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) args: executable: /bin/bash register: output - changed_when: output.stdout != "" + changed_when: output.stdout != '' + - setup: # gather facts tasks: - name: install dependencies 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 is version('20.04', '<') + + - name: install certbot-nginx on ubuntu > 20 + apt: + pkg: + - 'python3-certbot-nginx' + when: ansible_distribution == 'Ubuntu' and ansible_distribution_version is version('20.04', '>=') - name: request initial letsencrypt certificate command: certbot certonly --nginx --agree-tos -d '{{ domain }}' -m '{{ letsencrypt_contact_email }}' @@ -24,27 +47,48 @@ creates: '/etc/letsencrypt/live/{{domain}}/privkey.pem' - name: create lemmy folder - file: path={{item.path}} {{item.owner}} state=directory + file: + path: '{{item.path}}' + owner: '{{item.owner}}' + state: directory with_items: - - { path: '/lemmy/', owner: 'root' } - - { path: '/lemmy/volumes/', owner: 'root' } - - { path: '/lemmy/volumes/pictrs/', owner: '991' } + - path: '{{lemmy_base_dir}}' + owner: 'root' + - path: '{{lemmy_base_dir}}/volumes/' + owner: 'root' + - path: '{{lemmy_base_dir}}/volumes/pictrs/' + owner: '991' - block: - 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: - - { src: 'templates/docker-compose.yml', dest: '/lemmy/docker-compose.yml', mode: '0600' } - - { src: 'templates/nginx.conf', dest: '/etc/nginx/sites-enabled/lemmy.conf', mode: '0644' } - - { src: '../docker/iframely.config.local.js', dest: '/lemmy/iframely.config.local.js', mode: '0600' } - vars: + - src: 'templates/docker-compose.yml' + dest: '{{lemmy_base_dir}}/docker-compose.yml' + 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: lemmy_docker_image: "dessalines/lemmy:{{ lookup('file', 'VERSION') }}" lemmy_port: "8536" pictshare_port: "8537" iframely_port: "8538" - 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: postgres_password: "{{ lookup('password', 'passwords/{{ inventory_hostname }}/postgres chars=ascii_letters,digits') }}" jwt_password: "{{ lookup('password', 'passwords/{{ inventory_hostname }}/jwt chars=ascii_letters,digits') }}" @@ -57,7 +101,7 @@ - name: start docker-compose docker_compose: - project_src: /lemmy/ + project_src: '{{lemmy_base_dir}}' state: present pull: yes remove_orphans: yes @@ -67,7 +111,7 @@ - name: certbot renewal cronjob cron: - special_time=daily - name=certbot-renew-lemmy - user=root - job="certbot certonly --nginx -d '{{ domain }}' --deploy-hook 'nginx -s reload'" + special_time: daily + name: certbot-renew-lemmy + user: root + job: "certbot certonly --nginx -d '{{ domain }}' --deploy-hook 'nginx -s reload'" diff --git a/ansible/lemmy_dev.yml b/ansible/lemmy_dev.yml index 7a3683610..e85566653 100644 --- a/ansible/lemmy_dev.yml +++ b/ansible/lemmy_dev.yml @@ -1,24 +1,34 @@ --- - hosts: all vars: - lemmy_docker_image: "lemmy:dev" + lemmy_docker_image: 'lemmy:dev' # Install python if required # https://www.josharcher.uk/code/ansible-python-connection-failure-ubuntu-server-1604/ gather_facts: False 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 raw: test -e /usr/bin/python || (apt -y update && apt install -y python-minimal python-setuptools) args: executable: /bin/bash register: output - changed_when: output.stdout != "" + changed_when: output.stdout != '' - setup: # gather facts tasks: - name: install dependencies 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 command: certbot certonly --nginx --agree-tos -d '{{ domain }}' -m '{{ letsencrypt_contact_email }}' @@ -26,25 +36,46 @@ creates: '/etc/letsencrypt/live/{{domain}}/privkey.pem' - name: create lemmy folder - file: path={{item.path}} owner={{item.owner}} state=directory + file: + path: '{{item.path}}' + owner: '{{item.owner}}' + state: directory with_items: - - { path: '/lemmy/', owner: 'root' } - - { path: '/lemmy/volumes/', owner: 'root' } - - { path: '/lemmy/volumes/pictrs/', owner: '991' } + - path: '{{lemmy_base_dir}}/lemmy/' + owner: 'root' + - path: '{{lemmy_base_dir}}/volumes/' + owner: 'root' + - path: '{{lemmy_base_dir}}/volumes/pictrs/' + owner: '991' - block: - 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: - - { src: 'templates/docker-compose.yml', dest: '/lemmy/docker-compose.yml', mode: '0600' } - - { src: 'templates/nginx.conf', dest: '/etc/nginx/sites-enabled/lemmy.conf', mode: '0644' } - - { src: '../docker/iframely.config.local.js', dest: '/lemmy/iframely.config.local.js', mode: '0600' } + - src: 'templates/docker-compose.yml' + dest: '{{lemmy_base_dir}}/docker-compose.yml' + 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) - template: src='templates/config.hjson' dest='/lemmy/lemmy.hjson' mode='0600' force='no' owner='1000' group='1000' - vars: - postgres_password: "{{ lookup('password', 'passwords/{{ inventory_hostname }}/postgres chars=ascii_letters,digits') }}" - jwt_password: "{{ lookup('password', 'passwords/{{ inventory_hostname }}/jwt chars=ascii_letters,digits') }}" + template: + src: 'templates/config.hjson' + dest: '{{lemmy_base_dir}}/lemmy.hjson' + mode: '0600' + force: false + owner: '1000' + group: '1000' + vars: + postgres_password: "{{ lookup('password', 'passwords/{{ inventory_hostname }}/postgres chars=ascii_letters,digits') }}" + jwt_password: "{{ lookup('password', 'passwords/{{ inventory_hostname }}/jwt chars=ascii_letters,digits') }}" - name: build the dev docker image local_action: shell cd .. && sudo docker build . -f docker/dev/Dockerfile -t lemmy:dev @@ -59,22 +90,29 @@ local_action: shell sudo docker save lemmy:dev > lemmy-dev.tar - 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 docker_image: name: lemmy tag: dev - load_path: /lemmy/lemmy-dev.tar + load_path: '{{lemmy_base_dir}}/lemmy-dev.tar' source: load force_source: yes register: image_import - 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 - 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 systemd: @@ -86,7 +124,7 @@ # be a problem for testing - name: start docker-compose docker_compose: - project_src: /lemmy/ + project_src: '{{lemmy_base_dir}}' state: present recreate: always remove_orphans: yes @@ -97,7 +135,7 @@ - name: certbot renewal cronjob cron: - special_time=daily - name=certbot-renew-lemmy - user=root - job="certbot certonly --nginx -d '{{ domain }}' --deploy-hook 'nginx -s reload'" + special_time: daily + name: certbot-renew-lemmy + user: root + job: "certbot certonly --nginx -d '{{ domain }}' --deploy-hook 'nginx -s reload'" diff --git a/ansible/uninstall.yml b/ansible/uninstall.yml index 252c5bd1f..34c24d2ed 100644 --- a/ansible/uninstall.yml +++ b/ansible/uninstall.yml @@ -22,27 +22,33 @@ - name: stop docker-compose docker_compose: - project_src: /lemmy/ + project_src: '{{lemmy_base_dir}}' state: absent - name: delete data - file: path={{item.path}} state=absent + file: + path: '{{item.path}}' + state: absent with_items: - - { path: '/lemmy/' } - - { path: '/etc/nginx/sites-enabled/lemmy.conf' } + - path: '{{lemmy_base_dir}}' + - path: '/etc/nginx/sites-enabled/lemmy.conf' - name: Remove a volume - docker_volume: name={{item.name}} state=absent + docker_volume: + name: '{{item.name}}' + state: absent with_items: - - { name: 'lemmy_lemmy_db' } - - { name: 'lemmy_lemmy_pictshare' } + - name: 'lemmy_lemmy_db' + - name: 'lemmy_lemmy_pictshare' - name: delete entire ecloud folder - file: path='/mnt/repo-base/' state=absent + file: + path: '/mnt/repo-base/' + state: absent when: delete_certs|bool - name: remove certbot cronjob cron: - name=certbot-renew-lemmy - state=absent + name: certbot-renew-lemmy + state: absent diff --git a/docker/dev/docker-compose.yml b/docker/dev/docker-compose.yml index bdcb4308a..51a3ecdab 100644 --- a/docker/dev/docker-compose.yml +++ b/docker/dev/docker-compose.yml @@ -20,6 +20,8 @@ services: postgres: image: postgres:12-alpine + ports: + - "127.0.0.1:5432:5432" environment: - POSTGRES_USER=lemmy - POSTGRES_PASSWORD=password diff --git a/docker/federation-test/run-tests.sh b/docker/federation-test/run-tests.sh index fdb0e129b..57c6cc8ff 100755 --- a/docker/federation-test/run-tests.sh +++ b/docker/federation-test/run-tests.sh @@ -5,17 +5,21 @@ 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 -for Item in alpha beta gamma ; do - sudo mkdir -p volumes/pictrs_$Item - sudo chown -R 991:991 volumes/pictrs_$Item -done +sudo mkdir -p volumes/pictrs_alpha +sudo chown -R 991:991 volumes/pictrs_alpha sudo docker-compose --file ../federation/docker-compose.yml --project-directory . up -d pushd ../../ui -yarn echo "Waiting for Lemmy to start..." while [[ "$(curl -s -o /dev/null -w '%{http_code}' 'localhost:8540/api/v1/site')" != "200" ]]; do sleep 1; done while [[ "$(curl -s -o /dev/null -w '%{http_code}' 'localhost:8550/api/v1/site')" != "200" ]]; do sleep 1; done diff --git a/docker/federation-test/servers.sh b/docker/federation-test/servers.sh new file mode 100755 index 000000000..36f10cd82 --- /dev/null +++ b/docker/federation-test/servers.sh @@ -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 diff --git a/docker/federation-test/tests.sh b/docker/federation-test/tests.sh new file mode 100755 index 000000000..2e88ffb25 --- /dev/null +++ b/docker/federation-test/tests.sh @@ -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 diff --git a/docker/federation/Dockerfile b/docker/federation/Dockerfile index ec7bf2d22..caf081758 100644 --- a/docker/federation/Dockerfile +++ b/docker/federation/Dockerfile @@ -3,7 +3,7 @@ FROM ekidd/rust-musl-builder:1.42.0-openssl11 USER root RUN mkdir /app/dist/documentation/ -p \ && addgroup --gid 1001 lemmy \ - && adduser --disabled-password --shell /bin/sh -u 1001 --ingroup lemmy lemmy + && adduser --gecos "" --disabled-password --shell /bin/sh -u 1001 --ingroup lemmy lemmy # Copy resources COPY server/config/defaults.hjson /app/config/defaults.hjson diff --git a/docker/federation/docker-compose.yml b/docker/federation/docker-compose.yml index d5f9a5225..cbc648e65 100644 --- a/docker/federation/docker-compose.yml +++ b/docker/federation/docker-compose.yml @@ -12,28 +12,33 @@ services: - ../federation/nginx.conf:/etc/nginx/nginx.conf restart: on-failure depends_on: - - lemmy_alpha - - pictrs_alpha - - lemmy_beta - - pictrs_beta - - lemmy_gamma - - pictrs_gamma + - lemmy-alpha + - pictrs + - lemmy-beta + - lemmy-gamma - 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 environment: - - LEMMY_HOSTNAME=lemmy_alpha:8540 + - LEMMY_HOSTNAME=lemmy-alpha:8540 - LEMMY_DATABASE_URL=postgres://lemmy:password@postgres_alpha:5432/lemmy - LEMMY_JWT_SECRET=changeme - LEMMY_FRONT_END_DIR=/app/dist - LEMMY_FEDERATION__ENABLED=true - LEMMY_FEDERATION__TLS_ENABLED=false - - LEMMY_FEDERATION__ALLOWED_INSTANCES=lemmy_beta,lemmy_gamma + - LEMMY_FEDERATION__ALLOWED_INSTANCES=lemmy-beta,lemmy-gamma - LEMMY_PORT=8540 - LEMMY_SETUP__ADMIN_USERNAME=lemmy_alpha - LEMMY_SETUP__ADMIN_PASSWORD=lemmy - - LEMMY_SETUP__SITE_NAME=lemmy_alpha + - LEMMY_SETUP__SITE_NAME=lemmy-alpha - RUST_BACKTRACE=1 - RUST_LOG=debug depends_on: @@ -46,26 +51,21 @@ services: - POSTGRES_DB=lemmy volumes: - ./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 environment: - - LEMMY_HOSTNAME=lemmy_beta:8550 + - LEMMY_HOSTNAME=lemmy-beta:8550 - LEMMY_DATABASE_URL=postgres://lemmy:password@postgres_beta:5432/lemmy - LEMMY_JWT_SECRET=changeme - LEMMY_FRONT_END_DIR=/app/dist - LEMMY_FEDERATION__ENABLED=true - LEMMY_FEDERATION__TLS_ENABLED=false - - LEMMY_FEDERATION__ALLOWED_INSTANCES=lemmy_alpha,lemmy_gamma + - LEMMY_FEDERATION__ALLOWED_INSTANCES=lemmy-alpha,lemmy-gamma - LEMMY_PORT=8550 - LEMMY_SETUP__ADMIN_USERNAME=lemmy_beta - LEMMY_SETUP__ADMIN_PASSWORD=lemmy - - LEMMY_SETUP__SITE_NAME=lemmy_beta + - LEMMY_SETUP__SITE_NAME=lemmy-beta - RUST_BACKTRACE=1 - RUST_LOG=debug depends_on: @@ -78,26 +78,21 @@ services: - POSTGRES_DB=lemmy volumes: - ./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 environment: - - LEMMY_HOSTNAME=lemmy_gamma:8560 + - LEMMY_HOSTNAME=lemmy-gamma:8560 - LEMMY_DATABASE_URL=postgres://lemmy:password@postgres_gamma:5432/lemmy - LEMMY_JWT_SECRET=changeme - LEMMY_FRONT_END_DIR=/app/dist - LEMMY_FEDERATION__ENABLED=true - LEMMY_FEDERATION__TLS_ENABLED=false - - LEMMY_FEDERATION__ALLOWED_INSTANCES=lemmy_alpha,lemmy_beta + - LEMMY_FEDERATION__ALLOWED_INSTANCES=lemmy-alpha,lemmy-beta - LEMMY_PORT=8560 - LEMMY_SETUP__ADMIN_USERNAME=lemmy_gamma - LEMMY_SETUP__ADMIN_PASSWORD=lemmy - - LEMMY_SETUP__SITE_NAME=lemmy_gamma + - LEMMY_SETUP__SITE_NAME=lemmy-gamma - RUST_BACKTRACE=1 - RUST_LOG=debug depends_on: @@ -110,11 +105,6 @@ services: - POSTGRES_DB=lemmy volumes: - ./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: image: dogbin/iframely:latest diff --git a/docker/federation/nginx.conf b/docker/federation/nginx.conf index 25160eb6c..2093297eb 100644 --- a/docker/federation/nginx.conf +++ b/docker/federation/nginx.conf @@ -12,7 +12,7 @@ http { client_max_body_size 50M; location / { - proxy_pass http://lemmy_alpha:8540; + proxy_pass http://lemmy-alpha:8540; proxy_set_header X-Real-IP $remote_addr; proxy_set_header Host $host; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; @@ -26,7 +26,7 @@ http { # pict-rs images location /pictrs { 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 Host $host; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; @@ -52,7 +52,7 @@ http { client_max_body_size 50M; location / { - proxy_pass http://lemmy_beta:8550; + proxy_pass http://lemmy-beta:8550; proxy_set_header X-Real-IP $remote_addr; proxy_set_header Host $host; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; @@ -66,7 +66,7 @@ http { # pict-rs images location /pictrs { 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 Host $host; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; @@ -92,7 +92,7 @@ http { client_max_body_size 50M; location / { - proxy_pass http://lemmy_gamma:8560; + proxy_pass http://lemmy-gamma:8560; proxy_set_header X-Real-IP $remote_addr; proxy_set_header Host $host; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; @@ -106,7 +106,7 @@ http { # pict-rs images location /pictrs { 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 Host $host; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; diff --git a/docker/prod/docker-compose.yml b/docker/prod/docker-compose.yml index 018501275..698d2ba6b 100644 --- a/docker/prod/docker-compose.yml +++ b/docker/prod/docker-compose.yml @@ -12,7 +12,7 @@ services: restart: always lemmy: - image: dessalines/lemmy:v0.7.5 + image: dessalines/lemmy:v0.7.11 ports: - "127.0.0.1:8536:8536" restart: always diff --git a/docs/src/contributing_federation_development.md b/docs/src/contributing_federation_development.md index 520a61275..143ae9f8b 100644 --- a/docs/src/contributing_federation_development.md +++ b/docs/src/contributing_federation_development.md @@ -5,14 +5,7 @@ If you don't have a local clone of the Lemmy repo yet, just run the following command: ```bash -git clone https://github.com/LemmyNet/lemmy -b federation -``` - -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 +git clone https://github.com/LemmyNet/lemmy ``` ## Running locally @@ -26,18 +19,34 @@ You need to have the following packages installed, the Docker service needs to b Then run the following ```bash -cd dev/federation-test -./run-federation-test.bash +cd docker/federation +./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 -[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`. +The federation test sets up 3 instances: + +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 -Note that federation is currently in alpha. Only use it for testing, not on any production server, and be aware -that you might have to wipe the instance data at one point or another. +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. 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 @@ -47,11 +56,12 @@ Follow the normal installation instructions, either with [Ansible](administratio ``` federation: { 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/ diff --git a/server/Cargo.lock b/server/Cargo.lock index a8f5c8805..7b0d9a88c 100644 --- a/server/Cargo.lock +++ b/server/Cargo.lock @@ -49,25 +49,25 @@ dependencies = [ [[package]] name = "actix" -version = "0.9.0" +version = "0.10.0-alpha.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4af87564ff659dee8f9981540cac9418c45e910c8072fdedd643a262a38fcaf" +checksum = "a9028932f36d45df020c92317ccb879ab77d8f066f57ff143dd5bee93ba3de0d" dependencies = [ - "actix-http", "actix-rt", "actix_derive", "bitflags", "bytes", "crossbeam-channel", "derive_more", - "futures", - "lazy_static", + "futures-channel", + "futures-util", "log", + "once_cell", "parking_lot", "pin-project", "smallvec", "tokio", - "tokio-util 0.2.0", + "tokio-util 0.3.1", "trust-dns-proto", "trust-dns-resolver", ] @@ -89,9 +89,9 @@ dependencies = [ [[package]] name = "actix-connect" -version = "1.0.2" +version = "2.0.0-alpha.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c95cc9569221e9802bf4c377f6c18b90ef10227d787611decf79fd47d2a8e76c" +checksum = "2551ed85d5e157c13f8f523cdb13a6292d948049eb2dc2072bbee3ec350399a2" dependencies = [ "actix-codec", "actix-rt", @@ -99,18 +99,21 @@ dependencies = [ "actix-utils", "derive_more", "either", - "futures", + "futures-util", "http", "log", + "rustls", + "tokio-rustls", "trust-dns-proto", "trust-dns-resolver", + "webpki", ] [[package]] name = "actix-files" -version = "0.2.2" +version = "0.3.0-alpha.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "193b22cb1f7b4ff12a4eb2415d6d19e47e44ea93e05930b30d05375ea29d3529" +checksum = "23b32e0fdd5998c2712549cbc39dff46c8754d55e3dd9f4d017d9e28de30cac6" dependencies = [ "actix-http", "actix-service", @@ -129,26 +132,25 @@ dependencies = [ [[package]] name = "actix-http" -version = "1.0.1" +version = "2.0.0-alpha.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c16664cc4fdea8030837ad5a845eb231fb93fc3c5c171edfefb52fad92ce9019" +checksum = "fd7ea0568480d199952a51de70271946da57c33cc0e8b83f54383e70958dff21" dependencies = [ "actix-codec", "actix-connect", "actix-rt", "actix-service", "actix-threadpool", + "actix-tls", "actix-utils", - "base64 0.11.0", + "base64 0.12.3", "bitflags", "brotli2", "bytes", - "chrono", "copyless", "derive_more", "either", "encoding_rs", - "failure", "flate2", "futures-channel", "futures-core", @@ -169,9 +171,9 @@ dependencies = [ "serde 1.0.114", "serde_json", "serde_urlencoded", - "sha1", + "sha-1", "slab", - "time", + "time 0.2.16", ] [[package]] @@ -273,9 +275,9 @@ dependencies = [ [[package]] name = "actix-tls" -version = "1.0.0" +version = "2.0.0-alpha.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4e5b4faaf105e9a6d389c606c298dcdb033061b00d532af9df56ff3a54995a8" +checksum = "dd2d9f3e70cbad0f06c6922950c5997ba0fd44c82e143d1c374023eb50457588" dependencies = [ "actix-codec", "actix-rt", @@ -285,6 +287,10 @@ dependencies = [ "either", "futures", "log", + "rustls", + "tokio-rustls", + "webpki", + "webpki-roots", ] [[package]] @@ -307,9 +313,9 @@ dependencies = [ [[package]] name = "actix-web" -version = "2.0.0" +version = "3.0.0-alpha.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3158e822461040822f0dbf1735b9c2ce1f95f93b651d7a7aded00b1efbb1f635" +checksum = "8bd6df56ec5f9a1a0d8335f156f36e1e8f76dbd736fa0cc0f6bc3a69be1e6124" dependencies = [ "actix-codec", "actix-http", @@ -327,25 +333,29 @@ dependencies = [ "bytes", "derive_more", "encoding_rs", - "futures", + "futures-channel", + "futures-core", + "futures-util", "fxhash", "log", "mime", - "net2", "pin-project", "regex", + "rustls", "serde 1.0.114", "serde_json", "serde_urlencoded", - "time", + "socket2", + "time 0.2.16", + "tinyvec", "url", ] [[package]] name = "actix-web-actors" -version = "2.0.0" +version = "3.0.0-alpha.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc1bd41bd66c4e9b5274cec87aac30168e63d64e96fd19db38edef6b46ba2982" +checksum = "2b5efeb3907582f9c724ce27be093ab8aafabd97be828bc6750c0d467f5e1aa3" dependencies = [ "actix", "actix-codec", @@ -408,7 +418,7 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b" dependencies = [ - "winapi 0.3.8", + "winapi 0.3.9", ] [[package]] @@ -448,7 +458,7 @@ checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" dependencies = [ "hermit-abi", "libc", - "winapi 0.3.8", + "winapi 0.3.9", ] [[package]] @@ -465,15 +475,15 @@ checksum = "f8aac770f1885fd7e387acedd76065302551364496e46b3dd00860b2f8359b9d" [[package]] name = "awc" -version = "1.0.1" +version = "2.0.0-alpha.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7601d4d1d7ef2335d6597a41b5fe069f6ab799b85f53565ab390e7b7065aac5" +checksum = "a7038a9747cd5159b9f0550895eaf865c0143baa7e4eee834e9294d0a7e0e4be" dependencies = [ "actix-codec", "actix-http", "actix-rt", "actix-service", - "base64 0.11.0", + "base64 0.12.3", "bytes", "derive_more", "futures-core", @@ -481,6 +491,7 @@ dependencies = [ "mime", "percent-encoding", "rand 0.7.3", + "rustls", "serde 1.0.114", "serde_json", "serde_urlencoded", @@ -500,6 +511,12 @@ dependencies = [ "rustc-demangle", ] +[[package]] +name = "base-x" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b20b618342cf9891c292c4f5ac2cde7287cc5c87e87e9c769d617793607dec1" + [[package]] name = "base64" version = "0.9.3" @@ -527,17 +544,17 @@ checksum = "b41b7ea54a0c9d92199de89e20e58d49f02f8e699814ef3fdf266f6f748d15c7" [[package]] name = "base64" -version = "0.12.2" +version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e223af0dc48c96d4f8342ec01a4974f139df863896b316681efd36742f22cc67" +checksum = "3441f0f7b02788e948e47f457ca01f1d7e6d92c693bc132c22b087d3141c03ff" [[package]] name = "bcrypt" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41b70db86f3c560199b0dada79a22b9a924622384abb2a756a9707ffcce077f2" +checksum = "6378bd17c4830c1b7ed644dde88f247b1560d46c68ff3da1b788984b09c0df31" dependencies = [ - "base64 0.12.2", + "base64 0.12.3", "blowfish", "byteorder", "getrandom", @@ -558,16 +575,25 @@ dependencies = [ "block-padding", "byte-tools", "byteorder", - "generic-array", + "generic-array 0.12.3", ] [[package]] -name = "block-cipher-trait" -version = "0.6.2" +name = "block-buffer" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c924d49bd09e7c06003acda26cd9742e796e34282ec6c1189404dee0c1f4774" +checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" dependencies = [ - "generic-array", + "generic-array 0.14.2", +] + +[[package]] +name = "block-cipher" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa136449e765dc7faa244561ccae839c394048667929af599b5d931ebe7b7f10" +dependencies = [ + "generic-array 0.14.2", ] [[package]] @@ -581,13 +607,13 @@ dependencies = [ [[package]] name = "blowfish" -version = "0.4.0" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6aeb80d00f2688459b8542068abd974cfb101e7a82182414a99b5026c0d85cc3" +checksum = "91d01392750dd899a2528948d6b856afe2df508d627fc7c339868c0bd0141b4b" dependencies = [ - "block-cipher-trait", + "block-cipher", "byteorder", - "opaque-debug", + "opaque-debug 0.2.3", ] [[package]] @@ -654,9 +680,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.0.54" +version = "1.0.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7bbb73db36c1246e9034e307d0fba23f9a2e251faa47ade70c1bd252220c8311" +checksum = "b1be3409f94d7bdceeb5f5fac551039d9b3f00e25da7a74fc4d33400a0d96368" [[package]] name = "cfg-if" @@ -673,7 +699,7 @@ dependencies = [ "num-integer", "num-traits 0.2.12", "serde 1.0.114", - "time", + "time 0.1.43", ] [[package]] @@ -751,6 +777,12 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b3a71ab494c0b5b860bdc8407ae08978052417070c2ced38573a9157ad75b8ac" +[[package]] +name = "cpuid-bool" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d375c433320f6c5057ae04a04376eef4d04ce2801448cf8863a78da99107be4" + [[package]] name = "crc32fast" version = "1.2.0" @@ -781,37 +813,6 @@ dependencies = [ "lazy_static", ] -[[package]] -name = "curl" -version = "0.4.30" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0447a642435be046540f042950d874a4907f9fee28c0513a0beb3ba89f91eb7" -dependencies = [ - "curl-sys", - "libc", - "openssl-probe", - "openssl-sys", - "schannel", - "socket2", - "winapi 0.3.8", -] - -[[package]] -name = "curl-sys" -version = "0.4.32+curl-7.70.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "834425a2f22fdd621434196965bf99fbfd9eaed96348488e27b7ac40736c560b" -dependencies = [ - "cc", - "libc", - "libnghttp2-sys", - "libz-sys", - "openssl-sys", - "pkg-config", - "vcpkg", - "winapi 0.3.8", -] - [[package]] name = "darling" version = "0.10.2" @@ -874,9 +875,9 @@ dependencies = [ [[package]] name = "derive_more" -version = "0.99.8" +version = "0.99.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc655351f820d774679da6cdc23355a93de496867d8203496675162e17b1d671" +checksum = "298998b1cf6b5b2c8a7b023dfd45821825ce3ba8a8af55c921a0e734e4653f76" dependencies = [ "proc-macro2", "quote", @@ -925,9 +926,24 @@ version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f3d0c8c8752312f9713efd397ff63acb9f85585afbf179282e720e7704954dd5" dependencies = [ - "generic-array", + "generic-array 0.12.3", ] +[[package]] +name = "digest" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" +dependencies = [ + "generic-array 0.14.2", +] + +[[package]] +name = "discard" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "212d0f5754cb6769937f4501cc0e67f4f4483c8d2c3e1e922ee9edbe4ab4c7c0" + [[package]] name = "dotenv" version = "0.15.0" @@ -957,7 +973,7 @@ dependencies = [ "encoding", "lazy_static", "rand 0.4.6", - "time", + "time 0.1.43", "version_check 0.1.5", ] @@ -1271,7 +1287,7 @@ dependencies = [ "libc", "log", "rustc_version", - "winapi 0.3.8", + "winapi 0.3.9", ] [[package]] @@ -1283,6 +1299,16 @@ dependencies = [ "typenum", ] +[[package]] +name = "generic-array" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac746a5f3bbfdadd6106868134545e684693d54d9d44f6e9588a7d54af0bf980" +dependencies = [ + "typenum", + "version_check 0.9.2", +] + [[package]] name = "getrandom" version = "0.1.14" @@ -1355,7 +1381,7 @@ checksum = "3c731c3e10504cc8ed35cfe2f1db4c9274c3d35fa486e3b31df46f068ef3e867" dependencies = [ "libc", "match_cfg", - "winapi 0.3.8", + "winapi 0.3.9", ] [[package]] @@ -1385,6 +1411,24 @@ dependencies = [ "thiserror", ] +[[package]] +name = "http-signature-normalization-actix" +version = "0.4.0-alpha.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c6efbc3e600cdd617585f4f15be3726c6942fb2eba3c8c79474c5d3159ad7c0" +dependencies = [ + "actix-http", + "actix-web", + "base64 0.12.3", + "bytes", + "chrono", + "futures", + "http-signature-normalization", + "log", + "sha2", + "thiserror", +] + [[package]] name = "httparse" version = "1.3.4" @@ -1443,35 +1487,10 @@ checksum = "f7e2f18aece9709094573a9f24f483c4f65caa4298e2f7ae1b71cc65d853fad7" dependencies = [ "socket2", "widestring", - "winapi 0.3.8", + "winapi 0.3.9", "winreg", ] -[[package]] -name = "isahc" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f54e7cf252df9a36605ccfabea2a754ad30c24b51b77f830486e555ac8e76bce" -dependencies = [ - "bytes", - "crossbeam-channel", - "crossbeam-utils", - "curl", - "curl-sys", - "encoding_rs", - "futures-channel", - "futures-io", - "futures-util", - "http", - "lazy_static", - "log", - "mime", - "slab", - "sluice", - "tracing", - "tracing-futures", -] - [[package]] name = "itertools" version = "0.9.0" @@ -1502,7 +1521,7 @@ version = "7.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b1f325ae57ddcf609f02d891486ce740f5bbd0cc3e93f9bffaacdf6594b21404" dependencies = [ - "base64 0.12.2", + "base64 0.12.3", "pem", "ring", "serde 1.0.114", @@ -1544,7 +1563,9 @@ dependencies = [ "actix-rt", "actix-web", "actix-web-actors", - "base64 0.12.2", + "async-trait", + "awc", + "base64 0.12.3", "bcrypt", "chrono", "comrak", @@ -1557,8 +1578,7 @@ dependencies = [ "futures", "htmlescape", "http", - "http-signature-normalization", - "isahc", + "http-signature-normalization-actix", "itertools", "jsonwebtoken", "lazy_static", @@ -1608,7 +1628,7 @@ dependencies = [ "email", "lettre", "mime", - "time", + "time 0.1.43", "uuid 0.7.4", ] @@ -1631,28 +1651,6 @@ version = "0.2.71" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9457b06509d27052635f90d6466700c65095fdf75409b3fbdd903e988b886f49" -[[package]] -name = "libnghttp2-sys" -version = "0.1.4+1.41.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03624ec6df166e79e139a2310ca213283d6b3c30810c54844f307086d4488df1" -dependencies = [ - "cc", - "libc", -] - -[[package]] -name = "libz-sys" -version = "1.0.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2eb5e43362e38e2bca2fd5f5134c4d4564a23a5c28e9b95411652021a8675ebe" -dependencies = [ - "cc", - "libc", - "pkg-config", - "vcpkg", -] - [[package]] name = "linked-hash-map" version = "0.3.0" @@ -1851,7 +1849,7 @@ checksum = "2ba7c918ac76704fb42afcbbb43891e72731f3dcca3bef2a19786297baf14af7" dependencies = [ "cfg-if", "libc", - "winapi 0.3.8", + "winapi 0.3.9", ] [[package]] @@ -1943,10 +1941,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2839e79665f131bdb5782e51f2c6c9599c133c6098982a54c794358bf432529c" [[package]] -name = "openssl" -version = "0.10.29" +name = "opaque-debug" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cee6d85f4cb4c4f59a6a85d5b68a233d280c82e29e822913b9c8b129fbf20bdd" +checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" + +[[package]] +name = "openssl" +version = "0.10.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d575eff3665419f9b83678ff2815858ad9d11567e082f5ac1814baba4e2bcb4" dependencies = [ "bitflags", "cfg-if", @@ -1996,7 +2000,7 @@ dependencies = [ "libc", "redox_syscall", "smallvec", - "winapi 0.3.8", + "winapi 0.3.9", ] [[package]] @@ -2005,7 +2009,7 @@ version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "59698ea79df9bf77104aefd39cc3ec990cb9693fb59c3b0a70ddf2646fdffb4b" dependencies = [ - "base64 0.12.2", + "base64 0.12.3", "once_cell", "regex", ] @@ -2179,7 +2183,7 @@ dependencies = [ "libc", "rand_core 0.3.1", "rdrand", - "winapi 0.3.8", + "winapi 0.3.9", ] [[package]] @@ -2198,7 +2202,7 @@ dependencies = [ "rand_os", "rand_pcg", "rand_xorshift", - "winapi 0.3.8", + "winapi 0.3.9", ] [[package]] @@ -2293,7 +2297,7 @@ checksum = "1166d5c91dc97b88d1decc3285bb0a99ed84b05cfd0bc2341bdf2d43fc41e39b" dependencies = [ "libc", "rand_core 0.4.2", - "winapi 0.3.8", + "winapi 0.3.9", ] [[package]] @@ -2307,7 +2311,7 @@ dependencies = [ "libc", "rand_core 0.4.2", "rdrand", - "winapi 0.3.8", + "winapi 0.3.9", ] [[package]] @@ -2368,7 +2372,7 @@ version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" dependencies = [ - "winapi 0.3.8", + "winapi 0.3.9", ] [[package]] @@ -2393,7 +2397,7 @@ dependencies = [ "spin", "untrusted", "web-sys", - "winapi 0.3.8", + "winapi 0.3.9", ] [[package]] @@ -2421,6 +2425,19 @@ dependencies = [ "semver", ] +[[package]] +name = "rustls" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0d4a31f5d68413404705d6982529b0e11a9aacd4839d1d6222ee3b8cb4015e1" +dependencies = [ + "base64 0.11.0", + "log", + "ring", + "sct", + "webpki", +] + [[package]] name = "ryu" version = "1.0.5" @@ -2440,7 +2457,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f05ba609c234e60bee0d547fe94a4c7e9da733d1c962cf6e59efa4cd9c8bc75" dependencies = [ "lazy_static", - "winapi 0.3.8", + "winapi 0.3.9", ] [[package]] @@ -2464,6 +2481,16 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" +[[package]] +name = "sct" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3042af939fca8c3453b7af0f1c66e533a15a86169e39de2657310ade8f98d3c" +dependencies = [ + "ring", + "untrusted", +] + [[package]] name = "security-framework" version = "0.4.4" @@ -2580,10 +2607,10 @@ version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f7d94d0bede923b3cea61f3f1ff57ff8cdfd77b400fb8f9998949e0cf04163df" dependencies = [ - "block-buffer", - "digest", + "block-buffer 0.7.3", + "digest 0.8.1", "fake-simd", - "opaque-debug", + "opaque-debug 0.2.3", ] [[package]] @@ -2594,14 +2621,15 @@ checksum = "2579985fda508104f7587689507983eadd6a6e84dd35d6d115361f530916fa0d" [[package]] name = "sha2" -version = "0.8.2" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a256f46ea78a0c0d9ff00077504903ac881a1dafdc20da66545699e7776b3e69" +checksum = "2933378ddfeda7ea26f48c555bdad8bb446bf8a3d17832dc83e380d444cfb8c1" dependencies = [ - "block-buffer", - "digest", - "fake-simd", - "opaque-debug", + "block-buffer 0.9.0", + "cfg-if", + "cpuid-bool", + "digest 0.9.0", + "opaque-debug 0.3.0", ] [[package]] @@ -2631,18 +2659,6 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c111b5bd5695e56cffe5129854aa230b39c93a305372fdbb2668ca2394eea9f8" -[[package]] -name = "sluice" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fed13b7cb46f13a15db2c4740f087a848acc8b31af89f95844d40137451f89b1" -dependencies = [ - "futures-channel", - "futures-core", - "futures-io", - "futures-util", -] - [[package]] name = "smallvec" version = "1.4.0" @@ -2658,7 +2674,7 @@ dependencies = [ "cfg-if", "libc", "redox_syscall", - "winapi 0.3.8", + "winapi 0.3.9", ] [[package]] @@ -2667,12 +2683,70 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" +[[package]] +name = "standback" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0437cfb83762844799a60e1e3b489d5ceb6a650fbacb86437badc1b6d87b246" +dependencies = [ + "version_check 0.9.2", +] + [[package]] name = "static_assertions" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" +[[package]] +name = "stdweb" +version = "0.4.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d022496b16281348b52d0e30ae99e01a73d737b2f45d38fed4edf79f9325a1d5" +dependencies = [ + "discard", + "rustc_version", + "stdweb-derive", + "stdweb-internal-macros", + "stdweb-internal-runtime", + "wasm-bindgen", +] + +[[package]] +name = "stdweb-derive" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c87a60a40fccc84bef0652345bbbbbe20a605bf5d0ce81719fc476f5c03b50ef" +dependencies = [ + "proc-macro2", + "quote", + "serde 1.0.114", + "serde_derive", + "syn", +] + +[[package]] +name = "stdweb-internal-macros" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58fa5ff6ad0d98d1ffa8cb115892b6e69d67799f6763e162a1c9db421dc22e11" +dependencies = [ + "base-x", + "proc-macro2", + "quote", + "serde 1.0.114", + "serde_derive", + "serde_json", + "sha1", + "syn", +] + +[[package]] +name = "stdweb-internal-runtime" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "213701ba3370744dcd1a12960caa4843b3d68b4d1c0a5d575e0d65b2ee9d16c0" + [[package]] name = "strsim" version = "0.8.0" @@ -2737,7 +2811,7 @@ dependencies = [ "rand 0.7.3", "redox_syscall", "remove_dir_all", - "winapi 0.3.8", + "winapi 0.3.9", ] [[package]] @@ -2803,7 +2877,45 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ca8a50ef2360fbd1eeb0ecd46795a87a19024eb4b53c5dc916ca1fd95fe62438" dependencies = [ "libc", - "winapi 0.3.8", + "winapi 0.3.9", +] + +[[package]] +name = "time" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a51cadc5b1eec673a685ff7c33192ff7b7603d0b75446fb354939ee615acb15" +dependencies = [ + "cfg-if", + "libc", + "standback", + "stdweb", + "time-macros", + "version_check 0.9.2", + "winapi 0.3.9", +] + +[[package]] +name = "time-macros" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ae9b6e9f095bc105e183e3cd493d72579be3181ad4004fceb01adbe9eecab2d" +dependencies = [ + "proc-macro-hack", + "time-macros-impl", +] + +[[package]] +name = "time-macros-impl" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5c3be1edfad6027c69f5491cf4cb310d1a71ecd6af742788c6ff8bced86b8fa" +dependencies = [ + "proc-macro-hack", + "proc-macro2", + "quote", + "standback", + "syn", ] [[package]] @@ -2830,7 +2942,19 @@ dependencies = [ "pin-project-lite", "signal-hook-registry", "slab", - "winapi 0.3.8", + "winapi 0.3.9", +] + +[[package]] +name = "tokio-rustls" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15cb62a0d2770787abc96e99c1cd98fcf17f94959f3af63ca85bdfb203f051b4" +dependencies = [ + "futures-core", + "rustls", + "tokio", + "webpki", ] [[package]] @@ -2855,82 +2979,41 @@ checksum = "be8242891f2b6cbef26a2d7e8605133c2c554cd35b3e4948ea892d6d68436499" dependencies = [ "bytes", "futures-core", + "futures-io", "futures-sink", "log", "pin-project-lite", "tokio", ] -[[package]] -name = "tracing" -version = "0.1.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a41f40ed0e162c911ac6fcb53ecdc8134c46905fdbbae8c50add462a538b495f" -dependencies = [ - "cfg-if", - "log", - "tracing-attributes", - "tracing-core", -] - -[[package]] -name = "tracing-attributes" -version = "0.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99bbad0de3fd923c9c3232ead88510b783e5a4d16a6154adffa3d53308de984c" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "tracing-core" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0aa83a9a47081cd522c09c81b31aec2c9273424976f922ad61c053b58350b715" -dependencies = [ - "lazy_static", -] - -[[package]] -name = "tracing-futures" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab7bb6f14721aa00656086e9335d363c5c8747bae02ebe32ea2c7dece5689b4c" -dependencies = [ - "pin-project", - "tracing", -] - [[package]] name = "trust-dns-proto" -version = "0.18.0-alpha.2" +version = "0.19.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a7f3a2ab8a919f5eca52a468866a67ed7d3efa265d48a652a9a3452272b413f" +checksum = "cdd7061ba6f4d4d9721afedffbfd403f20f39a4301fee1b70d6fcd09cca69f28" dependencies = [ "async-trait", + "backtrace", "enum-as-inner", - "failure", "futures", "idna", "lazy_static", "log", "rand 0.7.3", "smallvec", - "socket2", + "thiserror", "tokio", "url", ] [[package]] name = "trust-dns-resolver" -version = "0.18.0-alpha.2" +version = "0.19.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f90b1502b226f8b2514c6d5b37bafa8c200d7ca4102d57dc36ee0f3b7a04a2f" +checksum = "0f23cdfdc3d8300b3c50c9e84302d3bd6d860fb9529af84ace6cf9665f181b77" dependencies = [ + "backtrace", "cfg-if", - "failure", "futures", "ipconfig", "lazy_static", @@ -2938,6 +3021,7 @@ dependencies = [ "lru-cache", "resolv-conf", "smallvec", + "thiserror", "tokio", "trust-dns-proto", ] @@ -3028,9 +3112,9 @@ checksum = "caaa9d531767d1ff2150b9332433f32a24622147e5ebb1f26409d5da67afd479" [[package]] name = "unicode-xid" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "826e7639553986605ec5979c7dd957c7895e93eabed50ab2ffa7f6128a75097c" +checksum = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564" [[package]] name = "unicode_categories" @@ -3200,6 +3284,25 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "webpki" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab146130f5f790d45f82aeeb09e55a256573373ec64409fc19a6fb82fb1032ae" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "webpki-roots" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8eff4b7516a57307f9349c64bf34caa34b940b66fed4b2fb3136cb7386e5739" +dependencies = [ + "webpki", +] + [[package]] name = "widestring" version = "0.4.2" @@ -3214,9 +3317,9 @@ checksum = "167dc9d6949a9b857f3451275e911c3f44255842c1f7a76f33c55103a909087a" [[package]] name = "winapi" -version = "0.3.8" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8093091eeb260906a183e6ae1abdba2ef5ef2257a21801128899c3fc699229c6" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" dependencies = [ "winapi-i686-pc-windows-gnu", "winapi-x86_64-pc-windows-gnu", @@ -3240,7 +3343,7 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" dependencies = [ - "winapi 0.3.8", + "winapi 0.3.9", ] [[package]] @@ -3255,7 +3358,7 @@ version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b2986deb581c4fe11b621998a5e53361efe6b48a151178d0cd9eeffa4dc6acc9" dependencies = [ - "winapi 0.3.8", + "winapi 0.3.9", ] [[package]] @@ -3264,7 +3367,7 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7daf138b6b14196e3830a588acf1e86966c694d3e8fb026fb105b8b5dca07e6e" dependencies = [ - "winapi 0.3.8", + "winapi 0.3.9", ] [[package]] diff --git a/server/Cargo.toml b/server/Cargo.toml index df6c90afd..225079940 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -19,11 +19,12 @@ chrono = { version = "0.4.7", features = ["serde"] } serde_json = { version = "1.0.52", features = ["preserve_order"]} failure = "0.1.8" serde = { version = "1.0.105", features = ["derive"] } -actix = "0.9.0" -actix-web = "2.0.0" -actix-files = "0.2.1" -actix-web-actors = "2.0.0" +actix = "0.10.0-alpha.2" +actix-web = { version = "3.0.0-alpha.3", features = ["rustls"] } +actix-files = "0.3.0-alpha.1" +actix-web-actors = "3.0.0-alpha.1" actix-rt = "1.1.1" +awc = "2.0.0-alpha.2" log = "0.4.0" env_logger = "0.7.1" rand = "0.7.3" @@ -34,19 +35,19 @@ regex = "1.3.5" lazy_static = "1.3.0" lettre = "0.9.3" lettre_email = "0.9.4" -sha2 = "0.8.1" rss = "1.9.0" htmlescape = "0.3.1" url = { version = "2.1.1", features = ["serde"] } config = {version = "0.10.1", default-features = false, features = ["hjson"] } percent-encoding = "2.1.0" -isahc = "0.9.2" comrak = "0.7" openssl = "0.10" 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" tokio = "0.2.21" futures = "0.3.5" itertools = "0.9.0" uuid = { version = "0.8", features = ["serde", "v4"] } +sha2 = "0.9" +async-trait = "0.1.36" diff --git a/server/src/api/comment.rs b/server/src/api/comment.rs index 562174587..c7406b370 100644 --- a/server/src/api/comment.rs +++ b/server/src/api/comment.rs @@ -1,6 +1,7 @@ use crate::{ api::{APIError, Oper, Perform}, apub::{ApubLikeableType, ApubObjectType}, + blocking, db::{ comment::*, comment_view::*, @@ -27,13 +28,10 @@ use crate::{ UserOperation, WebsocketInfo, }, + DbPool, + LemmyError, MentionData, }; -use diesel::{ - r2d2::{ConnectionManager, Pool}, - PgConnection, -}; -use failure::Error; use log::error; use serde::{Deserialize, Serialize}; use std::str::FromStr; @@ -97,14 +95,15 @@ pub struct GetCommentsResponse { comments: Vec, } +#[async_trait::async_trait(?Send)] impl Perform for Oper { type Response = CommentResponse; - fn perform( + async fn perform( &self, - pool: Pool>, + pool: &DbPool, websocket_info: Option, - ) -> Result { + ) -> Result { let data: &CreateComment = &self.data; let claims = match Claims::decode(&data.auth) { @@ -114,20 +113,6 @@ impl Perform for Oper { 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 comment_form = CommentForm { @@ -144,21 +129,48 @@ impl Perform for Oper { 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, + Err(_e) => return Err(APIError::err("couldnt_create_comment").into()), + }; + + 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, Err(_e) => return Err(APIError::err("couldnt_create_comment").into()), }; - let updated_comment = match Comment::update_ap_id(&conn, inserted_comment.id) { - Ok(comment) => comment, - Err(_e) => return Err(APIError::err("couldnt_create_comment").into()), - }; - - updated_comment.send_create(&user, &conn)?; + updated_comment + .send_create(&user, &self.client, pool) + .await?; // Scan the comment for user mentions, add those rows 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 let like_form = CommentLikeForm { @@ -168,14 +180,17 @@ impl Perform for Oper { score: 1, }; - let _inserted_like = match CommentLike::like(&conn, &like_form) { - Ok(like) => like, - Err(_e) => return Err(APIError::err("couldnt_like_comment").into()), - }; + let like = move |conn: &'_ _| CommentLike::like(&conn, &like_form); + if blocking(pool, like).await?.is_err() { + 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 { comment: comment_view, @@ -198,14 +213,15 @@ impl Perform for Oper { } } +#[async_trait::async_trait(?Send)] impl Perform for Oper { type Response = CommentResponse; - fn perform( + async fn perform( &self, - pool: Pool>, + pool: &DbPool, websocket_info: Option, - ) -> Result { + ) -> Result { let data: &EditComment = &self.data; let claims = match Claims::decode(&data.auth) { @@ -215,30 +231,44 @@ impl Perform for Oper { 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 orig_comment = CommentView::read(&conn, data.edit_id, None)?; + let edit_id = data.edit_id; + let orig_comment = + 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. if data.read.is_none() { // Verify its the creator or a mod, or an admin let mut editors: Vec = vec![data.creator_id]; + let community_id = orig_comment.community_id; editors.append( - &mut CommunityModeratorView::for_community(&conn, orig_comment.community_id)? - .into_iter() - .map(|m| m.user_id) - .collect(), + &mut blocking(pool, move |conn| { + Ok( + CommunityModeratorView::for_community(&conn, community_id)? + .into_iter() + .map(|m| m.user_id) + .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) { return Err(APIError::err("no_comment_edit_allowed").into()); } // 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()); } @@ -250,7 +280,8 @@ impl Perform for Oper { 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 { content: content_slurs_removed, @@ -270,31 +301,48 @@ impl Perform for Oper { 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, Err(_e) => return Err(APIError::err("couldnt_update_comment").into()), }; if let Some(deleted) = data.deleted.to_owned() { if deleted { - updated_comment.send_delete(&user, &conn)?; + updated_comment + .send_delete(&user, &self.client, pool) + .await?; } 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() { if removed { - updated_comment.send_remove(&user, &conn)?; + updated_comment + .send_remove(&user, &self.client, pool) + .await?; } else { - updated_comment.send_undo_remove(&user, &conn)?; + updated_comment + .send_undo_remove(&user, &self.client, pool) + .await?; } } 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 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 if let Some(removed) = data.removed.to_owned() { @@ -304,10 +352,14 @@ impl Perform for Oper { removed: Some(removed), 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 { comment: comment_view, @@ -330,14 +382,15 @@ impl Perform for Oper { } } +#[async_trait::async_trait(?Send)] impl Perform for Oper { type Response = CommentResponse; - fn perform( + async fn perform( &self, - pool: Pool>, + pool: &DbPool, _websocket_info: Option, - ) -> Result { + ) -> Result { let data: &SaveComment = &self.data; let claims = match Claims::decode(&data.auth) { @@ -352,21 +405,23 @@ impl Perform for Oper { user_id, }; - let conn = pool.get()?; - if data.save { - match CommentSaved::save(&conn, &comment_saved_form) { - Ok(comment) => comment, - Err(_e) => return Err(APIError::err("couldnt_save_comment").into()), - }; + let save_comment = move |conn: &'_ _| CommentSaved::save(conn, &comment_saved_form); + if blocking(pool, save_comment).await?.is_err() { + return Err(APIError::err("couldnt_save_comment").into()); + } } else { - match CommentSaved::unsave(&conn, &comment_saved_form) { - Ok(comment) => comment, - Err(_e) => return Err(APIError::err("couldnt_save_comment").into()), - }; + let unsave_comment = move |conn: &'_ _| CommentSaved::unsave(conn, &comment_saved_form); + if blocking(pool, unsave_comment).await?.is_err() { + 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 { comment: comment_view, @@ -375,14 +430,15 @@ impl Perform for Oper { } } +#[async_trait::async_trait(?Send)] impl Perform for Oper { type Response = CommentResponse; - fn perform( + async fn perform( &self, - pool: Pool>, + pool: &DbPool, websocket_info: Option, - ) -> Result { + ) -> Result { let data: &CreateCommentLike = &self.data; let claims = match Claims::decode(&data.auth) { @@ -394,36 +450,42 @@ impl Perform for Oper { let mut recipient_ids = Vec::new(); - let conn = pool.get()?; - // Don't do a downvote if site has downvotes disabled if data.score == -1 { - let site = SiteView::read(&conn)?; + let site = blocking(pool, move |conn| SiteView::read(conn)).await??; if !site.enable_downvotes { return Err(APIError::err("downvotes_disabled").into()); } } // Check for a community ban - let post = Post::read(&conn, data.post_id)?; - if CommunityUserBanView::get(&conn, user_id, post.community_id).is_ok() { + 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 = User_::read(&conn, user_id)?; + let user = blocking(pool, move |conn| User_::read(conn, user_id)).await??; if user.banned { 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 match comment.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 { - 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); } } @@ -440,27 +502,33 @@ impl Perform for Oper { }; // 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 let do_add = like_form.score != 0 && (like_form.score == 1 || like_form.score == -1); if do_add { - let _inserted_like = match CommentLike::like(&conn, &like_form) { - Ok(like) => like, - Err(_e) => return Err(APIError::err("couldnt_like_comment").into()), - }; + let like_form2 = like_form.clone(); + let like = move |conn: &'_ _| CommentLike::like(conn, &like_form2); + if blocking(pool, like).await?.is_err() { + return Err(APIError::err("couldnt_like_comment").into()); + } if like_form.score == 1 { - comment.send_like(&user, &conn)?; + comment.send_like(&user, &self.client, pool).await?; } else if like_form.score == -1 { - comment.send_dislike(&user, &conn)?; + comment.send_dislike(&user, &self.client, pool).await?; } } 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 - 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 { comment: liked_comment, @@ -483,14 +551,15 @@ impl Perform for Oper { } } +#[async_trait::async_trait(?Send)] impl Perform for Oper { type Response = GetCommentsResponse; - fn perform( + async fn perform( &self, - pool: Pool>, + pool: &DbPool, websocket_info: Option, - ) -> Result { + ) -> Result { let data: &GetComments = &self.data; let user_claims: Option = match &data.auth { @@ -509,19 +578,23 @@ impl Perform for Oper { let type_ = ListingType::from_str(&data.type_)?; let sort = SortType::from_str(&data.sort)?; - let conn = pool.get()?; - - let comments = match CommentQueryBuilder::create(&conn) - .listing_type(type_) - .sort(&sort) - .for_community_id(data.community_id) - .my_user_id(user_id) - .page(data.page) - .limit(data.limit) - .list() - { + let community_id = data.community_id; + let page = data.page; + let limit = data.limit; + let comments = blocking(pool, move |conn| { + CommentQueryBuilder::create(conn) + .listing_type(type_) + .sort(&sort) + .for_community_id(community_id) + .my_user_id(user_id) + .page(page) + .limit(limit) + .list() + }) + .await?; + let comments = match 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 { @@ -542,8 +615,23 @@ impl Perform for Oper { } } -pub fn send_local_notifs( - conn: &PgConnection, +pub async fn send_local_notifs( + mentions: Vec, + comment: Comment, + user: User_, + post: Post, + pool: &DbPool, +) -> Result, 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], comment: &Comment, user: &User_, diff --git a/server/src/api/community.rs b/server/src/api/community.rs index 3fc67eb34..02071c577 100644 --- a/server/src/api/community.rs +++ b/server/src/api/community.rs @@ -7,6 +7,7 @@ use crate::{ ActorType, EndpointType, }, + blocking, db::{Bannable, Crud, Followable, Joinable, SortType}, is_valid_community_name, naive_from_unix, @@ -18,12 +19,9 @@ use crate::{ UserOperation, WebsocketInfo, }, + DbPool, + LemmyError, }; -use diesel::{ - r2d2::{ConnectionManager, Pool}, - PgConnection, -}; -use failure::Error; use serde::{Deserialize, Serialize}; use std::str::FromStr; @@ -138,14 +136,15 @@ pub struct TransferCommunity { auth: String, } +#[async_trait::async_trait(?Send)] impl Perform for Oper { type Response = GetCommunityResponse; - fn perform( + async fn perform( &self, - pool: Pool>, + pool: &DbPool, websocket_info: Option, - ) -> Result { + ) -> Result { let data: &GetCommunity = &self.data; let user_id: Option = match &data.auth { @@ -159,33 +158,38 @@ impl Perform for Oper { None => None, }; - let conn = pool.get()?; - + let name = data.name.to_owned().unwrap_or_else(|| "main".to_string()); let community = match data.id { - Some(id) => Community::read(&conn, id)?, - None => { - match Community::read_from_name( - &conn, - &data.name.to_owned().unwrap_or_else(|| "main".to_string()), - ) { - Ok(community) => community, - Err(_e) => return Err(APIError::err("couldnt_find_community").into()), - } - } + Some(id) => blocking(pool, move |conn| Community::read(conn, id)).await??, + None => match blocking(pool, move |conn| Community::read_from_name(conn, &name)).await? { + Ok(community) => community, + Err(_e) => return Err(APIError::err("couldnt_find_community").into()), + }, }; - let community_view = match CommunityView::read(&conn, community.id, user_id) { + let community_id = community.id; + let community_view = match blocking(pool, move |conn| { + CommunityView::read(conn, community_id, user_id) + }) + .await? + { Ok(community) => community, 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 = match blocking(pool, move |conn| { + CommunityModeratorView::for_community(conn, community_id) + }) + .await? + { Ok(moderators) => moderators, Err(_e) => return Err(APIError::err("couldnt_find_community").into()), }; - let site_creator_id = Site::read(&conn, 1)?.creator_id; - let mut admins = UserView::admins(&conn)?; + let site = blocking(pool, move |conn| Site::read(conn, 1)).await??; + 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_user = admins.remove(creator_index); admins.insert(0, creator_user); @@ -220,14 +224,15 @@ impl Perform for Oper { } } +#[async_trait::async_trait(?Send)] impl Perform for Oper { type Response = CommunityResponse; - fn perform( + async fn perform( &self, - pool: Pool>, + pool: &DbPool, _websocket_info: Option, - ) -> Result { + ) -> Result { let data: &CreateCommunity = &self.data; let claims = match Claims::decode(&data.auth) { @@ -255,10 +260,9 @@ impl Perform for Oper { let user_id = claims.id; - let conn = pool.get()?; - // 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()); } @@ -283,34 +287,36 @@ impl Perform for Oper { published: None, }; - let inserted_community = match Community::create(&conn, &community_form) { - Ok(community) => community, - Err(_e) => return Err(APIError::err("community_already_exists").into()), - }; + let inserted_community = + match blocking(pool, move |conn| Community::create(conn, &community_form)).await? { + Ok(community) => community, + Err(_e) => return Err(APIError::err("community_already_exists").into()), + }; let community_moderator_form = CommunityModeratorForm { community_id: inserted_community.id, user_id, }; - let _inserted_community_moderator = - match CommunityModerator::join(&conn, &community_moderator_form) { - Ok(user) => user, - Err(_e) => return Err(APIError::err("community_moderator_already_exists").into()), - }; + let join = move |conn: &'_ _| CommunityModerator::join(conn, &community_moderator_form); + if blocking(pool, join).await?.is_err() { + return Err(APIError::err("community_moderator_already_exists").into()); + } let community_follower_form = CommunityFollowerForm { community_id: inserted_community.id, user_id, }; - let _inserted_community_follower = - match CommunityFollower::follow(&conn, &community_follower_form) { - Ok(user) => user, - Err(_e) => return Err(APIError::err("community_follower_already_exists").into()), - }; + let follow = move |conn: &'_ _| CommunityFollower::follow(conn, &community_follower_form); + if blocking(pool, follow).await?.is_err() { + 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 { community: community_view, @@ -318,14 +324,15 @@ impl Perform for Oper { } } +#[async_trait::async_trait(?Send)] impl Perform for Oper { type Response = CommunityResponse; - fn perform( + async fn perform( &self, - pool: Pool>, + pool: &DbPool, websocket_info: Option, - ) -> Result { + ) -> Result { let data: &EditCommunity = &self.data; if let Err(slurs) = slur_check(&data.name) { @@ -353,28 +360,34 @@ impl Perform for Oper { let user_id = claims.id; - let conn = pool.get()?; - // 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 { return Err(APIError::err("site_ban").into()); } // Verify its a mod + let edit_id = data.edit_id; let mut editors: Vec = Vec::new(); editors.append( - &mut CommunityModeratorView::for_community(&conn, data.edit_id)? - .into_iter() - .map(|m| m.user_id) - .collect(), + &mut blocking(pool, move |conn| { + CommunityModeratorView::for_community(conn, edit_id) + .map(|v| v.into_iter().map(|m| m.user_id).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) { 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 { name: data.name.to_owned(), @@ -394,7 +407,12 @@ impl Perform for Oper { 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, Err(_e) => return Err(APIError::err("couldnt_update_community").into()), }; @@ -412,24 +430,36 @@ impl Perform for Oper { reason: data.reason.to_owned(), expires, }; - ModRemoveCommunity::create(&conn, &form)?; + blocking(pool, move |conn| ModRemoveCommunity::create(conn, &form)).await??; } if let Some(deleted) = data.deleted.to_owned() { if deleted { - updated_community.send_delete(&user, &conn)?; + updated_community + .send_delete(&user, &self.client, pool) + .await?; } 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() { if removed { - updated_community.send_remove(&user, &conn)?; + updated_community + .send_remove(&user, &self.client, pool) + .await?; } 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 { community: community_view, @@ -453,14 +483,15 @@ impl Perform for Oper { } } +#[async_trait::async_trait(?Send)] impl Perform for Oper { type Response = ListCommunitiesResponse; - fn perform( + async fn perform( &self, - pool: Pool>, + pool: &DbPool, _websocket_info: Option, - ) -> Result { + ) -> Result { let data: &ListCommunities = &self.data; let user_claims: Option = match &data.auth { @@ -483,29 +514,33 @@ impl Perform for Oper { let sort = SortType::from_str(&data.sort)?; - let conn = pool.get()?; - - let communities = CommunityQueryBuilder::create(&conn) - .sort(&sort) - .for_user(user_id) - .show_nsfw(show_nsfw) - .page(data.page) - .limit(data.limit) - .list()?; + let page = data.page; + let limit = data.limit; + let communities = blocking(pool, move |conn| { + CommunityQueryBuilder::create(conn) + .sort(&sort) + .for_user(user_id) + .show_nsfw(show_nsfw) + .page(page) + .limit(limit) + .list() + }) + .await??; // Return the jwt Ok(ListCommunitiesResponse { communities }) } } +#[async_trait::async_trait(?Send)] impl Perform for Oper { type Response = CommunityResponse; - fn perform( + async fn perform( &self, - pool: Pool>, + pool: &DbPool, _websocket_info: Option, - ) -> Result { + ) -> Result { let data: &FollowCommunity = &self.data; let claims = match Claims::decode(&data.auth) { @@ -515,9 +550,8 @@ impl Perform for Oper { let user_id = claims.id; - let conn = pool.get()?; - - let community = Community::read(&conn, data.community_id)?; + let community_id = data.community_id; + let community = blocking(pool, move |conn| Community::read(conn, community_id)).await??; let community_follower_form = CommunityFollowerForm { community_id: data.community_id, user_id, @@ -525,34 +559,44 @@ impl Perform for Oper { if community.local { if data.follow { - match CommunityFollower::follow(&conn, &community_follower_form) { - Ok(user) => user, - Err(_e) => return Err(APIError::err("community_follower_already_exists").into()), - }; + let follow = move |conn: &'_ _| CommunityFollower::follow(conn, &community_follower_form); + if blocking(pool, follow).await?.is_err() { + 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()), - }; + 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 = User_::read(&conn, user_id)?; + let user = blocking(pool, move |conn| User_::read(conn, user_id)).await??; if data.follow { // Dont actually add to the community followers here, because you need // to wait for the accept - user.send_follow(&community.actor_id, &conn)?; + user + .send_follow(&community.actor_id, &self.client, pool) + .await?; } else { - user.send_unfollow(&community.actor_id, &conn)?; - match CommunityFollower::unfollow(&conn, &community_follower_form) { - Ok(user) => user, - Err(_e) => return Err(APIError::err("community_follower_already_exists").into()), - }; + user + .send_unfollow(&community.actor_id, &self.client, pool) + .await?; + 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 } - 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 { community: community_view, @@ -560,14 +604,15 @@ impl Perform for Oper { } } +#[async_trait::async_trait(?Send)] impl Perform for Oper { type Response = GetFollowedCommunitiesResponse; - fn perform( + async fn perform( &self, - pool: Pool>, + pool: &DbPool, _websocket_info: Option, - ) -> Result { + ) -> Result { let data: &GetFollowedCommunities = &self.data; let claims = match Claims::decode(&data.auth) { @@ -577,27 +622,29 @@ impl Perform for Oper { let user_id = claims.id; - let conn = pool.get()?; - - let communities: Vec = - match CommunityFollowerView::for_user(&conn, user_id) { - Ok(communities) => communities, - Err(_e) => return Err(APIError::err("system_err_login").into()), - }; + let communities = match blocking(pool, move |conn| { + CommunityFollowerView::for_user(conn, user_id) + }) + .await? + { + Ok(communities) => communities, + _ => return Err(APIError::err("system_err_login").into()), + }; // Return the jwt Ok(GetFollowedCommunitiesResponse { communities }) } } +#[async_trait::async_trait(?Send)] impl Perform for Oper { type Response = BanFromCommunityResponse; - fn perform( + async fn perform( &self, - pool: Pool>, + pool: &DbPool, websocket_info: Option, - ) -> Result { + ) -> Result { let data: &BanFromCommunity = &self.data; let claims = match Claims::decode(&data.auth) { @@ -612,18 +659,16 @@ impl Perform for Oper { user_id: data.user_id, }; - let conn = pool.get()?; - if data.ban { - match CommunityUserBan::ban(&conn, &community_user_ban_form) { - Ok(user) => user, - Err(_e) => return Err(APIError::err("community_user_already_banned").into()), - }; + let ban = move |conn: &'_ _| CommunityUserBan::ban(conn, &community_user_ban_form); + if blocking(pool, ban).await?.is_err() { + return Err(APIError::err("community_user_already_banned").into()); + } } else { - match CommunityUserBan::unban(&conn, &community_user_ban_form) { - Ok(user) => user, - Err(_e) => return Err(APIError::err("community_user_already_banned").into()), - }; + let unban = move |conn: &'_ _| CommunityUserBan::unban(conn, &community_user_ban_form); + if blocking(pool, unban).await?.is_err() { + return Err(APIError::err("community_user_already_banned").into()); + } } // Mod tables @@ -640,9 +685,10 @@ impl Perform for Oper { banned: Some(data.ban), 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 { user: user_view, @@ -662,14 +708,15 @@ impl Perform for Oper { } } +#[async_trait::async_trait(?Send)] impl Perform for Oper { type Response = AddModToCommunityResponse; - fn perform( + async fn perform( &self, - pool: Pool>, + pool: &DbPool, websocket_info: Option, - ) -> Result { + ) -> Result { let data: &AddModToCommunity = &self.data; let claims = match Claims::decode(&data.auth) { @@ -684,18 +731,16 @@ impl Perform for Oper { user_id: data.user_id, }; - let conn = pool.get()?; - if data.added { - match CommunityModerator::join(&conn, &community_moderator_form) { - Ok(user) => user, - Err(_e) => return Err(APIError::err("community_moderator_already_exists").into()), - }; + let join = move |conn: &'_ _| CommunityModerator::join(conn, &community_moderator_form); + if blocking(pool, join).await?.is_err() { + return Err(APIError::err("community_moderator_already_exists").into()); + } } else { - match CommunityModerator::leave(&conn, &community_moderator_form) { - Ok(user) => user, - Err(_e) => return Err(APIError::err("community_moderator_already_exists").into()), - }; + let leave = move |conn: &'_ _| CommunityModerator::leave(conn, &community_moderator_form); + if blocking(pool, leave).await?.is_err() { + return Err(APIError::err("community_moderator_already_exists").into()); + } } // Mod tables @@ -705,9 +750,13 @@ impl Perform for Oper { community_id: data.community_id, 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 }; @@ -724,14 +773,15 @@ impl Perform for Oper { } } +#[async_trait::async_trait(?Send)] impl Perform for Oper { type Response = GetCommunityResponse; - fn perform( + async fn perform( &self, - pool: Pool>, + pool: &DbPool, _websocket_info: Option, - ) -> Result { + ) -> Result { let data: &TransferCommunity = &self.data; let claims = match Claims::decode(&data.auth) { @@ -741,12 +791,14 @@ impl Perform for Oper { 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_user = admins.remove(creator_index); admins.insert(0, creator_user); @@ -774,13 +826,18 @@ impl Perform for Oper { published: None, }; - let _updated_community = match Community::update(&conn, data.community_id, &community_form) { - Ok(community) => community, - Err(_e) => return Err(APIError::err("couldnt_update_community").into()), + let community_id = data.community_id; + let update = move |conn: &'_ _| Community::update(conn, community_id, &community_form); + 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. - 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 .iter() .position(|r| r.user_id == data.user_id) @@ -788,19 +845,23 @@ impl Perform for Oper { let creator_user = community_mods.remove(creator_index); 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 { let community_moderator_form = CommunityModeratorForm { community_id: cmod.community_id, user_id: cmod.user_id, }; - let _inserted_community_moderator = - match CommunityModerator::join(&conn, &community_moderator_form) { - Ok(user) => user, - Err(_e) => return Err(APIError::err("community_moderator_already_exists").into()), - }; + let join = move |conn: &'_ _| CommunityModerator::join(conn, &community_moderator_form); + if blocking(pool, join).await?.is_err() { + return Err(APIError::err("community_moderator_already_exists").into()); + } } // Mod tables @@ -810,14 +871,24 @@ impl Perform for Oper { community_id: data.community_id, 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, 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, Err(_e) => return Err(APIError::err("couldnt_find_community").into()), }; diff --git a/server/src/api/mod.rs b/server/src/api/mod.rs index afd62aff8..6df9909c5 100644 --- a/server/src/api/mod.rs +++ b/server/src/api/mod.rs @@ -1,12 +1,10 @@ use crate::{ db::{community::*, community_view::*, moderator::*, site::*, user::*, user_view::*}, websocket::WebsocketInfo, + DbPool, + LemmyError, }; -use diesel::{ - r2d2::{ConnectionManager, Pool}, - PgConnection, -}; -use failure::Error; +use actix_web::client::Client; pub mod comment; pub mod community; @@ -30,20 +28,22 @@ impl APIError { pub struct Oper { data: T, + client: Client, } impl Oper { - pub fn new(data: Data) -> Oper { - Oper { data } + pub fn new(data: Data, client: Client) -> Oper { + Oper { data, client } } } +#[async_trait::async_trait(?Send)] pub trait Perform { type Response: serde::ser::Serialize + Send; - fn perform( + async fn perform( &self, - pool: Pool>, + pool: &DbPool, websocket_info: Option, - ) -> Result; + ) -> Result; } diff --git a/server/src/api/post.rs b/server/src/api/post.rs index a3ac4915e..840f15305 100644 --- a/server/src/api/post.rs +++ b/server/src/api/post.rs @@ -1,6 +1,7 @@ use crate::{ api::{APIError, Oper, Perform}, apub::{ApubLikeableType, ApubObjectType}, + blocking, db::{ comment_view::*, community_view::*, @@ -26,12 +27,9 @@ use crate::{ UserOperation, WebsocketInfo, }, + DbPool, + LemmyError, }; -use diesel::{ - r2d2::{ConnectionManager, Pool}, - PgConnection, -}; -use failure::Error; use serde::{Deserialize, Serialize}; use std::str::FromStr; @@ -112,14 +110,15 @@ pub struct SavePost { auth: String, } +#[async_trait::async_trait(?Send)] impl Perform for Oper { type Response = PostResponse; - fn perform( + async fn perform( &self, - pool: Pool>, + pool: &DbPool, websocket_info: Option, - ) -> Result { + ) -> Result { let data: &CreatePost = &self.data; let claims = match Claims::decode(&data.auth) { @@ -139,22 +138,23 @@ impl Perform for Oper { let user_id = claims.id; - let conn = pool.get()?; - // 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()); } // 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 { return Err(APIError::err("site_ban").into()); } // Fetch Iframely and pictrs cached image 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 { name: data.name.to_owned(), @@ -177,7 +177,7 @@ impl Perform for Oper { 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, Err(e) => { let err_type = if e.to_string() == "value too long for type character varying(200)" { @@ -190,12 +190,14 @@ impl Perform for Oper { } }; - let updated_post = match Post::update_ap_id(&conn, inserted_post.id) { - Ok(post) => post, - Err(_e) => return Err(APIError::err("couldnt_create_post").into()), - }; + 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, + 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 let like_form = PostLikeForm { @@ -204,15 +206,20 @@ impl Perform for Oper { score: 1, }; - let _inserted_like = match PostLike::like(&conn, &like_form) { - Ok(like) => like, - Err(_e) => return Err(APIError::err("couldnt_like_post").into()), - }; + let like = move |conn: &'_ _| PostLike::like(conn, &like_form); + if blocking(pool, like).await?.is_err() { + 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 - 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, Err(_e) => return Err(APIError::err("couldnt_find_post").into()), }; @@ -231,14 +238,15 @@ impl Perform for Oper { } } +#[async_trait::async_trait(?Send)] impl Perform for Oper { type Response = GetPostResponse; - fn perform( + async fn perform( &self, - pool: Pool>, + pool: &DbPool, websocket_info: Option, - ) -> Result { + ) -> Result { let data: &GetPost = &self.data; let user_id: Option = match &data.auth { @@ -252,25 +260,38 @@ impl Perform for Oper { None => None, }; - let conn = pool.get()?; - - let post_view = match PostView::read(&conn, data.id, user_id) { + let id = data.id; + let post_view = match blocking(pool, move |conn| PostView::read(conn, id, user_id)).await? { Ok(post) => post, Err(_e) => return Err(APIError::err("couldnt_find_post").into()), }; - let comments = CommentQueryBuilder::create(&conn) - .for_post_id(data.id) - .my_user_id(user_id) - .limit(9999) - .list()?; + let id = data.id; + let comments = blocking(pool, move |conn| { + CommentQueryBuilder::create(conn) + .for_post_id(id) + .my_user_id(user_id) + .limit(9999) + .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 mut admins = UserView::admins(&conn)?; + 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 creator_index = admins.iter().position(|r| r.id == site_creator_id).unwrap(); let creator_user = admins.remove(creator_index); admins.insert(0, creator_user); @@ -305,14 +326,15 @@ impl Perform for Oper { } } +#[async_trait::async_trait(?Send)] impl Perform for Oper { type Response = GetPostsResponse; - fn perform( + async fn perform( &self, - pool: Pool>, + pool: &DbPool, websocket_info: Option, - ) -> Result { + ) -> Result { let data: &GetPosts = &self.data; let user_claims: Option = match &data.auth { @@ -336,17 +358,21 @@ impl Perform for Oper { let type_ = ListingType::from_str(&data.type_)?; let sort = SortType::from_str(&data.sort)?; - let conn = pool.get()?; - - let posts = match PostQueryBuilder::create(&conn) - .listing_type(type_) - .sort(&sort) - .show_nsfw(show_nsfw) - .for_community_id(data.community_id) - .my_user_id(user_id) - .page(data.page) - .limit(data.limit) - .list() + let page = data.page; + let limit = data.limit; + let community_id = data.community_id; + let posts = match blocking(pool, move |conn| { + PostQueryBuilder::create(conn) + .listing_type(type_) + .sort(&sort) + .show_nsfw(show_nsfw) + .for_community_id(community_id) + .my_user_id(user_id) + .page(page) + .limit(limit) + .list() + }) + .await? { Ok(posts) => posts, Err(_e) => return Err(APIError::err("couldnt_get_posts").into()), @@ -370,14 +396,15 @@ impl Perform for Oper { } } +#[async_trait::async_trait(?Send)] impl Perform for Oper { type Response = PostResponse; - fn perform( + async fn perform( &self, - pool: Pool>, + pool: &DbPool, websocket_info: Option, - ) -> Result { + ) -> Result { let data: &CreatePostLike = &self.data; let claims = match Claims::decode(&data.auth) { @@ -387,24 +414,27 @@ impl Perform for Oper { let user_id = claims.id; - let conn = pool.get()?; - // Don't do a downvote if site has downvotes disabled if data.score == -1 { - let site = SiteView::read(&conn)?; + let site = blocking(pool, move |conn| SiteView::read(conn)).await??; if !site.enable_downvotes { return Err(APIError::err("downvotes_disabled").into()); } } // Check for a community ban - let post = Post::read(&conn, data.post_id)?; - if CommunityUserBanView::get(&conn, user_id, post.community_id).is_ok() { + 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 = User_::read(&conn, user_id)?; + let user = blocking(pool, move |conn| User_::read(conn, user_id)).await??; if user.banned { return Err(APIError::err("site_ban").into()); } @@ -416,26 +446,33 @@ impl Perform for Oper { }; // 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 let do_add = like_form.score != 0 && (like_form.score == 1 || like_form.score == -1); if do_add { - let _inserted_like = match PostLike::like(&conn, &like_form) { - Ok(like) => like, - Err(_e) => return Err(APIError::err("couldnt_like_post").into()), - }; + let like_form2 = like_form.clone(); + let like = move |conn: &'_ _| PostLike::like(conn, &like_form2); + if blocking(pool, like).await?.is_err() { + return Err(APIError::err("couldnt_like_post").into()); + } if like_form.score == 1 { - post.send_like(&user, &conn)?; + post.send_like(&user, &self.client, pool).await?; } else if like_form.score == -1 { - post.send_dislike(&user, &conn)?; + post.send_dislike(&user, &self.client, pool).await?; } } 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, Err(_e) => return Err(APIError::err("couldnt_find_post").into()), }; @@ -454,14 +491,15 @@ impl Perform for Oper { } } +#[async_trait::async_trait(?Send)] impl Perform for Oper { type Response = PostResponse; - fn perform( + async fn perform( &self, - pool: Pool>, + pool: &DbPool, websocket_info: Option, - ) -> Result { + ) -> Result { let data: &EditPost = &self.data; if let Err(slurs) = slur_check(&data.name) { @@ -481,37 +519,46 @@ impl Perform for Oper { let user_id = claims.id; - let conn = pool.get()?; - // Verify its the creator or a mod or admin + let community_id = data.community_id; let mut editors: Vec = vec![data.creator_id]; editors.append( - &mut CommunityModeratorView::for_community(&conn, data.community_id)? - .into_iter() - .map(|m| m.user_id) - .collect(), + &mut blocking(pool, move |conn| { + CommunityModeratorView::for_community(conn, community_id) + .map(|v| v.into_iter().map(|m| m.user_id).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) { return Err(APIError::err("no_post_edit_allowed").into()); } // 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()); } // 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 { return Err(APIError::err("site_ban").into()); } // Fetch Iframely and Pictrs cached image 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 { name: data.name.to_owned(), @@ -534,7 +581,9 @@ impl Perform for Oper { 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, Err(e) => { let err_type = if e.to_string() == "value too long for type character varying(200)" { @@ -555,7 +604,7 @@ impl Perform for Oper { removed: Some(removed), 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() { @@ -564,7 +613,7 @@ impl Perform for Oper { post_id: data.edit_id, locked: Some(locked), }; - ModLockPost::create(&conn, &form)?; + blocking(pool, move |conn| ModLockPost::create(conn, &form)).await??; } if let Some(stickied) = data.stickied.to_owned() { @@ -573,26 +622,34 @@ impl Perform for Oper { post_id: data.edit_id, 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 deleted { - updated_post.send_delete(&user, &conn)?; + updated_post.send_delete(&user, &self.client, pool).await?; } 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() { if removed { - updated_post.send_remove(&user, &conn)?; + updated_post.send_remove(&user, &self.client, pool).await?; } else { - updated_post.send_undo_remove(&user, &conn)?; + updated_post + .send_undo_remove(&user, &self.client, pool) + .await?; } } 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 }; @@ -608,14 +665,15 @@ impl Perform for Oper { } } +#[async_trait::async_trait(?Send)] impl Perform for Oper { type Response = PostResponse; - fn perform( + async fn perform( &self, - pool: Pool>, + pool: &DbPool, _websocket_info: Option, - ) -> Result { + ) -> Result { let data: &SavePost = &self.data; let claims = match Claims::decode(&data.auth) { @@ -630,21 +688,23 @@ impl Perform for Oper { user_id, }; - let conn = pool.get()?; - if data.save { - match PostSaved::save(&conn, &post_saved_form) { - Ok(post) => post, - Err(_e) => return Err(APIError::err("couldnt_save_post").into()), - }; + let save = move |conn: &'_ _| PostSaved::save(conn, &post_saved_form); + if blocking(pool, save).await?.is_err() { + return Err(APIError::err("couldnt_save_post").into()); + } } else { - match PostSaved::unsave(&conn, &post_saved_form) { - Ok(post) => post, - Err(_e) => return Err(APIError::err("couldnt_save_post").into()), - }; + let unsave = move |conn: &'_ _| PostSaved::unsave(conn, &post_saved_form); + if blocking(pool, unsave).await?.is_err() { + 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 }) } diff --git a/server/src/api/site.rs b/server/src/api/site.rs index faee30cbb..f45561a82 100644 --- a/server/src/api/site.rs +++ b/server/src/api/site.rs @@ -2,6 +2,7 @@ use super::user::Register; use crate::{ api::{APIError, Oper, Perform}, apub::fetcher::search_by_apub_id, + blocking, db::{ category::*, comment_view::*, @@ -22,12 +23,9 @@ use crate::{ slur_check, slurs_vec_to_str, websocket::{server::SendAllMessage, UserOperation, WebsocketInfo}, + DbPool, + LemmyError, }; -use diesel::{ - r2d2::{ConnectionManager, Pool}, - PgConnection, -}; -use failure::Error; use log::{debug, info}; use serde::{Deserialize, Serialize}; use std::str::FromStr; @@ -139,87 +137,79 @@ pub struct SaveSiteConfig { auth: String, } +#[async_trait::async_trait(?Send)] impl Perform for Oper { type Response = ListCategoriesResponse; - fn perform( + async fn perform( &self, - pool: Pool>, + pool: &DbPool, _websocket_info: Option, - ) -> Result { + ) -> Result { let _data: &ListCategories = &self.data; - let conn = pool.get()?; - - let categories: Vec = Category::list_all(&conn)?; + let categories = blocking(pool, move |conn| Category::list_all(conn)).await??; // Return the jwt Ok(ListCategoriesResponse { categories }) } } +#[async_trait::async_trait(?Send)] impl Perform for Oper { type Response = GetModlogResponse; - fn perform( + async fn perform( &self, - pool: Pool>, + pool: &DbPool, _websocket_info: Option, - ) -> Result { + ) -> Result { 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( - &conn, - data.community_id, - data.mod_user_id, - data.page, - data.limit, - )?; - let locked_posts = ModLockPostView::list( - &conn, - data.community_id, - data.mod_user_id, - data.page, - data.limit, - )?; - let stickied_posts = ModStickyPostView::list( - &conn, - data.community_id, - data.mod_user_id, - data.page, - data.limit, - )?; - let removed_comments = ModRemoveCommentView::list( - &conn, - data.community_id, - 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, - )?; + let locked_posts = blocking(pool, move |conn| { + ModLockPostView::list(conn, community_id, mod_user_id, page, limit) + }) + .await??; + + let stickied_posts = blocking(pool, move |conn| { + ModStickyPostView::list(conn, community_id, mod_user_id, page, limit) + }) + .await??; + + let removed_comments = blocking(pool, move |conn| { + ModRemoveCommentView::list(conn, community_id, mod_user_id, page, limit) + }) + .await??; + + let banned_from_community = blocking(pool, move |conn| { + ModBanFromCommunityView::list(conn, community_id, mod_user_id, page, limit) + }) + .await??; + + let added_to_community = blocking(pool, move |conn| { + ModAddCommunityView::list(conn, community_id, mod_user_id, page, limit) + }) + .await??; // 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() { - ( - ModRemoveCommunityView::list(&conn, data.mod_user_id, data.page, data.limit)?, - ModBanView::list(&conn, data.mod_user_id, data.page, data.limit)?, - ModAddView::list(&conn, data.mod_user_id, data.page, data.limit)?, - ) + blocking(pool, move |conn| { + Ok(( + ModRemoveCommunityView::list(conn, mod_user_id, page, limit)?, + ModBanView::list(conn, mod_user_id, page, limit)?, + ModAddView::list(conn, mod_user_id, page, limit)?, + )) as Result<_, LemmyError> + }) + .await?? } else { (Vec::new(), Vec::new(), Vec::new()) }; @@ -239,14 +229,15 @@ impl Perform for Oper { } } +#[async_trait::async_trait(?Send)] impl Perform for Oper { type Response = SiteResponse; - fn perform( + async fn perform( &self, - pool: Pool>, + pool: &DbPool, _websocket_info: Option, - ) -> Result { + ) -> Result { let data: &CreateSite = &self.data; let claims = match Claims::decode(&data.auth) { @@ -266,10 +257,9 @@ impl Perform for Oper { let user_id = claims.id; - let conn = pool.get()?; - // 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()); } @@ -283,24 +273,25 @@ impl Perform for Oper { updated: None, }; - match Site::create(&conn, &site_form) { - Ok(site) => site, - Err(_e) => return Err(APIError::err("site_already_exists").into()), - }; + let create_site = move |conn: &'_ _| Site::create(conn, &site_form); + if blocking(pool, create_site).await?.is_err() { + 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 }) } } +#[async_trait::async_trait(?Send)] impl Perform for Oper { type Response = SiteResponse; - fn perform( + async fn perform( &self, - pool: Pool>, + pool: &DbPool, websocket_info: Option, - ) -> Result { + ) -> Result { let data: &EditSite = &self.data; let claims = match Claims::decode(&data.auth) { @@ -320,14 +311,13 @@ impl Perform for Oper { let user_id = claims.id; - let conn = pool.get()?; - // 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()); } - let found_site = Site::read(&conn, 1)?; + let found_site = blocking(pool, move |conn| Site::read(conn, 1)).await??; let site_form = SiteForm { name: data.name.to_owned(), @@ -339,12 +329,12 @@ impl Perform for Oper { enable_nsfw: data.enable_nsfw, }; - match Site::update(&conn, 1, &site_form) { - Ok(site) => site, - Err(_e) => return Err(APIError::err("couldnt_update_site").into()), - }; + let update_site = move |conn: &'_ _| Site::update(conn, 1, &site_form); + if blocking(pool, update_site).await?.is_err() { + 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 }; @@ -360,21 +350,21 @@ impl Perform for Oper { } } +#[async_trait::async_trait(?Send)] impl Perform for Oper { type Response = GetSiteResponse; - fn perform( + async fn perform( &self, - pool: Pool>, + pool: &DbPool, websocket_info: Option, - ) -> Result { + ) -> Result { let _data: &GetSite = &self.data; - let conn = pool.get()?; - // TODO refactor this a little - let site_view = if let Ok(_site) = Site::read(&conn, 1) { - Some(SiteView::read(&conn)?) + let res = blocking(pool, move |conn| Site::read(conn, 1)).await?; + 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() { let register = Register { username: setup.admin_username.to_owned(), @@ -384,7 +374,9 @@ impl Perform for Oper { admin: 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); let create_site = CreateSite { @@ -395,14 +387,16 @@ impl Perform for Oper { enable_nsfw: true, 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); - Some(SiteView::read(&conn)?) + Some(blocking(pool, move |conn| SiteView::read(conn)).await??) } else { 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 if let Some(site_view) = site_view.to_owned() { @@ -415,7 +409,7 @@ impl Perform for Oper { } } - let banned = UserView::banned(&conn)?; + let banned = blocking(pool, move |conn| UserView::banned(conn)).await??; let online = if let Some(_ws) = websocket_info { // TODO @@ -437,21 +431,20 @@ impl Perform for Oper { } } +#[async_trait::async_trait(?Send)] impl Perform for Oper { type Response = SearchResponse; - fn perform( + async fn perform( &self, - pool: Pool>, + pool: &DbPool, _websocket_info: Option, - ) -> Result { + ) -> Result { let data: &Search = &self.data; dbg!(&data); - let conn = pool.get()?; - - match search_by_apub_id(&data.q, &conn) { + match search_by_apub_id(&data.q, &self.client, pool).await { Ok(r) => return Ok(r), Err(e) => debug!("Failed to resolve search query as activitypub ID: {}", e), } @@ -467,7 +460,6 @@ impl Perform for Oper { None => None, }; - let sort = SortType::from_str(&data.sort)?; let type_ = SearchType::from_str(&data.type_)?; let mut posts = Vec::new(); @@ -477,85 +469,126 @@ impl Perform for Oper { // 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_ { SearchType::Posts => { - posts = PostQueryBuilder::create(&conn) - .sort(&sort) - .show_nsfw(true) - .for_community_id(data.community_id) - .search_term(data.q.to_owned()) - .my_user_id(user_id) - .page(data.page) - .limit(data.limit) - .list()?; + posts = blocking(pool, move |conn| { + PostQueryBuilder::create(conn) + .sort(&sort) + .show_nsfw(true) + .for_community_id(community_id) + .search_term(q) + .my_user_id(user_id) + .page(page) + .limit(limit) + .list() + }) + .await??; } SearchType::Comments => { - comments = CommentQueryBuilder::create(&conn) - .sort(&sort) - .search_term(data.q.to_owned()) - .my_user_id(user_id) - .page(data.page) - .limit(data.limit) - .list()?; + comments = blocking(pool, move |conn| { + CommentQueryBuilder::create(&conn) + .sort(&sort) + .search_term(q) + .my_user_id(user_id) + .page(page) + .limit(limit) + .list() + }) + .await??; } SearchType::Communities => { - communities = CommunityQueryBuilder::create(&conn) - .sort(&sort) - .search_term(data.q.to_owned()) - .page(data.page) - .limit(data.limit) - .list()?; + communities = blocking(pool, move |conn| { + CommunityQueryBuilder::create(conn) + .sort(&sort) + .search_term(q) + .page(page) + .limit(limit) + .list() + }) + .await??; } SearchType::Users => { - users = UserQueryBuilder::create(&conn) - .sort(&sort) - .search_term(data.q.to_owned()) - .page(data.page) - .limit(data.limit) - .list()?; + users = blocking(pool, move |conn| { + UserQueryBuilder::create(conn) + .sort(&sort) + .search_term(q) + .page(page) + .limit(limit) + .list() + }) + .await??; } SearchType::All => { - posts = PostQueryBuilder::create(&conn) - .sort(&sort) - .show_nsfw(true) - .for_community_id(data.community_id) - .search_term(data.q.to_owned()) - .my_user_id(user_id) - .page(data.page) - .limit(data.limit) - .list()?; + posts = blocking(pool, move |conn| { + PostQueryBuilder::create(conn) + .sort(&sort) + .show_nsfw(true) + .for_community_id(community_id) + .search_term(q) + .my_user_id(user_id) + .page(page) + .limit(limit) + .list() + }) + .await??; - comments = CommentQueryBuilder::create(&conn) - .sort(&sort) - .search_term(data.q.to_owned()) - .my_user_id(user_id) - .page(data.page) - .limit(data.limit) - .list()?; + let q = data.q.to_owned(); + let sort = SortType::from_str(&data.sort)?; - communities = CommunityQueryBuilder::create(&conn) - .sort(&sort) - .search_term(data.q.to_owned()) - .page(data.page) - .limit(data.limit) - .list()?; + comments = blocking(pool, move |conn| { + CommentQueryBuilder::create(conn) + .sort(&sort) + .search_term(q) + .my_user_id(user_id) + .page(page) + .limit(limit) + .list() + }) + .await??; - users = UserQueryBuilder::create(&conn) - .sort(&sort) - .search_term(data.q.to_owned()) - .page(data.page) - .limit(data.limit) - .list()?; + let q = data.q.to_owned(); + let sort = SortType::from_str(&data.sort)?; + + communities = blocking(pool, move |conn| { + CommunityQueryBuilder::create(conn) + .sort(&sort) + .search_term(q) + .page(page) + .limit(limit) + .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 => { - posts = PostQueryBuilder::create(&conn) - .sort(&sort) - .show_nsfw(true) - .for_community_id(data.community_id) - .url_search(data.q.to_owned()) - .page(data.page) - .limit(data.limit) - .list()?; + posts = blocking(pool, move |conn| { + PostQueryBuilder::create(conn) + .sort(&sort) + .show_nsfw(true) + .for_community_id(community_id) + .url_search(q) + .page(page) + .limit(limit) + .list() + }) + .await??; } }; @@ -570,14 +603,15 @@ impl Perform for Oper { } } +#[async_trait::async_trait(?Send)] impl Perform for Oper { type Response = GetSiteResponse; - fn perform( + async fn perform( &self, - pool: Pool>, + pool: &DbPool, _websocket_info: Option, - ) -> Result { + ) -> Result { let data: &TransferSite = &self.data; let claims = match Claims::decode(&data.auth) { @@ -587,9 +621,7 @@ impl Perform for Oper { let user_id = claims.id; - let conn = pool.get()?; - - let read_site = Site::read(&conn, 1)?; + let read_site = blocking(pool, move |conn| Site::read(conn, 1)).await??; // Make sure user is the creator if read_site.creator_id != user_id { @@ -606,9 +638,9 @@ impl Perform for Oper { enable_nsfw: read_site.enable_nsfw, }; - match Site::update(&conn, 1, &site_form) { - Ok(site) => site, - Err(_e) => return Err(APIError::err("couldnt_update_site").into()), + let update_site = move |conn: &'_ _| Site::update(conn, 1, &site_form); + if blocking(pool, update_site).await?.is_err() { + return Err(APIError::err("couldnt_update_site").into()); }; // Mod tables @@ -618,11 +650,11 @@ impl Perform for Oper { 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 .iter() .position(|r| r.id == site_view.creator_id) @@ -630,7 +662,7 @@ impl Perform for Oper { let creator_user = admins.remove(creator_index); admins.insert(0, creator_user); - let banned = UserView::banned(&conn)?; + let banned = blocking(pool, move |conn| UserView::banned(conn)).await??; Ok(GetSiteResponse { site: Some(site_view), @@ -641,14 +673,15 @@ impl Perform for Oper { } } +#[async_trait::async_trait(?Send)] impl Perform for Oper { type Response = GetSiteConfigResponse; - fn perform( + async fn perform( &self, - pool: Pool>, + pool: &DbPool, _websocket_info: Option, - ) -> Result { + ) -> Result { let data: &GetSiteConfig = &self.data; let claims = match Claims::decode(&data.auth) { @@ -658,10 +691,8 @@ impl Perform for Oper { let user_id = claims.id; - let conn = pool.get()?; - // Only let admins read this - let admins = UserView::admins(&conn)?; + let admins = blocking(pool, move |conn| UserView::admins(conn)).await??; let admin_ids: Vec = admins.into_iter().map(|m| m.id).collect(); if !admin_ids.contains(&user_id) { @@ -674,14 +705,15 @@ impl Perform for Oper { } } +#[async_trait::async_trait(?Send)] impl Perform for Oper { type Response = GetSiteConfigResponse; - fn perform( + async fn perform( &self, - pool: Pool>, + pool: &DbPool, _websocket_info: Option, - ) -> Result { + ) -> Result { let data: &SaveSiteConfig = &self.data; let claims = match Claims::decode(&data.auth) { @@ -691,10 +723,8 @@ impl Perform for Oper { let user_id = claims.id; - let conn = pool.get()?; - // Only let admins read this - let admins = UserView::admins(&conn)?; + let admins = blocking(pool, move |conn| UserView::admins(conn)).await??; let admin_ids: Vec = admins.into_iter().map(|m| m.id).collect(); if !admin_ids.contains(&user_id) { diff --git a/server/src/api/user.rs b/server/src/api/user.rs index 0b6458e75..a4e47e41c 100644 --- a/server/src/api/user.rs +++ b/server/src/api/user.rs @@ -6,6 +6,7 @@ use crate::{ ApubObjectType, EndpointType, }, + blocking, db::{ comment::*, comment_view::*, @@ -43,13 +44,10 @@ use crate::{ UserOperation, WebsocketInfo, }, + DbPool, + LemmyError, }; use bcrypt::verify; -use diesel::{ - r2d2::{ConnectionManager, Pool}, - PgConnection, -}; -use failure::Error; use log::error; use serde::{Deserialize, Serialize}; use std::str::FromStr; @@ -252,20 +250,24 @@ pub struct UserJoinResponse { pub user_id: i32, } +#[async_trait::async_trait(?Send)] impl Perform for Oper { type Response = LoginResponse; - fn perform( + async fn perform( &self, - pool: Pool>, + pool: &DbPool, _websocket_info: Option, - ) -> Result { + ) -> Result { let data: &Login = &self.data; - let conn = pool.get()?; - // Fetch that username / email - let user: User_ = match User_::find_by_email_or_username(&conn, &data.username_or_email) { + let username_or_email = data.username_or_email.clone(); + let user = match blocking(pool, move |conn| { + User_::find_by_email_or_username(conn, &username_or_email) + }) + .await? + { Ok(user) => user, Err(_e) => return Err(APIError::err("couldnt_find_that_username_or_email").into()), }; @@ -281,20 +283,20 @@ impl Perform for Oper { } } +#[async_trait::async_trait(?Send)] impl Perform for Oper { type Response = LoginResponse; - fn perform( + async fn perform( &self, - pool: Pool>, + pool: &DbPool, _websocket_info: Option, - ) -> Result { + ) -> Result { let data: &Register = &self.data; - let conn = pool.get()?; - // Make sure site has open registration - if let Ok(site) = SiteView::read(&conn) { + if let Ok(site) = blocking(pool, move |conn| SiteView::read(conn)).await? { + let site: SiteView = site; if !site.open_registration { return Err(APIError::err("registration_closed").into()); } @@ -310,7 +312,11 @@ impl Perform for Oper { } // Make sure there are no admins - if data.admin && !UserView::admins(&conn)?.is_empty() { + let any_admins = blocking(pool, move |conn| { + UserView::admins(conn).map(|a| a.is_empty()) + }) + .await??; + if data.admin && !any_admins { return Err(APIError::err("admin_already_created").into()); } @@ -346,7 +352,7 @@ impl Perform for Oper { }; // Create the user - let inserted_user = match User_::register(&conn, &user_form) { + let inserted_user = match blocking(pool, move |conn| User_::register(conn, &user_form)).await? { Ok(user) => user, Err(e) => { let err_type = if e.to_string() @@ -364,7 +370,7 @@ impl Perform for Oper { let main_community_keypair = generate_actor_keypair()?; // Create the main community if it doesn't exist - let main_community: Community = match Community::read(&conn, 2) { + let main_community = match blocking(pool, move |conn| Community::read(conn, 2)).await? { Ok(c) => c, Err(_e) => { let default_community_name = "main"; @@ -385,7 +391,7 @@ impl Perform for Oper { last_refreshed_at: None, published: None, }; - Community::create(&conn, &community_form).unwrap() + blocking(pool, move |conn| Community::create(conn, &community_form)).await?? } }; @@ -395,11 +401,10 @@ impl Perform for Oper { user_id: inserted_user.id, }; - let _inserted_community_follower = - match CommunityFollower::follow(&conn, &community_follower_form) { - Ok(user) => user, - Err(_e) => return Err(APIError::err("community_follower_already_exists").into()), - }; + let follow = move |conn: &'_ _| CommunityFollower::follow(conn, &community_follower_form); + if blocking(pool, follow).await?.is_err() { + return Err(APIError::err("community_follower_already_exists").into()); + }; // If its an admin, add them as a mod and follower to main if data.admin { @@ -408,11 +413,10 @@ impl Perform for Oper { user_id: inserted_user.id, }; - let _inserted_community_moderator = - match CommunityModerator::join(&conn, &community_moderator_form) { - Ok(user) => user, - Err(_e) => return Err(APIError::err("community_moderator_already_exists").into()), - }; + let join = move |conn: &'_ _| CommunityModerator::join(conn, &community_moderator_form); + if blocking(pool, join).await?.is_err() { + return Err(APIError::err("community_moderator_already_exists").into()); + } } // Return the jwt @@ -422,14 +426,15 @@ impl Perform for Oper { } } +#[async_trait::async_trait(?Send)] impl Perform for Oper { type Response = LoginResponse; - fn perform( + async fn perform( &self, - pool: Pool>, + pool: &DbPool, _websocket_info: Option, - ) -> Result { + ) -> Result { let data: &SaveUserSettings = &self.data; let claims = match Claims::decode(&data.auth) { @@ -439,9 +444,7 @@ impl Perform for Oper { let user_id = claims.id; - let conn = pool.get()?; - - let read_user = User_::read(&conn, user_id)?; + let read_user = blocking(pool, move |conn| User_::read(conn, user_id)).await??; let email = match &data.email { Some(email) => Some(email.to_owned()), @@ -465,7 +468,12 @@ impl Perform for Oper { if !valid { return Err(APIError::err("password_incorrect").into()); } - User_::update_password(&conn, user_id, &new_password)?.password_encrypted + let new_password = new_password.to_owned(); + let user = blocking(pool, move |conn| { + User_::update_password(conn, user_id, &new_password) + }) + .await??; + user.password_encrypted } None => return Err(APIError::err("password_incorrect").into()), } @@ -501,7 +509,8 @@ impl Perform for Oper { last_refreshed_at: None, }; - let updated_user = match User_::update(&conn, user_id, &user_form) { + let res = blocking(pool, move |conn| User_::update(conn, user_id, &user_form)).await?; + let updated_user: User_ = match res { Ok(user) => user, Err(e) => { let err_type = if e.to_string() @@ -523,18 +532,17 @@ impl Perform for Oper { } } +#[async_trait::async_trait(?Send)] impl Perform for Oper { type Response = GetUserDetailsResponse; - fn perform( + async fn perform( &self, - pool: Pool>, + pool: &DbPool, _websocket_info: Option, - ) -> Result { + ) -> Result { let data: &GetUserDetails = &self.data; - let conn = pool.get()?; - let user_claims: Option = match &data.auth { Some(auth) => match Claims::decode(&auth) { Ok(claims) => Some(claims.claims), @@ -555,54 +563,71 @@ impl Perform for Oper { let sort = SortType::from_str(&data.sort)?; + let username = data + .username + .to_owned() + .unwrap_or_else(|| "admin".to_string()); let user_details_id = match data.user_id { Some(id) => id, None => { - match User_::read_from_name( - &conn, - &data - .username - .to_owned() - .unwrap_or_else(|| "admin".to_string()), - ) { + let user = blocking(pool, move |conn| User_::read_from_name(conn, &username)).await?; + match user { Ok(user) => user.id, Err(_e) => return Err(APIError::err("couldnt_find_that_username_or_email").into()), } } }; - let mut user_view = UserView::read(&conn, user_details_id)?; + let mut user_view = blocking(pool, move |conn| UserView::read(conn, user_details_id)).await??; - let mut posts_query = PostQueryBuilder::create(&conn) - .sort(&sort) - .show_nsfw(show_nsfw) - .saved_only(data.saved_only) - .for_community_id(data.community_id) - .my_user_id(user_id) - .page(data.page) - .limit(data.limit); + let page = data.page; + let limit = data.limit; + let saved_only = data.saved_only; + let community_id = data.community_id; + let (posts, comments) = blocking(pool, move |conn| { + let mut posts_query = PostQueryBuilder::create(conn) + .sort(&sort) + .show_nsfw(show_nsfw) + .saved_only(saved_only) + .for_community_id(community_id) + .my_user_id(user_id) + .page(page) + .limit(limit); - let mut comments_query = CommentQueryBuilder::create(&conn) - .sort(&sort) - .saved_only(data.saved_only) - .my_user_id(user_id) - .page(data.page) - .limit(data.limit); + let mut comments_query = CommentQueryBuilder::create(conn) + .sort(&sort) + .saved_only(saved_only) + .my_user_id(user_id) + .page(page) + .limit(limit); - // If its saved only, you don't care what creator it was - // Or, if its not saved, then you only want it for that specific creator - if !data.saved_only { - posts_query = posts_query.for_creator_id(user_details_id); - comments_query = comments_query.for_creator_id(user_details_id); - } + // If its saved only, you don't care what creator it was + // Or, if its not saved, then you only want it for that specific creator + if !saved_only { + posts_query = posts_query.for_creator_id(user_details_id); + comments_query = comments_query.for_creator_id(user_details_id); + } - let posts = posts_query.list()?; - let comments = comments_query.list()?; + let posts = posts_query.list()?; + let comments = comments_query.list()?; - let follows = CommunityFollowerView::for_user(&conn, user_details_id)?; - let moderates = CommunityModeratorView::for_user(&conn, user_details_id)?; - let site_creator_id = Site::read(&conn, 1)?.creator_id; - let mut admins = UserView::admins(&conn)?; + Ok((posts, comments)) as Result<_, LemmyError> + }) + .await??; + + let follows = blocking(pool, move |conn| { + CommunityFollowerView::for_user(conn, user_details_id) + }) + .await??; + let moderates = blocking(pool, move |conn| { + CommunityModeratorView::for_user(conn, user_details_id) + }) + .await??; + + 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 creator_index = admins.iter().position(|r| r.id == site_creator_id).unwrap(); let creator_user = admins.remove(creator_index); admins.insert(0, creator_user); @@ -628,14 +653,15 @@ impl Perform for Oper { } } +#[async_trait::async_trait(?Send)] impl Perform for Oper { type Response = AddAdminResponse; - fn perform( + async fn perform( &self, - pool: Pool>, + pool: &DbPool, websocket_info: Option, - ) -> Result { + ) -> Result { let data: &AddAdmin = &self.data; let claims = match Claims::decode(&data.auth) { @@ -645,17 +671,17 @@ impl Perform for Oper { let user_id = claims.id; - let conn = pool.get()?; - // Make sure user is an admin - if !UserView::read(&conn, user_id)?.admin { + let is_admin = move |conn: &'_ _| UserView::read(conn, user_id).map(|u| u.admin); + if !blocking(pool, is_admin).await?? { return Err(APIError::err("not_an_admin").into()); } - match User_::add_admin(&conn, user_id, data.added) { - Ok(user) => user, - Err(_e) => return Err(APIError::err("couldnt_update_user").into()), - }; + let added = data.added; + let add_admin = move |conn: &'_ _| User_::add_admin(conn, user_id, added); + if blocking(pool, add_admin).await?.is_err() { + return Err(APIError::err("couldnt_update_user").into()); + } // Mod tables let form = ModAddForm { @@ -664,10 +690,12 @@ impl Perform for Oper { removed: Some(!data.added), }; - ModAdd::create(&conn, &form)?; + blocking(pool, move |conn| ModAdd::create(conn, &form)).await??; - let site_creator_id = Site::read(&conn, 1)?.creator_id; - let mut admins = UserView::admins(&conn)?; + 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 creator_index = admins.iter().position(|r| r.id == site_creator_id).unwrap(); let creator_user = admins.remove(creator_index); admins.insert(0, creator_user); @@ -686,14 +714,15 @@ impl Perform for Oper { } } +#[async_trait::async_trait(?Send)] impl Perform for Oper { type Response = BanUserResponse; - fn perform( + async fn perform( &self, - pool: Pool>, + pool: &DbPool, websocket_info: Option, - ) -> Result { + ) -> Result { let data: &BanUser = &self.data; let claims = match Claims::decode(&data.auth) { @@ -703,17 +732,18 @@ impl Perform for Oper { let user_id = claims.id; - let conn = pool.get()?; - // Make sure user is an admin - if !UserView::read(&conn, user_id)?.admin { + let is_admin = move |conn: &'_ _| UserView::read(conn, user_id).map(|u| u.admin); + if !blocking(pool, is_admin).await?? { return Err(APIError::err("not_an_admin").into()); } - match User_::ban_user(&conn, user_id, data.ban) { - Ok(user) => user, - Err(_e) => return Err(APIError::err("couldnt_update_user").into()), - }; + let ban = data.ban; + let banned_user_id = data.user_id; + let ban_user = move |conn: &'_ _| User_::ban_user(conn, banned_user_id, ban); + if blocking(pool, ban_user).await?.is_err() { + return Err(APIError::err("couldnt_update_user").into()); + } // Mod tables let expires = match data.expires { @@ -729,9 +759,10 @@ impl Perform for Oper { expires, }; - ModBan::create(&conn, &form)?; + blocking(pool, move |conn| ModBan::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 = BanUserResponse { user: user_view, @@ -750,14 +781,15 @@ impl Perform for Oper { } } +#[async_trait::async_trait(?Send)] impl Perform for Oper { type Response = GetRepliesResponse; - fn perform( + async fn perform( &self, - pool: Pool>, + pool: &DbPool, _websocket_info: Option, - ) -> Result { + ) -> Result { let data: &GetReplies = &self.data; let claims = match Claims::decode(&data.auth) { @@ -769,27 +801,32 @@ impl Perform for Oper { let sort = SortType::from_str(&data.sort)?; - let conn = pool.get()?; - - let replies = ReplyQueryBuilder::create(&conn, user_id) - .sort(&sort) - .unread_only(data.unread_only) - .page(data.page) - .limit(data.limit) - .list()?; + let page = data.page; + let limit = data.limit; + let unread_only = data.unread_only; + let replies = blocking(pool, move |conn| { + ReplyQueryBuilder::create(conn, user_id) + .sort(&sort) + .unread_only(unread_only) + .page(page) + .limit(limit) + .list() + }) + .await??; Ok(GetRepliesResponse { replies }) } } +#[async_trait::async_trait(?Send)] impl Perform for Oper { type Response = GetUserMentionsResponse; - fn perform( + async fn perform( &self, - pool: Pool>, + pool: &DbPool, _websocket_info: Option, - ) -> Result { + ) -> Result { let data: &GetUserMentions = &self.data; let claims = match Claims::decode(&data.auth) { @@ -801,27 +838,32 @@ impl Perform for Oper { let sort = SortType::from_str(&data.sort)?; - let conn = pool.get()?; - - let mentions = UserMentionQueryBuilder::create(&conn, user_id) - .sort(&sort) - .unread_only(data.unread_only) - .page(data.page) - .limit(data.limit) - .list()?; + let page = data.page; + let limit = data.limit; + let unread_only = data.unread_only; + let mentions = blocking(pool, move |conn| { + UserMentionQueryBuilder::create(conn, user_id) + .sort(&sort) + .unread_only(unread_only) + .page(page) + .limit(limit) + .list() + }) + .await??; Ok(GetUserMentionsResponse { mentions }) } } +#[async_trait::async_trait(?Send)] impl Perform for Oper { type Response = UserMentionResponse; - fn perform( + async fn perform( &self, - pool: Pool>, + pool: &DbPool, _websocket_info: Option, - ) -> Result { + ) -> Result { let data: &EditUserMention = &self.data; let claims = match Claims::decode(&data.auth) { @@ -831,9 +873,9 @@ impl Perform for Oper { let user_id = claims.id; - let conn = pool.get()?; - - let user_mention = UserMention::read(&conn, data.user_mention_id)?; + let user_mention_id = data.user_mention_id; + let user_mention = + blocking(pool, move |conn| UserMention::read(conn, user_mention_id)).await??; let user_mention_form = UserMentionForm { recipient_id: user_id, @@ -841,13 +883,18 @@ impl Perform for Oper { read: data.read.to_owned(), }; - let _updated_user_mention = - match UserMention::update(&conn, user_mention.id, &user_mention_form) { - Ok(comment) => comment, - Err(_e) => return Err(APIError::err("couldnt_update_comment").into()), - }; + let user_mention_id = user_mention.id; + let update_mention = + move |conn: &'_ _| UserMention::update(conn, user_mention_id, &user_mention_form); + if blocking(pool, update_mention).await?.is_err() { + return Err(APIError::err("couldnt_update_comment").into()); + }; - let user_mention_view = UserMentionView::read(&conn, user_mention.id, user_id)?; + let user_mention_id = user_mention.id; + let user_mention_view = blocking(pool, move |conn| { + UserMentionView::read(conn, user_mention_id, user_id) + }) + .await??; Ok(UserMentionResponse { mention: user_mention_view, @@ -855,14 +902,15 @@ impl Perform for Oper { } } +#[async_trait::async_trait(?Send)] impl Perform for Oper { type Response = GetRepliesResponse; - fn perform( + async fn perform( &self, - pool: Pool>, + pool: &DbPool, _websocket_info: Option, - ) -> Result { + ) -> Result { let data: &MarkAllAsRead = &self.data; let claims = match Claims::decode(&data.auth) { @@ -872,28 +920,35 @@ impl Perform for Oper { let user_id = claims.id; - let conn = pool.get()?; - - let replies = ReplyQueryBuilder::create(&conn, user_id) - .unread_only(true) - .page(1) - .limit(999) - .list()?; + let replies = blocking(pool, move |conn| { + ReplyQueryBuilder::create(conn, user_id) + .unread_only(true) + .page(1) + .limit(999) + .list() + }) + .await??; + // TODO: this should probably be a bulk operation for reply in &replies { - match Comment::mark_as_read(&conn, reply.id) { - Ok(comment) => comment, - Err(_e) => return Err(APIError::err("couldnt_update_comment").into()), - }; + let reply_id = reply.id; + let mark_as_read = move |conn: &'_ _| Comment::mark_as_read(conn, reply_id); + if blocking(pool, mark_as_read).await?.is_err() { + return Err(APIError::err("couldnt_update_comment").into()); + } } // Mentions - let mentions = UserMentionQueryBuilder::create(&conn, user_id) - .unread_only(true) - .page(1) - .limit(999) - .list()?; + let mentions = blocking(pool, move |conn| { + UserMentionQueryBuilder::create(conn, user_id) + .unread_only(true) + .page(1) + .limit(999) + .list() + }) + .await??; + // TODO: this should probably be a bulk operation for mention in &mentions { let mention_form = UserMentionForm { recipient_id: mention.to_owned().recipient_id, @@ -901,20 +956,25 @@ impl Perform for Oper { read: Some(true), }; - let _updated_mention = - match UserMention::update(&conn, mention.user_mention_id, &mention_form) { - Ok(mention) => mention, - Err(_e) => return Err(APIError::err("couldnt_update_comment").into()), - }; + let user_mention_id = mention.user_mention_id; + let update_mention = + move |conn: &'_ _| UserMention::update(conn, user_mention_id, &mention_form); + if blocking(pool, update_mention).await?.is_err() { + return Err(APIError::err("couldnt_update_comment").into()); + } } // messages - let messages = PrivateMessageQueryBuilder::create(&conn, user_id) - .page(1) - .limit(999) - .unread_only(true) - .list()?; + let messages = blocking(pool, move |conn| { + PrivateMessageQueryBuilder::create(conn, user_id) + .page(1) + .limit(999) + .unread_only(true) + .list() + }) + .await??; + // TODO: this should probably be a bulk operation for message in &messages { let private_message_form = PrivateMessageForm { content: message.to_owned().content, @@ -928,25 +988,27 @@ impl Perform for Oper { published: None, }; - let _updated_message = match PrivateMessage::update(&conn, message.id, &private_message_form) - { - Ok(message) => message, - Err(_e) => return Err(APIError::err("couldnt_update_private_message").into()), - }; + let message_id = message.id; + let update_pm = + move |conn: &'_ _| PrivateMessage::update(conn, message_id, &private_message_form); + if blocking(pool, update_pm).await?.is_err() { + return Err(APIError::err("couldnt_update_private_message").into()); + } } Ok(GetRepliesResponse { replies: vec![] }) } } +#[async_trait::async_trait(?Send)] impl Perform for Oper { type Response = LoginResponse; - fn perform( + async fn perform( &self, - pool: Pool>, + pool: &DbPool, _websocket_info: Option, - ) -> Result { + ) -> Result { let data: &DeleteAccount = &self.data; let claims = match Claims::decode(&data.auth) { @@ -956,9 +1018,7 @@ impl Perform for Oper { let user_id = claims.id; - let conn = pool.get()?; - - let user: User_ = User_::read(&conn, user_id)?; + let user = blocking(pool, move |conn| User_::read(conn, user_id)).await??; // Verify the password let valid: bool = verify(&data.password, &user.password_encrypted).unwrap_or(false); @@ -967,30 +1027,40 @@ impl Perform for Oper { } // Comments - let comments = CommentQueryBuilder::create(&conn) - .for_creator_id(user_id) - .limit(std::i64::MAX) - .list()?; + let comments = blocking(pool, move |conn| { + CommentQueryBuilder::create(conn) + .for_creator_id(user_id) + .limit(std::i64::MAX) + .list() + }) + .await??; + // TODO: this should probably be a bulk operation for comment in &comments { - let _updated_comment = match Comment::permadelete(&conn, comment.id) { - Ok(comment) => comment, - Err(_e) => return Err(APIError::err("couldnt_update_comment").into()), - }; + let comment_id = comment.id; + let permadelete = move |conn: &'_ _| Comment::permadelete(conn, comment_id); + if blocking(pool, permadelete).await?.is_err() { + return Err(APIError::err("couldnt_update_comment").into()); + } } // Posts - let posts = PostQueryBuilder::create(&conn) - .sort(&SortType::New) - .for_creator_id(user_id) - .limit(std::i64::MAX) - .list()?; + let posts = blocking(pool, move |conn| { + PostQueryBuilder::create(conn) + .sort(&SortType::New) + .for_creator_id(user_id) + .limit(std::i64::MAX) + .list() + }) + .await??; + // TODO: this should probably be a bulk operation for post in &posts { - let _updated_post = match Post::permadelete(&conn, post.id) { - Ok(post) => post, - Err(_e) => return Err(APIError::err("couldnt_update_post").into()), - }; + let post_id = post.id; + let permadelete = move |conn: &'_ _| Post::permadelete(conn, post_id); + if blocking(pool, permadelete).await?.is_err() { + return Err(APIError::err("couldnt_update_post").into()); + } } Ok(LoginResponse { @@ -999,20 +1069,20 @@ impl Perform for Oper { } } +#[async_trait::async_trait(?Send)] impl Perform for Oper { type Response = PasswordResetResponse; - fn perform( + async fn perform( &self, - pool: Pool>, + pool: &DbPool, _websocket_info: Option, - ) -> Result { + ) -> Result { let data: &PasswordReset = &self.data; - let conn = pool.get()?; - // Fetch that email - let user: User_ = match User_::find_by_email(&conn, &data.email) { + let email = data.email.clone(); + let user = match blocking(pool, move |conn| User_::find_by_email(conn, &email)).await? { Ok(user) => user, Err(_e) => return Err(APIError::err("couldnt_find_that_username_or_email").into()), }; @@ -1021,7 +1091,12 @@ impl Perform for Oper { let token = generate_random_string(); // Insert the row - PasswordResetRequest::create_token(&conn, user.id, &token)?; + let token2 = token.clone(); + let user_id = user.id; + blocking(pool, move |conn| { + PasswordResetRequest::create_token(conn, user_id, &token2) + }) + .await??; // Email the pure token to the user. // TODO no i18n support here. @@ -1038,20 +1113,23 @@ impl Perform for Oper { } } +#[async_trait::async_trait(?Send)] impl Perform for Oper { type Response = LoginResponse; - fn perform( + async fn perform( &self, - pool: Pool>, + pool: &DbPool, _websocket_info: Option, - ) -> Result { + ) -> Result { let data: &PasswordChange = &self.data; - let conn = pool.get()?; - // Fetch the user_id from the token - let user_id = PasswordResetRequest::read_from_token(&conn, &data.token)?.user_id; + let token = data.token.clone(); + let user_id = blocking(pool, move |conn| { + PasswordResetRequest::read_from_token(conn, &token).map(|p| p.user_id) + }) + .await??; // Make sure passwords match if data.password != data.password_verify { @@ -1059,7 +1137,12 @@ impl Perform for Oper { } // Update the user with the new password - let updated_user = match User_::update_password(&conn, user_id, &data.password) { + let password = data.password.clone(); + let updated_user = match blocking(pool, move |conn| { + User_::update_password(conn, user_id, &password) + }) + .await? + { Ok(user) => user, Err(_e) => return Err(APIError::err("couldnt_update_user").into()), }; @@ -1071,14 +1154,15 @@ impl Perform for Oper { } } +#[async_trait::async_trait(?Send)] impl Perform for Oper { type Response = PrivateMessageResponse; - fn perform( + async fn perform( &self, - pool: Pool>, + pool: &DbPool, websocket_info: Option, - ) -> Result { + ) -> Result { let data: &CreatePrivateMessage = &self.data; let claims = match Claims::decode(&data.auth) { @@ -1090,10 +1174,8 @@ impl Perform for Oper { let hostname = &format!("https://{}", Settings::get().hostname); - let conn = pool.get()?; - // 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 { return Err(APIError::err("site_ban").into()); } @@ -1112,23 +1194,34 @@ impl Perform for Oper { published: None, }; - let inserted_private_message = match PrivateMessage::create(&conn, &private_message_form) { + let inserted_private_message = match blocking(pool, move |conn| { + PrivateMessage::create(conn, &private_message_form) + }) + .await? + { Ok(private_message) => private_message, Err(_e) => { return Err(APIError::err("couldnt_create_private_message").into()); } }; - let updated_private_message = - match PrivateMessage::update_ap_id(&conn, inserted_private_message.id) { - Ok(private_message) => private_message, - Err(_e) => return Err(APIError::err("couldnt_create_private_message").into()), - }; + let inserted_private_message_id = inserted_private_message.id; + let updated_private_message = match blocking(pool, move |conn| { + PrivateMessage::update_ap_id(&conn, inserted_private_message_id) + }) + .await? + { + Ok(private_message) => private_message, + Err(_e) => return Err(APIError::err("couldnt_create_private_message").into()), + }; - updated_private_message.send_create(&user, &conn)?; + updated_private_message + .send_create(&user, &self.client, pool) + .await?; // Send notifications to the recipient - let recipient_user = User_::read(&conn, data.recipient_id)?; + let recipient_id = data.recipient_id; + let recipient_user = blocking(pool, move |conn| User_::read(conn, recipient_id)).await??; if recipient_user.send_notifications_to_email { if let Some(email) = recipient_user.email { let subject = &format!( @@ -1147,7 +1240,10 @@ impl Perform for Oper { } } - let message = PrivateMessageView::read(&conn, inserted_private_message.id)?; + let message = blocking(pool, move |conn| { + PrivateMessageView::read(conn, inserted_private_message.id) + }) + .await??; let res = PrivateMessageResponse { message }; @@ -1164,14 +1260,15 @@ impl Perform for Oper { } } +#[async_trait::async_trait(?Send)] impl Perform for Oper { type Response = PrivateMessageResponse; - fn perform( + async fn perform( &self, - pool: Pool>, + pool: &DbPool, websocket_info: Option, - ) -> Result { + ) -> Result { let data: &EditPrivateMessage = &self.data; let claims = match Claims::decode(&data.auth) { @@ -1181,12 +1278,12 @@ impl Perform for Oper { let user_id = claims.id; - let conn = pool.get()?; - - let orig_private_message = PrivateMessage::read(&conn, data.edit_id)?; + let edit_id = data.edit_id; + let orig_private_message = + blocking(pool, move |conn| PrivateMessage::read(conn, edit_id)).await??; // 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 { return Err(APIError::err("site_ban").into()); } @@ -1219,23 +1316,34 @@ impl Perform for Oper { published: None, }; - let updated_private_message = - match PrivateMessage::update(&conn, data.edit_id, &private_message_form) { - Ok(private_message) => private_message, - Err(_e) => return Err(APIError::err("couldnt_update_private_message").into()), - }; + let edit_id = data.edit_id; + let updated_private_message = match blocking(pool, move |conn| { + PrivateMessage::update(conn, edit_id, &private_message_form) + }) + .await? + { + Ok(private_message) => private_message, + Err(_e) => return Err(APIError::err("couldnt_update_private_message").into()), + }; if let Some(deleted) = data.deleted.to_owned() { if deleted { - updated_private_message.send_delete(&user, &conn)?; + updated_private_message + .send_delete(&user, &self.client, pool) + .await?; } else { - updated_private_message.send_undo_delete(&user, &conn)?; + updated_private_message + .send_undo_delete(&user, &self.client, pool) + .await?; } } else { - updated_private_message.send_update(&user, &conn)?; + updated_private_message + .send_update(&user, &self.client, pool) + .await?; } - let message = PrivateMessageView::read(&conn, data.edit_id)?; + let edit_id = data.edit_id; + let message = blocking(pool, move |conn| PrivateMessageView::read(conn, edit_id)).await??; let res = PrivateMessageResponse { message }; @@ -1252,14 +1360,15 @@ impl Perform for Oper { } } +#[async_trait::async_trait(?Send)] impl Perform for Oper { type Response = PrivateMessagesResponse; - fn perform( + async fn perform( &self, - pool: Pool>, + pool: &DbPool, _websocket_info: Option, - ) -> Result { + ) -> Result { let data: &GetPrivateMessages = &self.data; let claims = match Claims::decode(&data.auth) { @@ -1269,26 +1378,31 @@ impl Perform for Oper { let user_id = claims.id; - let conn = pool.get()?; - - let messages = PrivateMessageQueryBuilder::create(&conn, user_id) - .page(data.page) - .limit(data.limit) - .unread_only(data.unread_only) - .list()?; + let page = data.page; + let limit = data.limit; + let unread_only = data.unread_only; + let messages = blocking(pool, move |conn| { + PrivateMessageQueryBuilder::create(&conn, user_id) + .page(page) + .limit(limit) + .unread_only(unread_only) + .list() + }) + .await??; Ok(PrivateMessagesResponse { messages }) } } +#[async_trait::async_trait(?Send)] impl Perform for Oper { type Response = UserJoinResponse; - fn perform( + async fn perform( &self, - _pool: Pool>, + _pool: &DbPool, websocket_info: Option, - ) -> Result { + ) -> Result { let data: &UserJoin = &self.data; let claims = match Claims::decode(&data.auth) { diff --git a/server/src/apub/activities.rs b/server/src/apub/activities.rs index b5bb9d76c..e5dc70457 100644 --- a/server/src/apub/activities.rs +++ b/server/src/apub/activities.rs @@ -1,20 +1,22 @@ use crate::{ apub::{extensions::signatures::sign, is_apub_id_valid, ActorType}, db::{activity::insert_activity, community::Community, user::User_}, + request::retry_custom, + DbPool, + LemmyError, }; use activitystreams::{context, object::properties::ObjectProperties, public, Activity, Base}; -use diesel::PgConnection; -use failure::{Error, _core::fmt::Debug}; -use isahc::prelude::*; +use actix_web::client::Client; use log::debug; use serde::Serialize; +use std::fmt::Debug; use url::Url; pub fn populate_object_props( props: &mut ObjectProperties, addressed_ccs: Vec, object_id: &str, -) -> Result<(), Error> { +) -> Result<(), LemmyError> { props .set_context_xsd_any_uri(context())? // TODO: the activity needs a seperate id from the object @@ -26,48 +28,61 @@ pub fn populate_object_props( Ok(()) } -pub fn send_activity_to_community( +pub async fn send_activity_to_community( creator: &User_, - conn: &PgConnection, community: &Community, to: Vec, activity: A, -) -> Result<(), Error> + client: &Client, + pool: &DbPool, +) -> Result<(), LemmyError> 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 community.local { - Community::do_announce(activity, &community, creator, conn)?; + Community::do_announce(activity, &community, creator, client, pool).await?; } else { - send_activity(&activity, creator, to)?; + send_activity(client, &activity, creator, to).await?; } + Ok(()) } /// Send an activity to a list of recipients, using the correct headers etc. -pub fn send_activity(activity: &A, actor: &dyn ActorType, to: Vec) -> Result<(), Error> +pub async fn send_activity( + client: &Client, + activity: &A, + actor: &dyn ActorType, + to: Vec, +) -> Result<(), LemmyError> where - A: Serialize + Debug, + A: Serialize, { - let json = serde_json::to_string(&activity)?; - debug!("Sending activitypub activity {} to {:?}", json, to); + let activity = serde_json::to_string(&activity)?; + debug!("Sending activitypub activity {} to {:?}", activity, to); + for t in to { let to_url = Url::parse(&t)?; if !is_apub_id_valid(&to_url) { debug!("Not sending activity to {} (invalid or blocklisted)", t); continue; } - let request = Request::post(t).header("Host", to_url.domain().unwrap()); - let signature = sign(&request, actor)?; - let res = request - .header("Signature", signature) - .header("Content-Type", "application/json") - .body(json.to_owned())? - .send()?; + + let res = retry_custom(|| async { + let request = client.post(&t).header("Content-Type", "application/json"); + + match sign(request, actor, activity.clone()).await { + Ok(signed) => Ok(signed.send().await), + Err(e) => Err(e), + } + }) + .await?; + debug!("Result for activity send: {:?}", res); } + Ok(()) } diff --git a/server/src/apub/comment.rs b/server/src/apub/comment.rs index 0a513f332..a42a52c2e 100644 --- a/server/src/apub/comment.rs +++ b/server/src/apub/comment.rs @@ -16,6 +16,7 @@ use crate::{ FromApub, ToApub, }, + blocking, convert_datetime, db::{ comment::{Comment, CommentForm}, @@ -26,6 +27,8 @@ use crate::{ }, routes::DbPoolParam, scrape_text_for_mentions, + DbPool, + LemmyError, MentionData, }; use activitystreams::{ @@ -35,9 +38,7 @@ use activitystreams::{ object::{kind::NoteType, properties::ObjectProperties, Note}, }; use activitystreams_new::object::Tombstone; -use actix_web::{body::Body, web::Path, HttpResponse, Result}; -use diesel::PgConnection; -use failure::Error; +use actix_web::{body::Body, client::Client, web::Path, HttpResponse}; use itertools::Itertools; use log::debug; use serde::Deserialize; @@ -51,32 +52,41 @@ pub struct CommentQuery { pub async fn get_apub_comment( info: Path, db: DbPoolParam, -) -> Result, Error> { +) -> Result, LemmyError> { let id = info.comment_id.parse::()?; - let comment = Comment::read(&&db.get()?, id)?; + let comment = blocking(&db, move |conn| Comment::read(conn, id)).await??; + if !comment.deleted { - Ok(create_apub_response(&comment.to_apub(&db.get().unwrap())?)) + Ok(create_apub_response(&comment.to_apub(&db).await?)) } else { Ok(create_apub_tombstone_response(&comment.to_tombstone()?)) } } +#[async_trait::async_trait(?Send)] impl ToApub for Comment { type Response = Note; - fn to_apub(&self, conn: &PgConnection) -> Result { + async fn to_apub(&self, pool: &DbPool) -> Result { let mut comment = Note::default(); let oprops: &mut ObjectProperties = comment.as_mut(); - let creator = User_::read(&conn, self.creator_id)?; - let post = Post::read(&conn, self.post_id)?; - let community = Community::read(&conn, post.community_id)?; + + let creator_id = self.creator_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 // [post_ap_id, Option(parent_comment_ap_id)] let mut in_reply_to_vec = vec![post.ap_id]; if let Some(parent_id) = self.parent_id { - let parent_comment = Comment::read(&conn, parent_id)?; + let parent_comment = blocking(pool, move |conn| Comment::read(conn, parent_id)).await??; + in_reply_to_vec.push(parent_comment.ap_id); } @@ -97,7 +107,7 @@ impl ToApub for Comment { Ok(comment) } - fn to_tombstone(&self) -> Result { + fn to_tombstone(&self) -> Result { create_tombstone( self.deleted, &self.ap_id, @@ -107,27 +117,34 @@ impl ToApub for Comment { } } +#[async_trait::async_trait(?Send)] impl FromApub for CommentForm { type ApubType = Note; /// Parse an ActivityPub note received from another instance into a Lemmy comment - fn from_apub(note: &Note, conn: &PgConnection) -> Result { + async fn from_apub( + note: &Note, + client: &Client, + pool: &DbPool, + ) -> Result { let oprops = ¬e.object_props; let creator_actor_id = &oprops.get_attributed_to_xsd_any_uri().unwrap().to_string(); - let creator = get_or_fetch_and_upsert_remote_user(&creator_actor_id, &conn)?; + + let 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 post_ap_id = in_reply_tos.next().unwrap().to_string(); // This post, or the parent comment might not yet exist on this server yet, fetch them. - let post = get_or_fetch_and_insert_remote_post(&post_ap_id, &conn)?; + 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 // For deeply nested comments, FromApub automatically gets called recursively let parent_id: Option = match in_reply_tos.next() { Some(parent_comment_uri) => { let parent_comment_ap_id = &parent_comment_uri.to_string(); - let parent_comment = get_or_fetch_and_insert_remote_comment(&parent_comment_ap_id, &conn)?; + let parent_comment = + get_or_fetch_and_insert_remote_comment(&parent_comment_ap_id, client, pool).await?; Some(parent_comment.id) } @@ -157,17 +174,27 @@ impl FromApub for CommentForm { } } +#[async_trait::async_trait(?Send)] impl ApubObjectType for Comment { /// Send out information about a newly created comment, to the followers of the community. - fn send_create(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error> { - let note = self.to_apub(conn)?; - let post = Post::read(&conn, self.post_id)?; - let community = Community::read(conn, post.community_id)?; + async fn send_create( + &self, + creator: &User_, + 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 maa: MentionsAndAddresses = - collect_non_local_mentions_and_addresses(&conn, &self.content, &community)?; - let mut create = Create::new(); populate_object_props(&mut create.object_props, maa.addressed_ccs, &id)?; @@ -179,20 +206,29 @@ impl ApubObjectType for Comment { .set_actor_xsd_any_uri(creator.actor_id.to_owned())? .set_object_base_box(note)?; - send_activity_to_community(&creator, &conn, &community, maa.inboxes, create)?; + send_activity_to_community(&creator, &community, maa.inboxes, create, client, pool).await?; Ok(()) } /// Send out information about an edited post, to the followers of the community. - fn send_update(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error> { - let note = self.to_apub(&conn)?; - let post = Post::read(&conn, self.post_id)?; - let community = Community::read(&conn, post.community_id)?; + async fn send_update( + &self, + creator: &User_, + 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 maa: MentionsAndAddresses = - collect_non_local_mentions_and_addresses(&conn, &self.content, &community)?; - let mut update = Update::new(); populate_object_props(&mut update.object_props, maa.addressed_ccs, &id)?; @@ -204,14 +240,24 @@ impl ApubObjectType for Comment { .set_actor_xsd_any_uri(creator.actor_id.to_owned())? .set_object_base_box(note)?; - send_activity_to_community(&creator, &conn, &community, maa.inboxes, update)?; + send_activity_to_community(&creator, &community, maa.inboxes, update, client, pool).await?; Ok(()) } - fn send_delete(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error> { - let note = self.to_apub(&conn)?; - let post = Post::read(&conn, self.post_id)?; - let community = Community::read(&conn, post.community_id)?; + async fn send_delete( + &self, + creator: &User_, + 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 mut delete = Delete::default(); @@ -228,18 +274,29 @@ impl ApubObjectType for Comment { send_activity_to_community( &creator, - &conn, &community, vec![community.get_shared_inbox_url()], delete, - )?; + client, + pool, + ) + .await?; Ok(()) } - fn send_undo_delete(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error> { - let note = self.to_apub(&conn)?; - let post = Post::read(&conn, self.post_id)?; - let community = Community::read(&conn, post.community_id)?; + async fn send_undo_delete( + &self, + creator: &User_, + 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 let id = format!("{}/delete/{}", self.ap_id, uuid::Uuid::new_v4()); @@ -274,18 +331,30 @@ impl ApubObjectType for Comment { send_activity_to_community( &creator, - &conn, &community, vec![community.get_shared_inbox_url()], undo, - )?; + client, + pool, + ) + .await?; Ok(()) } - fn send_remove(&self, mod_: &User_, conn: &PgConnection) -> Result<(), Error> { - let note = self.to_apub(&conn)?; - let post = Post::read(&conn, self.post_id)?; - let community = Community::read(&conn, post.community_id)?; + async fn send_remove( + &self, + mod_: &User_, + 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 mut remove = Remove::default(); @@ -302,18 +371,29 @@ impl ApubObjectType for Comment { send_activity_to_community( &mod_, - &conn, &community, vec![community.get_shared_inbox_url()], remove, - )?; + client, + pool, + ) + .await?; Ok(()) } - fn send_undo_remove(&self, mod_: &User_, conn: &PgConnection) -> Result<(), Error> { - let note = self.to_apub(&conn)?; - let post = Post::read(&conn, self.post_id)?; - let community = Community::read(&conn, post.community_id)?; + async fn send_undo_remove( + &self, + mod_: &User_, + 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 let id = format!("{}/remove/{}", self.ap_id, uuid::Uuid::new_v4()); @@ -347,20 +427,33 @@ impl ApubObjectType for Comment { send_activity_to_community( &mod_, - &conn, &community, vec![community.get_shared_inbox_url()], undo, - )?; + client, + pool, + ) + .await?; Ok(()) } } +#[async_trait::async_trait(?Send)] impl ApubLikeableType for Comment { - fn send_like(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error> { - let note = self.to_apub(&conn)?; - let post = Post::read(&conn, self.post_id)?; - let community = Community::read(&conn, post.community_id)?; + async fn send_like( + &self, + creator: &User_, + 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 mut like = Like::new(); @@ -376,18 +469,30 @@ impl ApubLikeableType for Comment { send_activity_to_community( &creator, - &conn, &community, vec![community.get_shared_inbox_url()], like, - )?; + client, + pool, + ) + .await?; Ok(()) } - fn send_dislike(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error> { - let note = self.to_apub(&conn)?; - let post = Post::read(&conn, self.post_id)?; - let community = Community::read(&conn, post.community_id)?; + async fn send_dislike( + &self, + creator: &User_, + 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 mut dislike = Dislike::new(); @@ -403,18 +508,30 @@ impl ApubLikeableType for Comment { send_activity_to_community( &creator, - &conn, &community, vec![community.get_shared_inbox_url()], dislike, - )?; + client, + pool, + ) + .await?; Ok(()) } - fn send_undo_like(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error> { - let note = self.to_apub(&conn)?; - let post = Post::read(&conn, self.post_id)?; - let community = Community::read(&conn, post.community_id)?; + async fn send_undo_like( + &self, + creator: &User_, + 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 mut like = Like::new(); @@ -446,11 +563,13 @@ impl ApubLikeableType for Comment { send_activity_to_community( &creator, - &conn, &community, vec![community.get_shared_inbox_url()], undo, - )?; + client, + pool, + ) + .await?; Ok(()) } } @@ -464,11 +583,12 @@ struct MentionsAndAddresses { /// This takes a comment, and builds a list of to_addresses, inboxes, /// and mention tags, so they know where to be sent to. /// Addresses are the users / addresses that go in the cc field. -fn collect_non_local_mentions_and_addresses( - conn: &PgConnection, +async fn collect_non_local_mentions_and_addresses( content: &str, community: &Community, -) -> Result { + client: &Client, + pool: &DbPool, +) -> Result { let mut addressed_ccs = vec![community.get_followers_url()]; // Add the mention tag @@ -480,14 +600,17 @@ fn collect_non_local_mentions_and_addresses( // Filter only the non-local ones .filter(|m| !m.is_local()) .collect::>(); + let mut mention_inboxes = Vec::new(); for mention in &mentions { // TODO should it be fetching it every time? - if let Ok(actor_id) = fetch_webfinger_url(mention) { + if let Ok(actor_id) = fetch_webfinger_url(mention, client).await { debug!("mention actor_id: {}", actor_id); addressed_ccs.push(actor_id.to_owned()); - let mention_user = get_or_fetch_and_upsert_remote_user(&actor_id, &conn)?; + + let mention_user = get_or_fetch_and_upsert_remote_user(&actor_id, client, pool).await?; let shared_inbox = mention_user.get_shared_inbox_url(); + mention_inboxes.push(shared_inbox); let mut mention_tag = Mention::new(); mention_tag diff --git a/server/src/apub/community.rs b/server/src/apub/community.rs index 8c8c3b280..f866511c8 100644 --- a/server/src/apub/community.rs +++ b/server/src/apub/community.rs @@ -12,6 +12,7 @@ use crate::{ GroupExt, ToApub, }, + blocking, convert_datetime, db::{ activity::insert_activity, @@ -21,6 +22,8 @@ use crate::{ }, naive_now, routes::DbPoolParam, + DbPool, + LemmyError, }; use activitystreams::{ activity::{Accept, Announce, Delete, Remove, Undo}, @@ -35,22 +38,22 @@ use activitystreams::{ }; use activitystreams_ext::Ext3; use activitystreams_new::{activity::Follow, object::Tombstone}; -use actix_web::{body::Body, web::Path, HttpResponse, Result}; -use diesel::PgConnection; -use failure::{Error, _core::fmt::Debug}; +use actix_web::{body::Body, client::Client, web, HttpResponse}; use itertools::Itertools; use serde::{Deserialize, Serialize}; +use std::fmt::Debug; #[derive(Deserialize)] pub struct CommunityQuery { community_name: String, } +#[async_trait::async_trait(?Send)] impl ToApub for Community { type Response = GroupExt; // Turn a Lemmy Community into an ActivityPub group that can be sent out over the network. - fn to_apub(&self, conn: &PgConnection) -> Result { + async fn to_apub(&self, pool: &DbPool) -> Result { let mut group = Group::default(); let oprops: &mut ObjectProperties = group.as_mut(); @@ -58,10 +61,12 @@ impl ToApub for Community { // then the rest of the moderators // TODO Technically the instance admins can mod the community, but lets // ignore that for now - let moderators = CommunityModeratorView::for_community(&conn, self.id)? - .into_iter() - .map(|m| m.user_actor_id) - .collect(); + let id = self.id; + let moderators = blocking(pool, move |conn| { + CommunityModeratorView::for_community(&conn, id) + }) + .await??; + let moderators = moderators.into_iter().map(|m| m.user_actor_id).collect(); oprops .set_context_xsd_any_uri(context())? @@ -92,7 +97,12 @@ impl ToApub for Community { .set_endpoints(endpoint_props)? .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( group, @@ -102,7 +112,7 @@ impl ToApub for Community { )) } - fn to_tombstone(&self) -> Result { + fn to_tombstone(&self) -> Result { create_tombstone( self.deleted, &self.actor_id, @@ -112,6 +122,7 @@ impl ToApub for Community { } } +#[async_trait::async_trait(?Send)] impl ActorType for Community { fn actor_id(&self) -> String { 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. - 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 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())?)?; 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(()) } - fn send_delete(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error> { - let group = self.to_apub(conn)?; + async fn send_delete( + &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 mut delete = Delete::default(); @@ -162,17 +184,25 @@ impl ActorType for Community { .set_actor_xsd_any_uri(creator.actor_id.to_owned())? .set_object_base_box(BaseBox::from_concrete(group)?)?; - insert_activity(&conn, self.creator_id, &delete, true)?; + 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, // the community was the actor. // But for delete, the creator is the actor, and does the signing - send_activity(&delete, creator, self.get_follower_inboxes(&conn)?)?; + send_activity(client, &delete, creator, inboxes).await?; Ok(()) } - fn send_undo_delete(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error> { - let group = self.to_apub(conn)?; + async fn send_undo_delete( + &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 mut delete = Delete::default(); @@ -203,17 +233,25 @@ impl ActorType for Community { .set_actor_xsd_any_uri(creator.actor_id.to_owned())? .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, // the community was the actor. // But for delete, the creator is the actor, and does the signing - send_activity(&undo, creator, self.get_follower_inboxes(&conn)?)?; + send_activity(client, &undo, creator, inboxes).await?; Ok(()) } - fn send_remove(&self, mod_: &User_, conn: &PgConnection) -> Result<(), Error> { - let group = self.to_apub(conn)?; + async fn send_remove( + &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 mut remove = Remove::default(); @@ -228,17 +266,25 @@ impl ActorType for Community { .set_actor_xsd_any_uri(mod_.actor_id.to_owned())? .set_object_base_box(BaseBox::from_concrete(group)?)?; - insert_activity(&conn, mod_.id, &remove, true)?; + 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, // the community was the actor. // But for delete, the creator is the actor, and does the signing - send_activity(&remove, mod_, self.get_follower_inboxes(&conn)?)?; + send_activity(client, &remove, mod_, inboxes).await?; Ok(()) } - fn send_undo_remove(&self, mod_: &User_, conn: &PgConnection) -> Result<(), Error> { - let group = self.to_apub(conn)?; + async fn send_undo_remove( + &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 mut remove = Remove::default(); @@ -268,51 +314,69 @@ impl ActorType for Community { .set_actor_xsd_any_uri(mod_.actor_id.to_owned())? .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, // the community was the actor. // But for remove , the creator is the actor, and does the signing - send_activity(&undo, mod_, self.get_follower_inboxes(&conn)?)?; + send_activity(client, &undo, mod_, inboxes).await?; Ok(()) } /// For a given community, returns the inboxes of all followers. - fn get_follower_inboxes(&self, conn: &PgConnection) -> Result, Error> { - Ok( - CommunityFollowerView::for_community(conn, self.id)? - .into_iter() - .map(|c| get_shared_inbox(&c.user_actor_id)) - .filter(|s| !s.is_empty()) - .unique() - .collect(), - ) + async fn get_follower_inboxes(&self, pool: &DbPool) -> Result, LemmyError> { + let id = self.id; + + let inboxes = blocking(pool, move |conn| { + CommunityFollowerView::for_community(conn, id) + }) + .await??; + let inboxes = inboxes + .into_iter() + .map(|c| get_shared_inbox(&c.user_actor_id)) + .filter(|s| !s.is_empty()) + .unique() + .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!() } - 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!() } } +#[async_trait::async_trait(?Send)] impl FromApub for CommunityForm { type ApubType = GroupExt; /// Parse an ActivityPub group received from another instance into a Lemmy community. - fn from_apub(group: &GroupExt, conn: &PgConnection) -> Result { + async fn from_apub(group: &GroupExt, client: &Client, pool: &DbPool) -> Result { let group_extensions: &GroupExtension = &group.ext_one; let oprops = &group.inner.object_props; let aprops = &group.ext_two; let public_key: &PublicKey = &group.ext_three.public_key; let mut creator_and_moderator_uris = oprops.get_many_attributed_to_xsd_any_uris().unwrap(); - let creator = creator_and_moderator_uris - .next() - .map(|c| get_or_fetch_and_upsert_remote_user(&c.to_string(), &conn).unwrap()) - .unwrap(); + let creator_uri = creator_and_moderator_uris.next().unwrap(); + + let creator = get_or_fetch_and_upsert_remote_user(creator_uri.as_str(), client, pool).await?; Ok(CommunityForm { name: oprops.get_name_xsd_string().unwrap().to_string(), @@ -342,14 +406,18 @@ impl FromApub for CommunityForm { /// Return the community json over HTTP. pub async fn get_apub_community_http( - info: Path, + info: web::Path, db: DbPoolParam, -) -> Result, Error> { - let community = Community::read_from_name(&&db.get()?, &info.community_name)?; +) -> Result, LemmyError> { + let community = blocking(&db, move |conn| { + Community::read_from_name(conn, &info.community_name) + }) + .await??; + if !community.deleted { - Ok(create_apub_response( - &community.to_apub(&db.get().unwrap())?, - )) + let apub = community.to_apub(&db).await?; + + Ok(create_apub_response(&apub)) } else { 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). pub async fn get_apub_community_followers( - info: Path, + info: web::Path, db: DbPoolParam, -) -> Result, Error> { - let community = Community::read_from_name(&&db.get()?, &info.community_name)?; +) -> Result, LemmyError> { + let community = blocking(&db, move |conn| { + Community::read_from_name(&conn, &info.community_name) + }) + .await??; - let conn = db.get()?; - - //As we are an object, we validated that the community id was valid - let community_followers = CommunityFollowerView::for_community(&conn, community.id).unwrap(); + let community_id = community.id; + let community_followers = blocking(&db, move |conn| { + CommunityFollowerView::for_community(&conn, community_id) + }) + .await??; let mut collection = UnorderedCollection::default(); let oprops: &mut ObjectProperties = collection.as_mut(); @@ -379,12 +451,13 @@ pub async fn get_apub_community_followers( } impl Community { - pub fn do_announce( + pub async fn do_announce( activity: A, community: &Community, sender: &dyn ActorType, - conn: &PgConnection, - ) -> Result + client: &Client, + pool: &DbPool, + ) -> Result where A: Activity + Base + Serialize + Debug, { @@ -399,15 +472,16 @@ impl Community { .set_actor_xsd_any_uri(community.actor_id.to_owned())? .set_object_base_box(BaseBox::from_concrete(activity)?)?; - insert_activity(&conn, community.creator_id, &announce, true)?; + 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 // 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() 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()) } diff --git a/server/src/apub/community_inbox.rs b/server/src/apub/community_inbox.rs index 975f26877..996e0c251 100644 --- a/server/src/apub/community_inbox.rs +++ b/server/src/apub/community_inbox.rs @@ -4,6 +4,7 @@ use crate::{ fetcher::{get_or_fetch_and_upsert_remote_community, get_or_fetch_and_upsert_remote_user}, ActorType, }, + blocking, db::{ activity::insert_activity, community::{Community, CommunityFollower, CommunityFollowerForm}, @@ -11,14 +12,14 @@ use crate::{ Followable, }, routes::{ChatServerParam, DbPoolParam}, + LemmyError, }; use activitystreams::activity::Undo; use activitystreams_new::activity::Follow; -use actix_web::{web, HttpRequest, HttpResponse, Result}; -use diesel::PgConnection; -use failure::{Error, _core::fmt::Debug}; +use actix_web::{client::Client, web, HttpRequest, HttpResponse}; use log::debug; use serde::Deserialize; +use std::fmt::Debug; #[serde(untagged)] #[derive(Deserialize, Debug)] @@ -28,7 +29,7 @@ pub enum CommunityAcceptedObjects { } impl CommunityAcceptedObjects { - fn follow(&self) -> Result { + fn follow(&self) -> Result { match self { CommunityAcceptedObjects::Follow(f) => Ok(f.to_owned()), CommunityAcceptedObjects::Undo(u) => Ok( @@ -49,16 +50,22 @@ pub async fn community_inbox( input: web::Json, path: web::Path, db: DbPoolParam, + client: web::Data, _chat_server: ChatServerParam, -) -> Result { +) -> Result { 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 { - return Err(format_err!( - "Received activity is addressed to remote community {}", - &community.actor_id - )); + return Err( + format_err!( + "Received activity is addressed to remote community {}", + &community.actor_id + ) + .into(), + ); } debug!( "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 community_uri = follow.object.as_single_xsd_any_uri().unwrap().to_string(); - let conn = db.get()?; - - let user = get_or_fetch_and_upsert_remote_user(&user_uri, &conn)?; - let community = get_or_fetch_and_upsert_remote_community(&community_uri, &conn)?; + 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?; verify(&request, &user)?; match input { - CommunityAcceptedObjects::Follow(f) => handle_follow(&f, &user, &community, &conn), - CommunityAcceptedObjects::Undo(u) => handle_undo_follow(&u, &user, &community, &conn), + CommunityAcceptedObjects::Follow(f) => handle_follow(f, user, community, &client, db).await, + 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 /// Accept activity. -fn handle_follow( - follow: &Follow, - user: &User_, - community: &Community, - conn: &PgConnection, -) -> Result { - insert_activity(&conn, user.id, &follow, false)?; +async fn handle_follow( + follow: Follow, + user: User_, + community: Community, + client: &Client, + db: DbPoolParam, +) -> Result { + insert_activity(user.id, follow.clone(), false, &db).await?; let community_follower_form = CommunityFollowerForm { community_id: community.id, @@ -97,27 +103,34 @@ fn handle_follow( }; // 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()) } -fn handle_undo_follow( - undo: &Undo, - user: &User_, - community: &Community, - conn: &PgConnection, -) -> Result { - insert_activity(&conn, user.id, &undo, false)?; +async fn handle_undo_follow( + undo: Undo, + user: User_, + community: Community, + db: DbPoolParam, +) -> Result { + insert_activity(user.id, undo, false, &db).await?; let community_follower_form = CommunityFollowerForm { community_id: community.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()) } diff --git a/server/src/apub/extensions/group_extensions.rs b/server/src/apub/extensions/group_extensions.rs index ece97706c..1c24eef57 100644 --- a/server/src/apub/extensions/group_extensions.rs +++ b/server/src/apub/extensions/group_extensions.rs @@ -1,7 +1,9 @@ -use crate::db::{category::Category, Crud}; +use crate::{ + db::{category::Category, Crud}, + LemmyError, +}; use activitystreams::{ext::Extension, Actor}; use diesel::PgConnection; -use failure::Error; use serde::{Deserialize, Serialize}; #[derive(Clone, Debug, Default, Deserialize, Serialize)] @@ -24,7 +26,7 @@ impl GroupExtension { conn: &PgConnection, category_id: i32, sensitive: bool, - ) -> Result { + ) -> Result { let category = Category::read(conn, category_id)?; let group_category = GroupCategory { identifier: category_id.to_string(), diff --git a/server/src/apub/extensions/signatures.rs b/server/src/apub/extensions/signatures.rs index 4156f0b3f..af46bc5ee 100644 --- a/server/src/apub/extensions/signatures.rs +++ b/server/src/apub/extensions/signatures.rs @@ -1,9 +1,10 @@ -use crate::apub::ActorType; +use crate::{apub::ActorType, LemmyError}; use activitystreams::ext::Extension; -use actix_web::HttpRequest; -use failure::Error; -use http::request::Builder; -use http_signature_normalization::Config; +use actix_web::{client::ClientRequest, HttpRequest}; +use http_signature_normalization_actix::{ + digest::{DigestClient, SignExt}, + Config, +}; use log::debug; use openssl::{ hash::MessageDigest, @@ -12,7 +13,7 @@ use openssl::{ sign::{Signer, Verifier}, }; use serde::{Deserialize, Serialize}; -use std::collections::BTreeMap; +use sha2::{Digest, Sha256}; lazy_static! { static ref HTTP_SIG_CONFIG: Config = Config::new(); @@ -24,7 +25,7 @@ pub struct Keypair { } /// Generate the asymmetric keypair for ActivityPub HTTP signatures. -pub fn generate_actor_keypair() -> Result { +pub fn generate_actor_keypair() -> Result { let rsa = Rsa::generate(2048)?; let pkey = PKey::from_rsa(rsa)?; let public_key = pkey.public_key_to_pem()?; @@ -36,56 +37,41 @@ pub fn generate_actor_keypair() -> Result { } /// Signs request headers with the given keypair. -pub fn sign(request: &Builder, actor: &dyn ActorType) -> Result { +pub async fn sign( + request: ClientRequest, + actor: &dyn ActorType, + activity: String, +) -> Result, LemmyError> { let signing_key_id = format!("{}#main-key", actor.actor_id()); + let private_key = actor.private_key(); - let headers = request - .headers_ref() - .unwrap() - .iter() - .map(|h| -> Result<(String, String), Error> { - Ok((h.0.as_str().to_owned(), h.1.to_str()?.to_owned())) - }) - .collect::, Error>>()?; + let digest_client = request + .signature_with_digest( + HTTP_SIG_CONFIG.clone(), + signing_key_id, + Sha256::new(), + activity, + move |signing_string| { + let private_key = PKey::private_key_from_pem(private_key.as_bytes())?; + let mut signer = Signer::new(MessageDigest::sha256(), &private_key).unwrap(); + signer.update(signing_string.as_bytes()).unwrap(); - 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(); - signer.update(signing_string.as_bytes()).unwrap(); - Ok(base64::encode(signer.sign_to_vec()?)) as Result<_, Error> - })? - .signature_header(); + Ok(base64::encode(signer.sign_to_vec()?)) as Result<_, LemmyError> + }, + ) + .await?; - Ok(signature_header_value) + Ok(digest_client) } -pub fn verify(request: &HttpRequest, actor: &dyn ActorType) -> Result<(), Error> { - let headers = request - .headers() - .iter() - .map(|h| -> Result<(String, String), Error> { - Ok((h.0.as_str().to_owned(), h.1.to_str()?.to_owned())) - }) - .collect::, Error>>()?; - +pub fn verify(request: &HttpRequest, actor: &dyn ActorType) -> Result<(), LemmyError> { let verified = HTTP_SIG_CONFIG .begin_verify( - request.method().as_str(), - request.uri().path_and_query().unwrap().as_str(), - headers, + request.method(), + request.uri().path_and_query(), + request.headers().clone(), )? - .verify(|signature, signing_string| -> Result { + .verify(|signature, signing_string| -> Result { debug!( "Verifying with key {}, message {}", &actor.public_key(), @@ -101,10 +87,7 @@ pub fn verify(request: &HttpRequest, actor: &dyn ActorType) -> Result<(), Error> debug!("verified signature for {}", &request.uri()); Ok(()) } else { - Err(format_err!( - "Invalid signature on request: {}", - &request.uri() - )) + Err(format_err!("Invalid signature on request: {}", &request.uri()).into()) } } diff --git a/server/src/apub/fetcher.rs b/server/src/apub/fetcher.rs index 7f7a3f971..598903d0c 100644 --- a/server/src/apub/fetcher.rs +++ b/server/src/apub/fetcher.rs @@ -1,15 +1,14 @@ use activitystreams::object::Note; -use actix_web::Result; +use actix_web::client::Client; use diesel::{result::Error::NotFound, PgConnection}; -use failure::{Error, _core::fmt::Debug}; -use isahc::prelude::*; use log::debug; use serde::Deserialize; -use std::time::Duration; +use std::{fmt::Debug, time::Duration}; use url::Url; use crate::{ api::site::SearchResponse, + blocking, db::{ comment::{Comment, CommentForm}, comment_view::CommentView, @@ -23,7 +22,10 @@ use crate::{ SearchType, }, naive_now, + request::{retry, RecvError}, routes::nodeinfo::{NodeInfo, NodeInfoWellKnown}, + DbPool, + LemmyError, }; use crate::{ @@ -43,36 +45,50 @@ use chrono::NaiveDateTime; static ACTOR_REFETCH_INTERVAL_SECONDS: i64 = 24 * 60 * 60; // Fetch nodeinfo metadata from a remote instance. -fn _fetch_node_info(domain: &str) -> Result { +async fn _fetch_node_info(client: &Client, domain: &str) -> Result { let well_known_uri = Url::parse(&format!( "{}://{}/.well-known/nodeinfo", get_apub_protocol_string(), domain ))?; - let well_known = fetch_remote_object::(&well_known_uri)?; - Ok(fetch_remote_object::(&well_known.links.href)?) + + let well_known = fetch_remote_object::(client, &well_known_uri).await?; + let nodeinfo = fetch_remote_object::(client, &well_known.links.href).await?; + + Ok(nodeinfo) } /// Fetch any type of ActivityPub object, handling things like HTTP headers, deserialisation, /// timeouts etc. -pub fn fetch_remote_object(url: &Url) -> Result +pub async fn fetch_remote_object( + client: &Client, + url: &Url, +) -> Result where Response: for<'de> Deserialize<'de>, { 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 text = Request::get(url.as_str()) - .header("Accept", APUB_JSON_CONTENT_TYPE) - .connect_timeout(timeout) - .timeout(timeout) - .body(())? - .send()? - .text()?; - let res: Response = serde_json::from_str(&text)?; - Ok(res) + + let json = retry(|| { + client + .get(url.as_str()) + .header("Accept", APUB_JSON_CONTENT_TYPE) + .timeout(timeout) + .send() + }) + .await? + .json() + .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. @@ -92,7 +108,11 @@ pub enum SearchAcceptedObjects { /// http://lemmy_alpha:8540/u/lemmy_alpha, or @lemmy_alpha@lemmy_alpha:8540 /// http://lemmy_alpha:8540/post/3 /// http://lemmy_alpha:8540/comment/2 -pub fn search_by_apub_id(query: &str, conn: &PgConnection) -> Result { +pub async fn search_by_apub_id( + query: &str, + client: &Client, + pool: &DbPool, +) -> Result { // Parse the shorthand query url let query_url = if query.contains('@') { debug!("{}", query); @@ -107,10 +127,10 @@ pub fn search_by_apub_id(query: &str, conn: &PgConnection) -> Result>(); (format!("/c/{}", split2[1]), split[1]) } else { - return Err(format_err!("Invalid search query: {}", query)); + return Err(format_err!("Invalid search query: {}", query).into()); } } 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); @@ -126,22 +146,41 @@ pub fn search_by_apub_id(query: &str, conn: &PgConnection) -> Result(&query_url)? { + + let response = match fetch_remote_object::(client, &query_url).await? { SearchAcceptedObjects::Person(p) => { let user_uri = p.inner.object_props.get_id().unwrap().to_string(); - let user = get_or_fetch_and_upsert_remote_user(&user_uri, &conn)?; - response.users = vec![UserView::read(conn, user.id)?]; + + 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) => { 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 // 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) => { - let p = upsert_post(&PostForm::from_apub(&p, conn)?, conn)?; - response.posts = vec![PostView::read(conn, p.id, None)?]; + let post_form = PostForm::from_apub(&p, client, pool).await?; + + 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) => { let post_url = c @@ -151,41 +190,59 @@ pub fn search_by_apub_id(query: &str, conn: &PgConnection) -> Result Result { - match User_::read_from_actor_id(&conn, &apub_id) { - Ok(u) => { - // If its older than a day, re-fetch it - if !u.local && should_refetch_actor(u.last_refreshed_at) { - debug!("Fetching and updating from remote user: {}", apub_id); - let person = fetch_remote_object::(&Url::parse(apub_id)?)?; - let mut uf = UserForm::from_apub(&person, &conn)?; - uf.last_refreshed_at = Some(naive_now()); - Ok(User_::update(&conn, u.id, &uf)?) - } else { - Ok(u) - } + client: &Client, + pool: &DbPool, +) -> Result { + 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 + Ok(u) if !u.local && should_refetch_actor(u.last_refreshed_at) => { + debug!("Fetching and updating from remote user: {}", apub_id); + let person = fetch_remote_object::(client, &Url::parse(apub_id)?).await?; + + let mut uf = UserForm::from_apub(&person, client, pool).await?; + uf.last_refreshed_at = Some(naive_now()); + let user = blocking(pool, move |conn| User_::update(conn, u.id, &uf)).await??; + + Ok(user) } + Ok(u) => Ok(u), Err(NotFound {}) => { debug!("Fetching and creating remote user: {}", apub_id); - let person = fetch_remote_object::(&Url::parse(apub_id)?)?; - let uf = UserForm::from_apub(&person, &conn)?; - Ok(User_::create(conn, &uf)?) + let person = fetch_remote_object::(client, &Url::parse(apub_id)?).await?; + + 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. -pub fn get_or_fetch_and_upsert_remote_community( +pub async fn get_or_fetch_and_upsert_remote_community( apub_id: &str, - conn: &PgConnection, -) -> Result { - match Community::read_from_actor_id(&conn, &apub_id) { - Ok(c) => { - if !c.local && should_refetch_actor(c.last_refreshed_at) { - debug!("Fetching and updating from remote community: {}", apub_id); - let group = fetch_remote_object::(&Url::parse(apub_id)?)?; - let mut cf = CommunityForm::from_apub(&group, conn)?; - cf.last_refreshed_at = Some(naive_now()); - Ok(Community::update(&conn, c.id, &cf)?) - } else { - Ok(c) - } + client: &Client, + pool: &DbPool, +) -> Result { + let apub_id_owned = apub_id.to_owned(); + 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); + let group = fetch_remote_object::(client, &Url::parse(apub_id)?).await?; + + let mut cf = CommunityForm::from_apub(&group, client, pool).await?; + cf.last_refreshed_at = Some(naive_now()); + let community = blocking(pool, move |conn| Community::update(conn, c.id, &cf)).await??; + + Ok(community) } + Ok(c) => Ok(c), Err(NotFound {}) => { debug!("Fetching and creating remote community: {}", apub_id); - let group = fetch_remote_object::(&Url::parse(apub_id)?)?; - let cf = CommunityForm::from_apub(&group, conn)?; - let community = Community::create(conn, &cf)?; + let group = fetch_remote_object::(client, &Url::parse(apub_id)?).await?; + + 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 let creator_and_moderator_uris = group @@ -232,74 +297,105 @@ pub fn get_or_fetch_and_upsert_remote_community( .object_props .get_many_attributed_to_xsd_any_uris() .unwrap(); - let creator_and_moderators = creator_and_moderator_uris - .map(|c| get_or_fetch_and_upsert_remote_user(&c.to_string(), &conn).unwrap()) - .collect::>(); - for mod_ in creator_and_moderators { - let community_moderator_form = CommunityModeratorForm { - community_id: community.id, - user_id: mod_.id, - }; - CommunityModerator::join(&conn, &community_moderator_form)?; + 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 { + let community_moderator_form = CommunityModeratorForm { + community_id, + user_id: mod_.id, + }; + + CommunityModerator::join(conn, &community_moderator_form)?; + } + Ok(()) as Result<(), LemmyError> + }) + .await??; + Ok(community) } - Err(e) => Err(Error::from(e)), + Err(e) => Err(e.into()), } } -fn upsert_post(post_form: &PostForm, conn: &PgConnection) -> Result { +fn upsert_post(post_form: &PostForm, conn: &PgConnection) -> Result { let existing = Post::read_from_apub_id(conn, &post_form.ap_id); match existing { Err(NotFound {}) => Ok(Post::create(conn, &post_form)?), Ok(p) => Ok(Post::update(conn, p.id, &post_form)?), - Err(e) => Err(Error::from(e)), + 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, - conn: &PgConnection, -) -> Result { - match Post::read_from_apub_id(conn, post_ap_id) { + client: &Client, + pool: &DbPool, +) -> Result { + 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), Err(NotFound {}) => { debug!("Fetching and creating remote post: {}", post_ap_id); - let post = fetch_remote_object::(&Url::parse(post_ap_id)?)?; - let post_form = PostForm::from_apub(&post, conn)?; - Ok(Post::create(conn, &post_form)?) + let post = fetch_remote_object::(client, &Url::parse(post_ap_id)?).await?; + let post_form = PostForm::from_apub(&post, client, pool).await?; + + 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 { +fn upsert_comment(comment_form: &CommentForm, conn: &PgConnection) -> Result { let existing = Comment::read_from_apub_id(conn, &comment_form.ap_id); match existing { Err(NotFound {}) => Ok(Comment::create(conn, &comment_form)?), Ok(p) => Ok(Comment::update(conn, p.id, &comment_form)?), - Err(e) => Err(Error::from(e)), + 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, - conn: &PgConnection, -) -> Result { - match Comment::read_from_apub_id(conn, comment_ap_id) { + client: &Client, + pool: &DbPool, +) -> Result { + 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), Err(NotFound {}) => { debug!( "Fetching and creating remote comment and its parents: {}", comment_ap_id ); - let comment = fetch_remote_object::(&Url::parse(comment_ap_id)?)?; - let comment_form = CommentForm::from_apub(&comment, conn)?; - Ok(Comment::create(conn, &comment_form)?) + let comment = fetch_remote_object::(client, &Url::parse(comment_ap_id)?).await?; + let comment_form = CommentForm::from_apub(&comment, client, pool).await?; + + 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 // and user actors // Fetch all posts in the outbox of the given user, and insert them into the database. -// fn fetch_community_outbox(community: &Community, conn: &PgConnection) -> Result, Error> { +// fn fetch_community_outbox(community: &Community, conn: &PgConnection) -> Result, LemmyError> { // let outbox_url = Url::parse(&community.get_outbox_url())?; // let outbox = fetch_remote_object::(&outbox_url)?; // let items = outbox.collection_props.get_many_items_base_boxes(); @@ -317,11 +413,11 @@ pub fn get_or_fetch_and_insert_remote_comment( // Ok( // items // .unwrap() -// .map(|obox: &BaseBox| -> Result { +// .map(|obox: &BaseBox| -> Result { // let page = obox.clone().to_concrete::()?; // PostForm::from_page(&page, conn) // }) // .map(|pf| upsert_post(&pf?, conn)) -// .collect::, Error>>()?, +// .collect::, LemmyError>>()?, // ) // } diff --git a/server/src/apub/mod.rs b/server/src/apub/mod.rs index 6a2d6cffb..90df87342 100644 --- a/server/src/apub/mod.rs +++ b/server/src/apub/mod.rs @@ -18,7 +18,10 @@ use crate::{ }, convert_datetime, db::user::User_, + request::{retry, RecvError}, routes::webfinger::WebFingerResponse, + DbPool, + LemmyError, MentionData, Settings, }; @@ -28,11 +31,8 @@ use activitystreams::{ }; use activitystreams_ext::{Ext1, Ext2, Ext3}; 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 diesel::PgConnection; -use failure::Error; -use isahc::prelude::*; use log::debug; use serde::Serialize; 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. fn is_apub_id_valid(apub_id: &Url) -> bool { + debug!("Checking {}", apub_id); if apub_id.scheme() != get_apub_protocol_string() { + debug!("invalid scheme: {:?}", apub_id.scheme()); return false; } @@ -112,15 +114,27 @@ fn is_apub_id_valid(apub_id: &Url) -> bool { .map(|d| d.to_string()) .collect(); match apub_id.domain() { - Some(d) => allowed_instances.contains(&d.to_owned()), - None => false, + Some(d) => { + 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 { type Response; - fn to_apub(&self, conn: &PgConnection) -> Result; - fn to_tombstone(&self) -> Result; + async fn to_apub(&self, pool: &DbPool) -> Result; + fn to_tombstone(&self) -> Result; } /// Updated is actually the deletion time @@ -129,7 +143,7 @@ fn create_tombstone( object_id: &str, updated: Option, former_type: String, -) -> Result { +) -> Result { if deleted { if let Some(updated) = updated { let mut tombstone = Tombstone::new(); @@ -138,37 +152,85 @@ fn create_tombstone( tombstone.set_deleted(convert_datetime(updated).into()); Ok(tombstone) } else { - Err(format_err!( - "Cant convert to tombstone because updated time was None." - )) + Err(format_err!("Cant convert to tombstone because updated time was None.").into()) } } else { - Err(format_err!( - "Cant convert object to tombstone if it wasnt deleted" - )) + Err(format_err!("Cant convert object to tombstone if it wasnt deleted").into()) } } +#[async_trait::async_trait(?Send)] pub trait FromApub { type ApubType; - fn from_apub(apub: &Self::ApubType, conn: &PgConnection) -> Result + async fn from_apub( + apub: &Self::ApubType, + client: &Client, + pool: &DbPool, + ) -> Result where Self: Sized; } +#[async_trait::async_trait(?Send)] pub trait ApubObjectType { - fn send_create(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error>; - fn send_update(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error>; - fn send_delete(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error>; - fn send_undo_delete(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error>; - fn send_remove(&self, mod_: &User_, conn: &PgConnection) -> Result<(), Error>; - fn send_undo_remove(&self, mod_: &User_, conn: &PgConnection) -> Result<(), Error>; + async fn send_create( + &self, + creator: &User_, + client: &Client, + pool: &DbPool, + ) -> 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 { - fn send_like(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error>; - fn send_dislike(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error>; - fn send_undo_like(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error>; + async fn send_like( + &self, + 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 { @@ -185,6 +247,7 @@ pub fn get_shared_inbox(actor_id: &str) -> String { ) } +#[async_trait::async_trait(?Send)] pub trait ActorType { 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, // and a user can't be followed (yet) #[allow(unused_variables)] - fn send_follow(&self, follow_actor_id: &str, conn: &PgConnection) -> Result<(), Error>; - fn send_unfollow(&self, follow_actor_id: &str, conn: &PgConnection) -> Result<(), Error>; + async fn send_follow( + &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)] - 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>; - fn send_undo_delete(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error>; + 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>; - fn send_remove(&self, mod_: &User_, conn: &PgConnection) -> Result<(), Error>; - fn send_undo_remove(&self, mod_: &User_, conn: &PgConnection) -> Result<(), Error>; + 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>; /// For a given community, returns the inboxes of all followers. - fn get_follower_inboxes(&self, conn: &PgConnection) -> Result, Error>; + async fn get_follower_inboxes(&self, pool: &DbPool) -> Result, LemmyError>; // TODO move these to the db rows fn get_inbox_url(&self) -> String { @@ -244,7 +342,10 @@ pub trait ActorType { } } -pub fn fetch_webfinger_url(mention: &MentionData) -> Result { +pub async fn fetch_webfinger_url( + mention: &MentionData, + client: &Client, +) -> Result { let fetch_url = format!( "{}://{}/.well-known/webfinger?resource=acct:{}@{}", get_apub_protocol_string(), @@ -253,8 +354,14 @@ pub fn fetch_webfinger_url(mention: &MentionData) -> Result { mention.domain ); 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 .links .iter() @@ -263,5 +370,5 @@ pub fn fetch_webfinger_url(mention: &MentionData) -> Result { link .href .to_owned() - .ok_or_else(|| format_err!("No href found.")) + .ok_or_else(|| format_err!("No href found.").into()) } diff --git a/server/src/apub/post.rs b/server/src/apub/post.rs index 3f86d34d1..60cb0b557 100644 --- a/server/src/apub/post.rs +++ b/server/src/apub/post.rs @@ -14,6 +14,7 @@ use crate::{ PageExt, ToApub, }, + blocking, convert_datetime, db::{ community::Community, @@ -22,6 +23,8 @@ use crate::{ Crud, }, routes::DbPoolParam, + DbPool, + LemmyError, Settings, }; use activitystreams::{ @@ -32,9 +35,7 @@ use activitystreams::{ }; use activitystreams_ext::Ext1; use activitystreams_new::object::Tombstone; -use actix_web::{body::Body, web::Path, HttpResponse, Result}; -use diesel::PgConnection; -use failure::Error; +use actix_web::{body::Body, client::Client, web, HttpResponse}; use serde::Deserialize; #[derive(Deserialize)] @@ -44,27 +45,33 @@ pub struct PostQuery { /// Return the post json over HTTP. pub async fn get_apub_post( - info: Path, + info: web::Path, db: DbPoolParam, -) -> Result, Error> { +) -> Result, LemmyError> { let id = info.post_id.parse::()?; - let post = Post::read(&&db.get()?, id)?; + let post = blocking(&db, move |conn| Post::read(conn, id)).await??; + if !post.deleted { - Ok(create_apub_response(&post.to_apub(&db.get().unwrap())?)) + Ok(create_apub_response(&post.to_apub(&db).await?)) } else { Ok(create_apub_tombstone_response(&post.to_tombstone()?)) } } +#[async_trait::async_trait(?Send)] impl ToApub for Post { type Response = PageExt; // Turn a Lemmy post into an ActivityPub page that can be sent out over the network. - fn to_apub(&self, conn: &PgConnection) -> Result { + async fn to_apub(&self, pool: &DbPool) -> Result { let mut page = Page::default(); let oprops: &mut ObjectProperties = page.as_mut(); - let creator = User_::read(conn, self.creator_id)?; - let community = Community::read(conn, self.community_id)?; + + 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 // 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)) } - fn to_tombstone(&self) -> Result { + fn to_tombstone(&self) -> Result { create_tombstone( self.deleted, &self.ap_id, @@ -151,17 +158,26 @@ impl ToApub for Post { } } +#[async_trait::async_trait(?Send)] impl FromApub for PostForm { type ApubType = PageExt; /// Parse an ActivityPub page received from another instance into a Lemmy post. - fn from_apub(page: &PageExt, conn: &PgConnection) -> Result { + async fn from_apub( + page: &PageExt, + client: &Client, + pool: &DbPool, + ) -> Result { let ext = &page.ext_one; let oprops = &page.inner.object_props; let creator_actor_id = &oprops.get_attributed_to_xsd_any_uri().unwrap().to_string(); - let creator = get_or_fetch_and_upsert_remote_user(&creator_actor_id, &conn)?; + + let 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 = 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() { Some(any_image) => any_image @@ -221,11 +237,20 @@ impl FromApub for PostForm { } } +#[async_trait::async_trait(?Send)] impl ApubObjectType for Post { /// Send out information about a newly created post, to the followers of the community. - fn send_create(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error> { - let page = self.to_apub(conn)?; - let community = Community::read(conn, self.community_id)?; + async fn send_create( + &self, + 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 mut create = Create::new(); @@ -241,18 +266,28 @@ impl ApubObjectType for Post { send_activity_to_community( creator, - conn, &community, vec![community.get_shared_inbox_url()], create, - )?; + client, + pool, + ) + .await?; Ok(()) } /// Send out information about an edited post, to the followers of the community. - fn send_update(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error> { - let page = self.to_apub(conn)?; - let community = Community::read(conn, self.community_id)?; + async fn send_update( + &self, + 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 mut update = Update::new(); @@ -268,17 +303,27 @@ impl ApubObjectType for Post { send_activity_to_community( creator, - conn, &community, vec![community.get_shared_inbox_url()], update, - )?; + client, + pool, + ) + .await?; Ok(()) } - fn send_delete(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error> { - let page = self.to_apub(conn)?; - let community = Community::read(conn, self.community_id)?; + async fn send_delete( + &self, + 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 mut delete = Delete::default(); @@ -293,21 +338,29 @@ impl ApubObjectType for Post { .set_actor_xsd_any_uri(creator.actor_id.to_owned())? .set_object_base_box(BaseBox::from_concrete(page)?)?; - let community = Community::read(conn, self.community_id)?; - send_activity_to_community( creator, - conn, &community, vec![community.get_shared_inbox_url()], delete, - )?; + client, + pool, + ) + .await?; Ok(()) } - fn send_undo_delete(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error> { - let page = self.to_apub(conn)?; - let community = Community::read(conn, self.community_id)?; + async fn send_undo_delete( + &self, + 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 mut delete = Delete::default(); @@ -338,20 +391,29 @@ impl ApubObjectType for Post { .set_actor_xsd_any_uri(creator.actor_id.to_owned())? .set_object_base_box(delete)?; - let community = Community::read(conn, self.community_id)?; send_activity_to_community( creator, - conn, &community, vec![community.get_shared_inbox_url()], undo, - )?; + client, + pool, + ) + .await?; Ok(()) } - fn send_remove(&self, mod_: &User_, conn: &PgConnection) -> Result<(), Error> { - let page = self.to_apub(conn)?; - let community = Community::read(conn, self.community_id)?; + async fn send_remove( + &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 mut remove = Remove::default(); @@ -366,20 +428,29 @@ impl ApubObjectType for Post { .set_actor_xsd_any_uri(mod_.actor_id.to_owned())? .set_object_base_box(BaseBox::from_concrete(page)?)?; - let community = Community::read(conn, self.community_id)?; - send_activity_to_community( mod_, - conn, &community, vec![community.get_shared_inbox_url()], remove, - )?; + client, + pool, + ) + .await?; Ok(()) } - fn send_undo_remove(&self, mod_: &User_, conn: &PgConnection) -> Result<(), Error> { - let page = self.to_apub(conn)?; - let community = Community::read(conn, self.community_id)?; + + async fn send_undo_remove( + &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 mut remove = Remove::default(); @@ -409,22 +480,32 @@ impl ApubObjectType for Post { .set_actor_xsd_any_uri(mod_.actor_id.to_owned())? .set_object_base_box(remove)?; - let community = Community::read(conn, self.community_id)?; send_activity_to_community( mod_, - conn, &community, vec![community.get_shared_inbox_url()], undo, - )?; + client, + pool, + ) + .await?; Ok(()) } } +#[async_trait::async_trait(?Send)] impl ApubLikeableType for Post { - fn send_like(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error> { - let page = self.to_apub(conn)?; - let community = Community::read(conn, self.community_id)?; + async fn send_like( + &self, + 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 mut like = Like::new(); @@ -440,17 +521,27 @@ impl ApubLikeableType for Post { send_activity_to_community( &creator, - &conn, &community, vec![community.get_shared_inbox_url()], like, - )?; + client, + pool, + ) + .await?; Ok(()) } - fn send_dislike(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error> { - let page = self.to_apub(conn)?; - let community = Community::read(conn, self.community_id)?; + async fn send_dislike( + &self, + 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 mut dislike = Dislike::new(); @@ -466,17 +557,27 @@ impl ApubLikeableType for Post { send_activity_to_community( &creator, - &conn, &community, vec![community.get_shared_inbox_url()], dislike, - )?; + client, + pool, + ) + .await?; Ok(()) } - fn send_undo_like(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error> { - let page = self.to_apub(conn)?; - let community = Community::read(conn, self.community_id)?; + async fn send_undo_like( + &self, + 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 mut like = Like::new(); @@ -508,11 +609,13 @@ impl ApubLikeableType for Post { send_activity_to_community( &creator, - &conn, &community, vec![community.get_shared_inbox_url()], undo, - )?; + client, + pool, + ) + .await?; Ok(()) } } diff --git a/server/src/apub/private_message.rs b/server/src/apub/private_message.rs index a700043ba..ae4c36267 100644 --- a/server/src/apub/private_message.rs +++ b/server/src/apub/private_message.rs @@ -7,6 +7,7 @@ use crate::{ FromApub, ToApub, }, + blocking, convert_datetime, db::{ activity::insert_activity, @@ -14,6 +15,8 @@ use crate::{ user::User_, Crud, }, + DbPool, + LemmyError, }; use activitystreams::{ activity::{Create, Delete, Undo, Update}, @@ -21,18 +24,21 @@ use activitystreams::{ object::{kind::NoteType, properties::ObjectProperties, Note}, }; use activitystreams_new::object::Tombstone; -use actix_web::Result; -use diesel::PgConnection; -use failure::Error; +use actix_web::client::Client; +#[async_trait::async_trait(?Send)] impl ToApub for PrivateMessage { type Response = Note; - fn to_apub(&self, conn: &PgConnection) -> Result { + async fn to_apub(&self, pool: &DbPool) -> Result { let mut private_message = Note::default(); let oprops: &mut ObjectProperties = private_message.as_mut(); - let creator = User_::read(&conn, self.creator_id)?; - let recipient = User_::read(&conn, self.recipient_id)?; + + 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 .set_context_xsd_any_uri(context())? @@ -49,7 +55,7 @@ impl ToApub for PrivateMessage { Ok(private_message) } - fn to_tombstone(&self) -> Result { + fn to_tombstone(&self) -> Result { create_tombstone( self.deleted, &self.ap_id, @@ -59,16 +65,24 @@ impl ToApub for PrivateMessage { } } +#[async_trait::async_trait(?Send)] impl FromApub for PrivateMessageForm { type ApubType = Note; /// Parse an ActivityPub note received from another instance into a Lemmy Private message - fn from_apub(note: &Note, conn: &PgConnection) -> Result { + async fn from_apub( + note: &Note, + client: &Client, + pool: &DbPool, + ) -> Result { let oprops = ¬e.object_props; let creator_actor_id = &oprops.get_attributed_to_xsd_any_uri().unwrap().to_string(); - let creator = get_or_fetch_and_upsert_remote_user(&creator_actor_id, &conn)?; + + let 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 = 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 { creator_id: creator.id, @@ -91,12 +105,20 @@ impl FromApub for PrivateMessageForm { } } +#[async_trait::async_trait(?Send)] impl ApubObjectType for PrivateMessage { /// Send out information about a newly created private message - fn send_create(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error> { - let note = self.to_apub(conn)?; + async fn send_create( + &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 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(); create @@ -110,17 +132,24 @@ impl ApubObjectType for PrivateMessage { .set_actor_xsd_any_uri(creator.actor_id.to_owned())? .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(()) } /// Send out information about an edited post, to the followers of the community. - fn send_update(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error> { - let note = self.to_apub(conn)?; + async fn send_update( + &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 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(); update @@ -134,16 +163,23 @@ impl ApubObjectType for PrivateMessage { .set_actor_xsd_any_uri(creator.actor_id.to_owned())? .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(()) } - fn send_delete(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error> { - let note = self.to_apub(conn)?; + async fn send_delete( + &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 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(); delete @@ -157,16 +193,23 @@ impl ApubObjectType for PrivateMessage { .set_actor_xsd_any_uri(creator.actor_id.to_owned())? .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(()) } - fn send_undo_delete(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error> { - let note = self.to_apub(conn)?; + async fn send_undo_delete( + &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 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(); delete @@ -195,17 +238,27 @@ impl ApubObjectType for PrivateMessage { .set_actor_xsd_any_uri(creator.actor_id.to_owned())? .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(()) } - fn send_remove(&self, _mod_: &User_, _conn: &PgConnection) -> Result<(), Error> { + async fn send_remove( + &self, + _mod_: &User_, + _client: &Client, + _pool: &DbPool, + ) -> Result<(), LemmyError> { 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!() } } diff --git a/server/src/apub/shared_inbox.rs b/server/src/apub/shared_inbox.rs index 1ada6ad1d..667732523 100644 --- a/server/src/apub/shared_inbox.rs +++ b/server/src/apub/shared_inbox.rs @@ -16,6 +16,7 @@ use crate::{ GroupExt, PageExt, }, + blocking, db::{ activity::insert_activity, comment::{Comment, CommentForm, CommentLike, CommentLikeForm}, @@ -34,6 +35,8 @@ use crate::{ server::{SendComment, SendCommunityRoomMessage, SendPost}, UserOperation, }, + DbPool, + LemmyError, }; use activitystreams::{ activity::{Announce, Create, Delete, Dislike, Like, Remove, Undo, Update}, @@ -42,11 +45,10 @@ use activitystreams::{ Base, BaseBox, }; -use actix_web::{web, HttpRequest, HttpResponse, Result}; -use diesel::PgConnection; -use failure::{Error, _core::fmt::Debug}; +use actix_web::{client::Client, web, HttpRequest, HttpResponse}; use log::debug; use serde::{Deserialize, Serialize}; +use std::fmt::Debug; #[serde(untagged)] #[derive(Serialize, Deserialize, Debug)] @@ -112,11 +114,13 @@ impl SharedAcceptedObjects { pub async fn shared_inbox( request: HttpRequest, input: web::Json, - db: DbPoolParam, + client: web::Data, + pool: DbPoolParam, chat_server: ChatServerParam, -) -> Result { +) -> Result { let activity = input.into_inner(); - let conn = &db.get().unwrap(); + let pool = &pool; + let client = &client; let json = serde_json::to_string(&activity)?; debug!("Shared inbox received activity: {}", json); @@ -128,112 +132,120 @@ pub async fn shared_inbox( let to = cc.replace("/followers", ""); // TODO: this is ugly - match get_or_fetch_and_upsert_remote_user(&sender.to_string(), &conn) { - Ok(u) => verify(&request, &u), + match get_or_fetch_and_upsert_remote_user(&sender.to_string(), &client, pool).await { + Ok(u) => verify(&request, &u)?, Err(_) => { - let c = get_or_fetch_and_upsert_remote_community(&sender.to_string(), &conn)?; - verify(&request, &c) + let c = get_or_fetch_and_upsert_remote_community(&sender.to_string(), &client, pool).await?; + verify(&request, &c)?; } - }?; + } match (activity, object.kind()) { (SharedAcceptedObjects::Create(c), Some("Page")) => { - receive_create_post(&c, &conn, chat_server)?; - announce_activity_if_valid::(*c, &to, sender, conn) + receive_create_post((*c).clone(), client, pool, chat_server).await?; + announce_activity_if_valid::(*c, &to, sender, client, pool).await } (SharedAcceptedObjects::Update(u), Some("Page")) => { - receive_update_post(&u, &conn, chat_server)?; - announce_activity_if_valid::(*u, &to, sender, conn) + receive_update_post((*u).clone(), client, pool, chat_server).await?; + announce_activity_if_valid::(*u, &to, sender, client, pool).await } (SharedAcceptedObjects::Like(l), Some("Page")) => { - receive_like_post(&l, &conn, chat_server)?; - announce_activity_if_valid::(*l, &to, sender, conn) + receive_like_post((*l).clone(), client, pool, chat_server).await?; + announce_activity_if_valid::(*l, &to, sender, client, pool).await } (SharedAcceptedObjects::Dislike(d), Some("Page")) => { - receive_dislike_post(&d, &conn, chat_server)?; - announce_activity_if_valid::(*d, &to, sender, conn) + receive_dislike_post((*d).clone(), client, pool, chat_server).await?; + announce_activity_if_valid::(*d, &to, sender, client, pool).await } (SharedAcceptedObjects::Delete(d), Some("Page")) => { - receive_delete_post(&d, &conn, chat_server)?; - announce_activity_if_valid::(*d, &to, sender, conn) + receive_delete_post((*d).clone(), client, pool, chat_server).await?; + announce_activity_if_valid::(*d, &to, sender, client, pool).await } (SharedAcceptedObjects::Remove(r), Some("Page")) => { - receive_remove_post(&r, &conn, chat_server)?; - announce_activity_if_valid::(*r, &to, sender, conn) + receive_remove_post((*r).clone(), client, pool, chat_server).await?; + announce_activity_if_valid::(*r, &to, sender, client, pool).await } (SharedAcceptedObjects::Create(c), Some("Note")) => { - receive_create_comment(&c, &conn, chat_server)?; - announce_activity_if_valid::(*c, &to, sender, conn) + receive_create_comment((*c).clone(), client, pool, chat_server).await?; + announce_activity_if_valid::(*c, &to, sender, client, pool).await } (SharedAcceptedObjects::Update(u), Some("Note")) => { - receive_update_comment(&u, &conn, chat_server)?; - announce_activity_if_valid::(*u, &to, sender, conn) + receive_update_comment((*u).clone(), client, pool, chat_server).await?; + announce_activity_if_valid::(*u, &to, sender, client, pool).await } (SharedAcceptedObjects::Like(l), Some("Note")) => { - receive_like_comment(&l, &conn, chat_server)?; - announce_activity_if_valid::(*l, &to, sender, conn) + receive_like_comment((*l).clone(), client, pool, chat_server).await?; + announce_activity_if_valid::(*l, &to, sender, client, pool).await } (SharedAcceptedObjects::Dislike(d), Some("Note")) => { - receive_dislike_comment(&d, &conn, chat_server)?; - announce_activity_if_valid::(*d, &to, sender, conn) + receive_dislike_comment((*d).clone(), client, pool, chat_server).await?; + announce_activity_if_valid::(*d, &to, sender, client, pool).await } (SharedAcceptedObjects::Delete(d), Some("Note")) => { - receive_delete_comment(&d, &conn, chat_server)?; - announce_activity_if_valid::(*d, &to, sender, conn) + receive_delete_comment((*d).clone(), client, pool, chat_server).await?; + announce_activity_if_valid::(*d, &to, sender, client, pool).await } (SharedAcceptedObjects::Remove(r), Some("Note")) => { - receive_remove_comment(&r, &conn, chat_server)?; - announce_activity_if_valid::(*r, &to, sender, conn) + receive_remove_comment((*r).clone(), client, pool, chat_server).await?; + announce_activity_if_valid::(*r, &to, sender, client, pool).await } (SharedAcceptedObjects::Delete(d), Some("Group")) => { - receive_delete_community(&d, &conn, chat_server)?; - announce_activity_if_valid::(*d, &to, sender, conn) + receive_delete_community((*d).clone(), client, pool, chat_server).await?; + announce_activity_if_valid::(*d, &to, sender, client, pool).await } (SharedAcceptedObjects::Remove(r), Some("Group")) => { - receive_remove_community(&r, &conn, chat_server)?; - announce_activity_if_valid::(*r, &to, sender, conn) + receive_remove_community((*r).clone(), client, pool, chat_server).await?; + announce_activity_if_valid::(*r, &to, sender, client, pool).await } (SharedAcceptedObjects::Undo(u), Some("Delete")) => { - receive_undo_delete(&u, &conn, chat_server)?; - announce_activity_if_valid::(*u, &to, sender, conn) + receive_undo_delete((*u).clone(), client, pool, chat_server).await?; + announce_activity_if_valid::(*u, &to, sender, client, pool).await } (SharedAcceptedObjects::Undo(u), Some("Remove")) => { - receive_undo_remove(&u, &conn, chat_server)?; - announce_activity_if_valid::(*u, &to, sender, conn) + receive_undo_remove((*u).clone(), client, pool, chat_server).await?; + announce_activity_if_valid::(*u, &to, sender, client, pool).await } (SharedAcceptedObjects::Undo(u), Some("Like")) => { - receive_undo_like(&u, &conn, chat_server)?; - announce_activity_if_valid::(*u, &to, sender, conn) + receive_undo_like((*u).clone(), client, pool, chat_server).await?; + announce_activity_if_valid::(*u, &to, sender, client, pool).await } - (SharedAcceptedObjects::Announce(a), _) => receive_announce(a, &conn, chat_server), + (SharedAcceptedObjects::Announce(a), _) => receive_announce(a, client, pool, chat_server).await, (a, _) => receive_unhandled_activity(a), } } // TODO: should pass in sender as ActorType, but thats a bit tricky in shared_inbox() -fn announce_activity_if_valid( +async fn announce_activity_if_valid( activity: A, community_uri: &str, sender: &str, - conn: &PgConnection, -) -> Result + client: &Client, + pool: &DbPool, +) -> Result where A: Activity + Base + Serialize + Debug, { - let community = Community::read_from_actor_id(conn, &community_uri)?; + let community_uri = community_uri.to_owned(); + let community = blocking(pool, move |conn| { + Community::read_from_actor_id(conn, &community_uri) + }) + .await??; + if community.local { - let sending_user = get_or_fetch_and_upsert_remote_user(&sender.to_string(), &conn)?; - Community::do_announce(activity, &community, &sending_user, conn) + let sending_user = get_or_fetch_and_upsert_remote_user(sender, client, pool).await?; + + Community::do_announce(activity, &community, &sending_user, client, pool).await } else { Ok(HttpResponse::NotFound().finish()) } } -fn receive_announce( +async fn receive_announce( announce: Box, - conn: &PgConnection, + client: &Client, + pool: &DbPool, chat_server: ChatServerParam, -) -> Result { +) -> Result { let object = announce .announce_props .get_object_base_box() @@ -245,8 +257,8 @@ fn receive_announce( let create = object.into_concrete::()?; let inner_object = create.create_props.get_object_base_box().unwrap(); match inner_object.kind() { - Some("Page") => receive_create_post(&create, &conn, chat_server), - Some("Note") => receive_create_comment(&create, &conn, chat_server), + Some("Page") => receive_create_post(create, client, pool, chat_server).await, + Some("Note") => receive_create_comment(create, client, pool, chat_server).await, _ => receive_unhandled_activity(announce), } } @@ -254,8 +266,8 @@ fn receive_announce( let update = object.into_concrete::()?; let inner_object = update.update_props.get_object_base_box().unwrap(); match inner_object.kind() { - Some("Page") => receive_update_post(&update, &conn, chat_server), - Some("Note") => receive_update_comment(&update, &conn, chat_server), + Some("Page") => receive_update_post(update, client, pool, chat_server).await, + Some("Note") => receive_update_comment(update, client, pool, chat_server).await, _ => receive_unhandled_activity(announce), } } @@ -263,8 +275,8 @@ fn receive_announce( let like = object.into_concrete::()?; let inner_object = like.like_props.get_object_base_box().unwrap(); match inner_object.kind() { - Some("Page") => receive_like_post(&like, &conn, chat_server), - Some("Note") => receive_like_comment(&like, &conn, chat_server), + Some("Page") => receive_like_post(like, client, pool, chat_server).await, + Some("Note") => receive_like_comment(like, client, pool, chat_server).await, _ => receive_unhandled_activity(announce), } } @@ -272,8 +284,8 @@ fn receive_announce( let dislike = object.into_concrete::()?; let inner_object = dislike.dislike_props.get_object_base_box().unwrap(); match inner_object.kind() { - Some("Page") => receive_dislike_post(&dislike, &conn, chat_server), - Some("Note") => receive_dislike_comment(&dislike, &conn, chat_server), + Some("Page") => receive_dislike_post(dislike, client, pool, chat_server).await, + Some("Note") => receive_dislike_comment(dislike, client, pool, chat_server).await, _ => receive_unhandled_activity(announce), } } @@ -281,8 +293,8 @@ fn receive_announce( let delete = object.into_concrete::()?; let inner_object = delete.delete_props.get_object_base_box().unwrap(); match inner_object.kind() { - Some("Page") => receive_delete_post(&delete, &conn, chat_server), - Some("Note") => receive_delete_comment(&delete, &conn, chat_server), + Some("Page") => receive_delete_post(delete, client, pool, chat_server).await, + Some("Note") => receive_delete_comment(delete, client, pool, chat_server).await, _ => receive_unhandled_activity(announce), } } @@ -290,8 +302,8 @@ fn receive_announce( let remove = object.into_concrete::()?; let inner_object = remove.remove_props.get_object_base_box().unwrap(); match inner_object.kind() { - Some("Page") => receive_remove_post(&remove, &conn, chat_server), - Some("Note") => receive_remove_comment(&remove, &conn, chat_server), + Some("Page") => receive_remove_post(remove, client, pool, chat_server).await, + Some("Note") => receive_remove_comment(remove, client, pool, chat_server).await, _ => receive_unhandled_activity(announce), } } @@ -299,9 +311,9 @@ fn receive_announce( let undo = object.into_concrete::()?; let inner_object = undo.undo_props.get_object_base_box().unwrap(); match inner_object.kind() { - Some("Delete") => receive_undo_delete(&undo, &conn, chat_server), - Some("Remove") => receive_undo_remove(&undo, &conn, chat_server), - Some("Like") => receive_undo_like(&undo, &conn, chat_server), + Some("Delete") => receive_undo_delete(undo, client, pool, chat_server).await, + Some("Remove") => receive_undo_remove(undo, client, pool, chat_server).await, + Some("Like") => receive_undo_like(undo, client, pool, chat_server).await, _ => receive_unhandled_activity(announce), } } @@ -309,7 +321,7 @@ fn receive_announce( } } -fn receive_unhandled_activity(activity: A) -> Result +fn receive_unhandled_activity(activity: A) -> Result where A: Debug, { @@ -317,11 +329,12 @@ where Ok(HttpResponse::NotImplemented().finish()) } -fn receive_create_post( - create: &Create, - conn: &PgConnection, +async fn receive_create_post( + create: Create, + client: &Client, + pool: &DbPool, chat_server: ChatServerParam, -) -> Result { +) -> Result { let page = create .create_props .get_object_base_box() @@ -336,15 +349,20 @@ fn receive_create_post( .unwrap() .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?; - insert_activity(&conn, user.id, &create, false)?; + insert_activity(user.id, create, false, pool).await?; - let post = PostForm::from_apub(&page, &conn)?; - let inserted_post = Post::create(conn, &post)?; + let post = PostForm::from_apub(&page, client, pool).await?; + + let inserted_post = blocking(pool, move |conn| Post::create(conn, &post)).await??; // Refetch the view - let post_view = PostView::read(&conn, inserted_post.id, None)?; + let inserted_post_id = inserted_post.id; + let post_view = blocking(pool, move |conn| { + PostView::read(conn, inserted_post_id, None) + }) + .await??; let res = PostResponse { post: post_view }; @@ -357,11 +375,12 @@ fn receive_create_post( Ok(HttpResponse::Ok().finish()) } -fn receive_create_comment( - create: &Create, - conn: &PgConnection, +async fn receive_create_comment( + create: Create, + client: &Client, + pool: &DbPool, chat_server: ChatServerParam, -) -> Result { +) -> Result { let note = create .create_props .get_object_base_box() @@ -376,23 +395,30 @@ fn receive_create_comment( .unwrap() .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?; - insert_activity(&conn, user.id, &create, false)?; + insert_activity(user.id, create, false, pool).await?; - let comment = CommentForm::from_apub(¬e, &conn)?; - let inserted_comment = Comment::create(conn, &comment)?; - let post = Post::read(&conn, inserted_comment.post_id)?; + let comment = CommentForm::from_apub(¬e, client, pool).await?; + + let inserted_comment = blocking(pool, move |conn| Comment::create(conn, &comment)).await??; + + let post_id = inserted_comment.post_id; + let post = blocking(pool, move |conn| Post::read(conn, post_id)).await??; // Note: // Although mentions could be gotten from the post tags (they are included there), or the ccs, // Its much easier to scrape them from the comment body, since the API has to do that // anyway. let mentions = scrape_text_for_mentions(&inserted_comment.content); - let recipient_ids = send_local_notifs(&conn, &mentions, &inserted_comment, &user, &post); + let recipient_ids = + send_local_notifs(mentions, inserted_comment.clone(), user, post, pool).await?; // Refetch the view - let comment_view = CommentView::read(&conn, inserted_comment.id, None)?; + let comment_view = blocking(pool, move |conn| { + CommentView::read(conn, inserted_comment.id, None) + }) + .await??; let res = CommentResponse { comment: comment_view, @@ -408,11 +434,12 @@ fn receive_create_comment( Ok(HttpResponse::Ok().finish()) } -fn receive_update_post( - update: &Update, - conn: &PgConnection, +async fn receive_update_post( + update: Update, + client: &Client, + pool: &DbPool, chat_server: ChatServerParam, -) -> Result { +) -> Result { let page = update .update_props .get_object_base_box() @@ -427,16 +454,20 @@ fn receive_update_post( .unwrap() .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?; - insert_activity(&conn, user.id, &update, false)?; + insert_activity(user.id, update, false, pool).await?; - let post = PostForm::from_apub(&page, conn)?; - let post_id = get_or_fetch_and_insert_remote_post(&post.ap_id, &conn)?.id; - Post::update(conn, post_id, &post)?; + let post = PostForm::from_apub(&page, client, pool).await?; + + let post_id = get_or_fetch_and_insert_remote_post(&post.ap_id, client, pool) + .await? + .id; + + blocking(pool, move |conn| Post::update(conn, post_id, &post)).await??; // Refetch the view - let post_view = PostView::read(&conn, post_id, None)?; + let post_view = blocking(pool, move |conn| PostView::read(conn, post_id, None)).await??; let res = PostResponse { post: post_view }; @@ -449,12 +480,12 @@ fn receive_update_post( Ok(HttpResponse::Ok().finish()) } -fn receive_like_post( - like: &Like, - - conn: &PgConnection, +async fn receive_like_post( + like: Like, + client: &Client, + pool: &DbPool, chat_server: ChatServerParam, -) -> Result { +) -> Result { let page = like .like_props .get_object_base_box() @@ -465,23 +496,29 @@ fn receive_like_post( let user_uri = like.like_props.get_actor_xsd_any_uri().unwrap().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?; - insert_activity(&conn, user.id, &like, false)?; + insert_activity(user.id, like, false, pool).await?; - let post = PostForm::from_apub(&page, conn)?; - let post_id = get_or_fetch_and_insert_remote_post(&post.ap_id, &conn)?.id; + let post = PostForm::from_apub(&page, client, pool).await?; + + let post_id = get_or_fetch_and_insert_remote_post(&post.ap_id, client, pool) + .await? + .id; let like_form = PostLikeForm { post_id, user_id: user.id, score: 1, }; - PostLike::remove(&conn, &like_form)?; - PostLike::like(&conn, &like_form)?; + blocking(pool, move |conn| { + PostLike::remove(conn, &like_form)?; + PostLike::like(conn, &like_form) + }) + .await??; // Refetch the view - let post_view = PostView::read(&conn, post_id, None)?; + let post_view = blocking(pool, move |conn| PostView::read(conn, post_id, None)).await??; let res = PostResponse { post: post_view }; @@ -494,12 +531,12 @@ fn receive_like_post( Ok(HttpResponse::Ok().finish()) } -fn receive_dislike_post( - dislike: &Dislike, - - conn: &PgConnection, +async fn receive_dislike_post( + dislike: Dislike, + client: &Client, + pool: &DbPool, chat_server: ChatServerParam, -) -> Result { +) -> Result { let page = dislike .dislike_props .get_object_base_box() @@ -514,23 +551,29 @@ fn receive_dislike_post( .unwrap() .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?; - insert_activity(&conn, user.id, &dislike, false)?; + insert_activity(user.id, dislike, false, pool).await?; - let post = PostForm::from_apub(&page, conn)?; - let post_id = get_or_fetch_and_insert_remote_post(&post.ap_id, &conn)?.id; + let post = PostForm::from_apub(&page, client, pool).await?; + + let post_id = get_or_fetch_and_insert_remote_post(&post.ap_id, client, pool) + .await? + .id; let like_form = PostLikeForm { post_id, user_id: user.id, score: -1, }; - PostLike::remove(&conn, &like_form)?; - PostLike::like(&conn, &like_form)?; + blocking(pool, move |conn| { + PostLike::remove(conn, &like_form)?; + PostLike::like(conn, &like_form) + }) + .await??; // Refetch the view - let post_view = PostView::read(&conn, post_id, None)?; + let post_view = blocking(pool, move |conn| PostView::read(conn, post_id, None)).await??; let res = PostResponse { post: post_view }; @@ -543,12 +586,12 @@ fn receive_dislike_post( Ok(HttpResponse::Ok().finish()) } -fn receive_update_comment( - update: &Update, - - conn: &PgConnection, +async fn receive_update_comment( + update: Update, + client: &Client, + pool: &DbPool, chat_server: ChatServerParam, -) -> Result { +) -> Result { let note = update .update_props .get_object_base_box() @@ -563,20 +606,30 @@ fn receive_update_comment( .unwrap() .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?; - insert_activity(&conn, user.id, &update, false)?; + insert_activity(user.id, update, false, pool).await?; - let comment = CommentForm::from_apub(¬e, &conn)?; - let comment_id = get_or_fetch_and_insert_remote_comment(&comment.ap_id, &conn)?.id; - let updated_comment = Comment::update(conn, comment_id, &comment)?; - let post = Post::read(&conn, updated_comment.post_id)?; + let comment = CommentForm::from_apub(¬e, client, pool).await?; + + let comment_id = get_or_fetch_and_insert_remote_comment(&comment.ap_id, client, pool) + .await? + .id; + + let updated_comment = blocking(pool, move |conn| { + Comment::update(conn, comment_id, &comment) + }) + .await??; + + let post_id = updated_comment.post_id; + let post = blocking(pool, move |conn| Post::read(conn, post_id)).await??; let mentions = scrape_text_for_mentions(&updated_comment.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?; // Refetch the view - let comment_view = CommentView::read(&conn, comment_id, None)?; + let comment_view = + blocking(pool, move |conn| CommentView::read(conn, comment_id, None)).await??; let res = CommentResponse { comment: comment_view, @@ -592,12 +645,12 @@ fn receive_update_comment( Ok(HttpResponse::Ok().finish()) } -fn receive_like_comment( - like: &Like, - - conn: &PgConnection, +async fn receive_like_comment( + like: Like, + client: &Client, + pool: &DbPool, chat_server: ChatServerParam, -) -> Result { +) -> Result { let note = like .like_props .get_object_base_box() @@ -608,23 +661,31 @@ fn receive_like_comment( let user_uri = like.like_props.get_actor_xsd_any_uri().unwrap().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?; - insert_activity(&conn, user.id, &like, false)?; + insert_activity(user.id, like, false, pool).await?; + + let comment = CommentForm::from_apub(¬e, client, pool).await?; + + let comment_id = get_or_fetch_and_insert_remote_comment(&comment.ap_id, client, pool) + .await? + .id; - let comment = CommentForm::from_apub(¬e, &conn)?; - let comment_id = get_or_fetch_and_insert_remote_comment(&comment.ap_id, &conn)?.id; let like_form = CommentLikeForm { comment_id, post_id: comment.post_id, user_id: user.id, score: 1, }; - CommentLike::remove(&conn, &like_form)?; - CommentLike::like(&conn, &like_form)?; + blocking(pool, move |conn| { + CommentLike::remove(conn, &like_form)?; + CommentLike::like(conn, &like_form) + }) + .await??; // Refetch the view - let comment_view = CommentView::read(&conn, comment_id, None)?; + let comment_view = + blocking(pool, move |conn| CommentView::read(conn, comment_id, None)).await??; // TODO get those recipient actor ids from somewhere let recipient_ids = vec![]; @@ -642,12 +703,12 @@ fn receive_like_comment( Ok(HttpResponse::Ok().finish()) } -fn receive_dislike_comment( - dislike: &Dislike, - - conn: &PgConnection, +async fn receive_dislike_comment( + dislike: Dislike, + client: &Client, + pool: &DbPool, chat_server: ChatServerParam, -) -> Result { +) -> Result { let note = dislike .dislike_props .get_object_base_box() @@ -662,23 +723,31 @@ fn receive_dislike_comment( .unwrap() .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?; - insert_activity(&conn, user.id, &dislike, false)?; + insert_activity(user.id, dislike, false, pool).await?; + + let comment = CommentForm::from_apub(¬e, client, pool).await?; + + let comment_id = get_or_fetch_and_insert_remote_comment(&comment.ap_id, client, pool) + .await? + .id; - let comment = CommentForm::from_apub(¬e, &conn)?; - let comment_id = get_or_fetch_and_insert_remote_comment(&comment.ap_id, &conn)?.id; let like_form = CommentLikeForm { comment_id, post_id: comment.post_id, user_id: user.id, score: -1, }; - CommentLike::remove(&conn, &like_form)?; - CommentLike::like(&conn, &like_form)?; + blocking(pool, move |conn| { + CommentLike::remove(conn, &like_form)?; + CommentLike::like(conn, &like_form) + }) + .await??; // Refetch the view - let comment_view = CommentView::read(&conn, comment_id, None)?; + let comment_view = + blocking(pool, move |conn| CommentView::read(conn, comment_id, None)).await??; // TODO get those recipient actor ids from somewhere let recipient_ids = vec![]; @@ -696,12 +765,12 @@ fn receive_dislike_comment( Ok(HttpResponse::Ok().finish()) } -fn receive_delete_community( - delete: &Delete, - - conn: &PgConnection, +async fn receive_delete_community( + delete: Delete, + client: &Client, + pool: &DbPool, chat_server: ChatServerParam, -) -> Result { +) -> Result { let user_uri = delete .delete_props .get_actor_xsd_any_uri() @@ -716,12 +785,18 @@ fn receive_delete_community( .to_owned() .into_concrete::()?; - 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?; - insert_activity(&conn, user.id, &delete, false)?; + insert_activity(user.id, delete, false, pool).await?; - let community_actor_id = CommunityForm::from_apub(&group, &conn)?.actor_id; - let community = Community::read_from_actor_id(conn, &community_actor_id)?; + let community_actor_id = CommunityForm::from_apub(&group, client, pool) + .await? + .actor_id; + + let community = blocking(pool, move |conn| { + Community::read_from_actor_id(conn, &community_actor_id) + }) + .await??; let community_form = CommunityForm { name: community.name.to_owned(), @@ -741,28 +816,38 @@ fn receive_delete_community( last_refreshed_at: None, }; - Community::update(&conn, community.id, &community_form)?; + let community_id = community.id; + blocking(pool, move |conn| { + Community::update(conn, community_id, &community_form) + }) + .await??; + let community_id = community.id; let res = CommunityResponse { - community: CommunityView::read(&conn, community.id, None)?, + community: blocking(pool, move |conn| { + CommunityView::read(conn, community_id, None) + }) + .await??, }; + let community_id = res.community.id; + chat_server.do_send(SendCommunityRoomMessage { op: UserOperation::EditCommunity, response: res, - community_id: community.id, + community_id, my_id: None, }); Ok(HttpResponse::Ok().finish()) } -fn receive_remove_community( - remove: &Remove, - - conn: &PgConnection, +async fn receive_remove_community( + remove: Remove, + client: &Client, + pool: &DbPool, chat_server: ChatServerParam, -) -> Result { +) -> Result { let mod_uri = remove .remove_props .get_actor_xsd_any_uri() @@ -777,12 +862,18 @@ fn receive_remove_community( .to_owned() .into_concrete::()?; - let mod_ = get_or_fetch_and_upsert_remote_user(&mod_uri, &conn)?; + let mod_ = get_or_fetch_and_upsert_remote_user(&mod_uri, client, pool).await?; - insert_activity(&conn, mod_.id, &remove, false)?; + insert_activity(mod_.id, remove, false, pool).await?; - let community_actor_id = CommunityForm::from_apub(&group, &conn)?.actor_id; - let community = Community::read_from_actor_id(conn, &community_actor_id)?; + let community_actor_id = CommunityForm::from_apub(&group, client, pool) + .await? + .actor_id; + + let community = blocking(pool, move |conn| { + Community::read_from_actor_id(conn, &community_actor_id) + }) + .await??; let community_form = CommunityForm { name: community.name.to_owned(), @@ -802,28 +893,38 @@ fn receive_remove_community( last_refreshed_at: None, }; - Community::update(&conn, community.id, &community_form)?; + let community_id = community.id; + blocking(pool, move |conn| { + Community::update(conn, community_id, &community_form) + }) + .await??; + let community_id = community.id; let res = CommunityResponse { - community: CommunityView::read(&conn, community.id, None)?, + community: blocking(pool, move |conn| { + CommunityView::read(conn, community_id, None) + }) + .await??, }; + let community_id = res.community.id; + chat_server.do_send(SendCommunityRoomMessage { op: UserOperation::EditCommunity, response: res, - community_id: community.id, + community_id, my_id: None, }); Ok(HttpResponse::Ok().finish()) } -fn receive_delete_post( - delete: &Delete, - - conn: &PgConnection, +async fn receive_delete_post( + delete: Delete, + client: &Client, + pool: &DbPool, chat_server: ChatServerParam, -) -> Result { +) -> Result { let user_uri = delete .delete_props .get_actor_xsd_any_uri() @@ -838,12 +939,13 @@ fn receive_delete_post( .to_owned() .into_concrete::()?; - 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?; - insert_activity(&conn, user.id, &delete, false)?; + insert_activity(user.id, delete, false, pool).await?; - let post_ap_id = PostForm::from_apub(&page, conn)?.ap_id; - let post = get_or_fetch_and_insert_remote_post(&post_ap_id, &conn)?; + let post_ap_id = PostForm::from_apub(&page, client, pool).await?.ap_id; + + let post = get_or_fetch_and_insert_remote_post(&post_ap_id, client, pool).await?; let post_form = PostForm { name: post.name.to_owned(), @@ -865,10 +967,12 @@ fn receive_delete_post( local: post.local, published: None, }; - Post::update(&conn, post.id, &post_form)?; + let post_id = post.id; + blocking(pool, move |conn| Post::update(conn, post_id, &post_form)).await??; // Refetch the view - let post_view = PostView::read(&conn, post.id, None)?; + let post_id = post.id; + let post_view = blocking(pool, move |conn| PostView::read(conn, post_id, None)).await??; let res = PostResponse { post: post_view }; @@ -881,12 +985,12 @@ fn receive_delete_post( Ok(HttpResponse::Ok().finish()) } -fn receive_remove_post( - remove: &Remove, - - conn: &PgConnection, +async fn receive_remove_post( + remove: Remove, + client: &Client, + pool: &DbPool, chat_server: ChatServerParam, -) -> Result { +) -> Result { let mod_uri = remove .remove_props .get_actor_xsd_any_uri() @@ -901,12 +1005,13 @@ fn receive_remove_post( .to_owned() .into_concrete::()?; - let mod_ = get_or_fetch_and_upsert_remote_user(&mod_uri, &conn)?; + let mod_ = get_or_fetch_and_upsert_remote_user(&mod_uri, client, pool).await?; - insert_activity(&conn, mod_.id, &remove, false)?; + insert_activity(mod_.id, remove, false, pool).await?; - let post_ap_id = PostForm::from_apub(&page, conn)?.ap_id; - let post = get_or_fetch_and_insert_remote_post(&post_ap_id, &conn)?; + let post_ap_id = PostForm::from_apub(&page, client, pool).await?.ap_id; + + let post = get_or_fetch_and_insert_remote_post(&post_ap_id, client, pool).await?; let post_form = PostForm { name: post.name.to_owned(), @@ -928,10 +1033,12 @@ fn receive_remove_post( local: post.local, published: None, }; - Post::update(&conn, post.id, &post_form)?; + let post_id = post.id; + blocking(pool, move |conn| Post::update(conn, post_id, &post_form)).await??; // Refetch the view - let post_view = PostView::read(&conn, post.id, None)?; + let post_id = post.id; + let post_view = blocking(pool, move |conn| PostView::read(conn, post_id, None)).await??; let res = PostResponse { post: post_view }; @@ -944,12 +1051,12 @@ fn receive_remove_post( Ok(HttpResponse::Ok().finish()) } -fn receive_delete_comment( - delete: &Delete, - - conn: &PgConnection, +async fn receive_delete_comment( + delete: Delete, + client: &Client, + pool: &DbPool, chat_server: ChatServerParam, -) -> Result { +) -> Result { let user_uri = delete .delete_props .get_actor_xsd_any_uri() @@ -964,12 +1071,14 @@ fn receive_delete_comment( .to_owned() .into_concrete::()?; - 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?; - insert_activity(&conn, user.id, &delete, false)?; + insert_activity(user.id, delete, false, pool).await?; + + let comment_ap_id = CommentForm::from_apub(¬e, client, pool).await?.ap_id; + + let comment = get_or_fetch_and_insert_remote_comment(&comment_ap_id, client, pool).await?; - let comment_ap_id = CommentForm::from_apub(¬e, &conn)?.ap_id; - let comment = get_or_fetch_and_insert_remote_comment(&comment_ap_id, &conn)?; let comment_form = CommentForm { content: comment.content.to_owned(), parent_id: comment.parent_id, @@ -983,10 +1092,16 @@ fn receive_delete_comment( ap_id: comment.ap_id, local: comment.local, }; - Comment::update(&conn, comment.id, &comment_form)?; + let comment_id = comment.id; + blocking(pool, move |conn| { + Comment::update(conn, comment_id, &comment_form) + }) + .await??; // Refetch the view - let comment_view = CommentView::read(&conn, comment.id, None)?; + let comment_id = comment.id; + let comment_view = + blocking(pool, move |conn| CommentView::read(conn, comment_id, None)).await??; // TODO get those recipient actor ids from somewhere let recipient_ids = vec![]; @@ -1004,12 +1119,12 @@ fn receive_delete_comment( Ok(HttpResponse::Ok().finish()) } -fn receive_remove_comment( - remove: &Remove, - - conn: &PgConnection, +async fn receive_remove_comment( + remove: Remove, + client: &Client, + pool: &DbPool, chat_server: ChatServerParam, -) -> Result { +) -> Result { let mod_uri = remove .remove_props .get_actor_xsd_any_uri() @@ -1024,12 +1139,14 @@ fn receive_remove_comment( .to_owned() .into_concrete::()?; - let mod_ = get_or_fetch_and_upsert_remote_user(&mod_uri, &conn)?; + let mod_ = get_or_fetch_and_upsert_remote_user(&mod_uri, client, pool).await?; - insert_activity(&conn, mod_.id, &remove, false)?; + insert_activity(mod_.id, remove, false, pool).await?; + + let comment_ap_id = CommentForm::from_apub(¬e, client, pool).await?.ap_id; + + let comment = get_or_fetch_and_insert_remote_comment(&comment_ap_id, client, pool).await?; - let comment_ap_id = CommentForm::from_apub(¬e, &conn)?.ap_id; - let comment = get_or_fetch_and_insert_remote_comment(&comment_ap_id, &conn)?; let comment_form = CommentForm { content: comment.content.to_owned(), parent_id: comment.parent_id, @@ -1043,10 +1160,16 @@ fn receive_remove_comment( ap_id: comment.ap_id, local: comment.local, }; - Comment::update(&conn, comment.id, &comment_form)?; + let comment_id = comment.id; + blocking(pool, move |conn| { + Comment::update(conn, comment_id, &comment_form) + }) + .await??; // Refetch the view - let comment_view = CommentView::read(&conn, comment.id, None)?; + let comment_id = comment.id; + let comment_view = + blocking(pool, move |conn| CommentView::read(conn, comment_id, None)).await??; // TODO get those recipient actor ids from somewhere let recipient_ids = vec![]; @@ -1064,12 +1187,12 @@ fn receive_remove_comment( Ok(HttpResponse::Ok().finish()) } -fn receive_undo_delete( - undo: &Undo, - - conn: &PgConnection, +async fn receive_undo_delete( + undo: Undo, + client: &Client, + pool: &DbPool, chat_server: ChatServerParam, -) -> Result { +) -> Result { let delete = undo .undo_props .get_object_base_box() @@ -1087,19 +1210,19 @@ fn receive_undo_delete( .unwrap(); match type_ { - "Note" => receive_undo_delete_comment(&delete, &conn, chat_server), - "Page" => receive_undo_delete_post(&delete, &conn, chat_server), - "Group" => receive_undo_delete_community(&delete, &conn, chat_server), - d => Err(format_err!("Undo Delete type {} not supported", d)), + "Note" => receive_undo_delete_comment(delete, client, pool, chat_server).await, + "Page" => receive_undo_delete_post(delete, client, pool, chat_server).await, + "Group" => receive_undo_delete_community(delete, client, pool, chat_server).await, + d => Err(format_err!("Undo Delete type {} not supported", d).into()), } } -fn receive_undo_remove( - undo: &Undo, - - conn: &PgConnection, +async fn receive_undo_remove( + undo: Undo, + client: &Client, + pool: &DbPool, chat_server: ChatServerParam, -) -> Result { +) -> Result { let remove = undo .undo_props .get_object_base_box() @@ -1117,19 +1240,19 @@ fn receive_undo_remove( .unwrap(); match type_ { - "Note" => receive_undo_remove_comment(&remove, &conn, chat_server), - "Page" => receive_undo_remove_post(&remove, &conn, chat_server), - "Group" => receive_undo_remove_community(&remove, &conn, chat_server), - d => Err(format_err!("Undo Delete type {} not supported", d)), + "Note" => receive_undo_remove_comment(remove, client, pool, chat_server).await, + "Page" => receive_undo_remove_post(remove, client, pool, chat_server).await, + "Group" => receive_undo_remove_community(remove, client, pool, chat_server).await, + d => Err(format_err!("Undo Delete type {} not supported", d).into()), } } -fn receive_undo_delete_comment( - delete: &Delete, - - conn: &PgConnection, +async fn receive_undo_delete_comment( + delete: Delete, + client: &Client, + pool: &DbPool, chat_server: ChatServerParam, -) -> Result { +) -> Result { let user_uri = delete .delete_props .get_actor_xsd_any_uri() @@ -1144,12 +1267,14 @@ fn receive_undo_delete_comment( .to_owned() .into_concrete::()?; - 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?; - insert_activity(&conn, user.id, &delete, false)?; + insert_activity(user.id, delete, false, pool).await?; + + let comment_ap_id = CommentForm::from_apub(¬e, client, pool).await?.ap_id; + + let comment = get_or_fetch_and_insert_remote_comment(&comment_ap_id, client, pool).await?; - let comment_ap_id = CommentForm::from_apub(¬e, &conn)?.ap_id; - let comment = get_or_fetch_and_insert_remote_comment(&comment_ap_id, &conn)?; let comment_form = CommentForm { content: comment.content.to_owned(), parent_id: comment.parent_id, @@ -1163,10 +1288,16 @@ fn receive_undo_delete_comment( ap_id: comment.ap_id, local: comment.local, }; - Comment::update(&conn, comment.id, &comment_form)?; + let comment_id = comment.id; + blocking(pool, move |conn| { + Comment::update(conn, comment_id, &comment_form) + }) + .await??; // Refetch the view - let comment_view = CommentView::read(&conn, comment.id, None)?; + let comment_id = comment.id; + let comment_view = + blocking(pool, move |conn| CommentView::read(conn, comment_id, None)).await??; // TODO get those recipient actor ids from somewhere let recipient_ids = vec![]; @@ -1184,12 +1315,12 @@ fn receive_undo_delete_comment( Ok(HttpResponse::Ok().finish()) } -fn receive_undo_remove_comment( - remove: &Remove, - - conn: &PgConnection, +async fn receive_undo_remove_comment( + remove: Remove, + client: &Client, + pool: &DbPool, chat_server: ChatServerParam, -) -> Result { +) -> Result { let mod_uri = remove .remove_props .get_actor_xsd_any_uri() @@ -1204,12 +1335,14 @@ fn receive_undo_remove_comment( .to_owned() .into_concrete::()?; - let mod_ = get_or_fetch_and_upsert_remote_user(&mod_uri, &conn)?; + let mod_ = get_or_fetch_and_upsert_remote_user(&mod_uri, client, pool).await?; - insert_activity(&conn, mod_.id, &remove, false)?; + insert_activity(mod_.id, remove, false, pool).await?; + + let comment_ap_id = CommentForm::from_apub(¬e, client, pool).await?.ap_id; + + let comment = get_or_fetch_and_insert_remote_comment(&comment_ap_id, client, pool).await?; - let comment_ap_id = CommentForm::from_apub(¬e, &conn)?.ap_id; - let comment = get_or_fetch_and_insert_remote_comment(&comment_ap_id, &conn)?; let comment_form = CommentForm { content: comment.content.to_owned(), parent_id: comment.parent_id, @@ -1223,10 +1356,16 @@ fn receive_undo_remove_comment( ap_id: comment.ap_id, local: comment.local, }; - Comment::update(&conn, comment.id, &comment_form)?; + let comment_id = comment.id; + blocking(pool, move |conn| { + Comment::update(conn, comment_id, &comment_form) + }) + .await??; // Refetch the view - let comment_view = CommentView::read(&conn, comment.id, None)?; + let comment_id = comment.id; + let comment_view = + blocking(pool, move |conn| CommentView::read(conn, comment_id, None)).await??; // TODO get those recipient actor ids from somewhere let recipient_ids = vec![]; @@ -1244,12 +1383,12 @@ fn receive_undo_remove_comment( Ok(HttpResponse::Ok().finish()) } -fn receive_undo_delete_post( - delete: &Delete, - - conn: &PgConnection, +async fn receive_undo_delete_post( + delete: Delete, + client: &Client, + pool: &DbPool, chat_server: ChatServerParam, -) -> Result { +) -> Result { let user_uri = delete .delete_props .get_actor_xsd_any_uri() @@ -1264,12 +1403,13 @@ fn receive_undo_delete_post( .to_owned() .into_concrete::()?; - 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?; - insert_activity(&conn, user.id, &delete, false)?; + insert_activity(user.id, delete, false, pool).await?; - let post_ap_id = PostForm::from_apub(&page, conn)?.ap_id; - let post = get_or_fetch_and_insert_remote_post(&post_ap_id, &conn)?; + let post_ap_id = PostForm::from_apub(&page, client, pool).await?.ap_id; + + let post = get_or_fetch_and_insert_remote_post(&post_ap_id, client, pool).await?; let post_form = PostForm { name: post.name.to_owned(), @@ -1291,10 +1431,12 @@ fn receive_undo_delete_post( local: post.local, published: None, }; - Post::update(&conn, post.id, &post_form)?; + let post_id = post.id; + blocking(pool, move |conn| Post::update(conn, post_id, &post_form)).await??; // Refetch the view - let post_view = PostView::read(&conn, post.id, None)?; + let post_id = post.id; + let post_view = blocking(pool, move |conn| PostView::read(conn, post_id, None)).await??; let res = PostResponse { post: post_view }; @@ -1307,12 +1449,12 @@ fn receive_undo_delete_post( Ok(HttpResponse::Ok().finish()) } -fn receive_undo_remove_post( - remove: &Remove, - - conn: &PgConnection, +async fn receive_undo_remove_post( + remove: Remove, + client: &Client, + pool: &DbPool, chat_server: ChatServerParam, -) -> Result { +) -> Result { let mod_uri = remove .remove_props .get_actor_xsd_any_uri() @@ -1327,12 +1469,13 @@ fn receive_undo_remove_post( .to_owned() .into_concrete::()?; - let mod_ = get_or_fetch_and_upsert_remote_user(&mod_uri, &conn)?; + let mod_ = get_or_fetch_and_upsert_remote_user(&mod_uri, client, pool).await?; - insert_activity(&conn, mod_.id, &remove, false)?; + insert_activity(mod_.id, remove, false, pool).await?; - let post_ap_id = PostForm::from_apub(&page, conn)?.ap_id; - let post = get_or_fetch_and_insert_remote_post(&post_ap_id, &conn)?; + let post_ap_id = PostForm::from_apub(&page, client, pool).await?.ap_id; + + let post = get_or_fetch_and_insert_remote_post(&post_ap_id, client, pool).await?; let post_form = PostForm { name: post.name.to_owned(), @@ -1354,10 +1497,12 @@ fn receive_undo_remove_post( local: post.local, published: None, }; - Post::update(&conn, post.id, &post_form)?; + let post_id = post.id; + blocking(pool, move |conn| Post::update(conn, post_id, &post_form)).await??; // Refetch the view - let post_view = PostView::read(&conn, post.id, None)?; + let post_id = post.id; + let post_view = blocking(pool, move |conn| PostView::read(conn, post_id, None)).await??; let res = PostResponse { post: post_view }; @@ -1370,12 +1515,12 @@ fn receive_undo_remove_post( Ok(HttpResponse::Ok().finish()) } -fn receive_undo_delete_community( - delete: &Delete, - - conn: &PgConnection, +async fn receive_undo_delete_community( + delete: Delete, + client: &Client, + pool: &DbPool, chat_server: ChatServerParam, -) -> Result { +) -> Result { let user_uri = delete .delete_props .get_actor_xsd_any_uri() @@ -1390,12 +1535,18 @@ fn receive_undo_delete_community( .to_owned() .into_concrete::()?; - 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?; - insert_activity(&conn, user.id, &delete, false)?; + insert_activity(user.id, delete, false, pool).await?; - let community_actor_id = CommunityForm::from_apub(&group, &conn)?.actor_id; - let community = Community::read_from_actor_id(conn, &community_actor_id)?; + let community_actor_id = CommunityForm::from_apub(&group, client, pool) + .await? + .actor_id; + + let community = blocking(pool, move |conn| { + Community::read_from_actor_id(conn, &community_actor_id) + }) + .await??; let community_form = CommunityForm { name: community.name.to_owned(), @@ -1415,28 +1566,38 @@ fn receive_undo_delete_community( last_refreshed_at: None, }; - Community::update(&conn, community.id, &community_form)?; + let community_id = community.id; + blocking(pool, move |conn| { + Community::update(conn, community_id, &community_form) + }) + .await??; + let community_id = community.id; let res = CommunityResponse { - community: CommunityView::read(&conn, community.id, None)?, + community: blocking(pool, move |conn| { + CommunityView::read(conn, community_id, None) + }) + .await??, }; + let community_id = res.community.id; + chat_server.do_send(SendCommunityRoomMessage { op: UserOperation::EditCommunity, response: res, - community_id: community.id, + community_id, my_id: None, }); Ok(HttpResponse::Ok().finish()) } -fn receive_undo_remove_community( - remove: &Remove, - - conn: &PgConnection, +async fn receive_undo_remove_community( + remove: Remove, + client: &Client, + pool: &DbPool, chat_server: ChatServerParam, -) -> Result { +) -> Result { let mod_uri = remove .remove_props .get_actor_xsd_any_uri() @@ -1451,12 +1612,18 @@ fn receive_undo_remove_community( .to_owned() .into_concrete::()?; - let mod_ = get_or_fetch_and_upsert_remote_user(&mod_uri, &conn)?; + let mod_ = get_or_fetch_and_upsert_remote_user(&mod_uri, client, pool).await?; - insert_activity(&conn, mod_.id, &remove, false)?; + insert_activity(mod_.id, remove, false, pool).await?; - let community_actor_id = CommunityForm::from_apub(&group, &conn)?.actor_id; - let community = Community::read_from_actor_id(conn, &community_actor_id)?; + let community_actor_id = CommunityForm::from_apub(&group, client, pool) + .await? + .actor_id; + + let community = blocking(pool, move |conn| { + Community::read_from_actor_id(conn, &community_actor_id) + }) + .await??; let community_form = CommunityForm { name: community.name.to_owned(), @@ -1476,28 +1643,38 @@ fn receive_undo_remove_community( last_refreshed_at: None, }; - Community::update(&conn, community.id, &community_form)?; + let community_id = community.id; + blocking(pool, move |conn| { + Community::update(conn, community_id, &community_form) + }) + .await??; + let community_id = community.id; let res = CommunityResponse { - community: CommunityView::read(&conn, community.id, None)?, + community: blocking(pool, move |conn| { + CommunityView::read(conn, community_id, None) + }) + .await??, }; + let community_id = res.community.id; + chat_server.do_send(SendCommunityRoomMessage { op: UserOperation::EditCommunity, response: res, - community_id: community.id, + community_id, my_id: None, }); Ok(HttpResponse::Ok().finish()) } -fn receive_undo_like( - undo: &Undo, - - conn: &PgConnection, +async fn receive_undo_like( + undo: Undo, + client: &Client, + pool: &DbPool, chat_server: ChatServerParam, -) -> Result { +) -> Result { let like = undo .undo_props .get_object_base_box() @@ -1515,18 +1692,18 @@ fn receive_undo_like( .unwrap(); match type_ { - "Note" => receive_undo_like_comment(&like, &conn, chat_server), - "Page" => receive_undo_like_post(&like, &conn, chat_server), - d => Err(format_err!("Undo Delete type {} not supported", d)), + "Note" => receive_undo_like_comment(like, client, pool, chat_server).await, + "Page" => receive_undo_like_post(like, client, pool, chat_server).await, + d => Err(format_err!("Undo Delete type {} not supported", d).into()), } } -fn receive_undo_like_comment( - like: &Like, - - conn: &PgConnection, +async fn receive_undo_like_comment( + like: Like, + client: &Client, + pool: &DbPool, chat_server: ChatServerParam, -) -> Result { +) -> Result { let note = like .like_props .get_object_base_box() @@ -1537,22 +1714,27 @@ fn receive_undo_like_comment( let user_uri = like.like_props.get_actor_xsd_any_uri().unwrap().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?; - insert_activity(&conn, user.id, &like, false)?; + insert_activity(user.id, like, false, pool).await?; + + let comment = CommentForm::from_apub(¬e, client, pool).await?; + + let comment_id = get_or_fetch_and_insert_remote_comment(&comment.ap_id, client, pool) + .await? + .id; - let comment = CommentForm::from_apub(¬e, &conn)?; - let comment_id = get_or_fetch_and_insert_remote_comment(&comment.ap_id, &conn)?.id; let like_form = CommentLikeForm { comment_id, post_id: comment.post_id, user_id: user.id, score: 0, }; - CommentLike::remove(&conn, &like_form)?; + blocking(pool, move |conn| CommentLike::remove(conn, &like_form)).await??; // Refetch the view - let comment_view = CommentView::read(&conn, comment_id, None)?; + let comment_view = + blocking(pool, move |conn| CommentView::read(conn, comment_id, None)).await??; // TODO get those recipient actor ids from somewhere let recipient_ids = vec![]; @@ -1570,12 +1752,12 @@ fn receive_undo_like_comment( Ok(HttpResponse::Ok().finish()) } -fn receive_undo_like_post( - like: &Like, - - conn: &PgConnection, +async fn receive_undo_like_post( + like: Like, + client: &Client, + pool: &DbPool, chat_server: ChatServerParam, -) -> Result { +) -> Result { let page = like .like_props .get_object_base_box() @@ -1586,22 +1768,25 @@ fn receive_undo_like_post( let user_uri = like.like_props.get_actor_xsd_any_uri().unwrap().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?; - insert_activity(&conn, user.id, &like, false)?; + insert_activity(user.id, like, false, pool).await?; - let post = PostForm::from_apub(&page, conn)?; - let post_id = get_or_fetch_and_insert_remote_post(&post.ap_id, &conn)?.id; + let post = PostForm::from_apub(&page, client, pool).await?; + + let post_id = get_or_fetch_and_insert_remote_post(&post.ap_id, client, pool) + .await? + .id; let like_form = PostLikeForm { post_id, user_id: user.id, score: 1, }; - PostLike::remove(&conn, &like_form)?; + blocking(pool, move |conn| PostLike::remove(conn, &like_form)).await??; // Refetch the view - let post_view = PostView::read(&conn, post_id, None)?; + let post_view = blocking(pool, move |conn| PostView::read(conn, post_id, None)).await??; let res = PostResponse { post: post_view }; diff --git a/server/src/apub/user.rs b/server/src/apub/user.rs index c840cc22d..51339ccf2 100644 --- a/server/src/apub/user.rs +++ b/server/src/apub/user.rs @@ -8,6 +8,7 @@ use crate::{ PersonExt, ToApub, }, + blocking, convert_datetime, db::{ activity::insert_activity, @@ -15,6 +16,8 @@ use crate::{ }, naive_now, routes::DbPoolParam, + DbPool, + LemmyError, }; use activitystreams::{ actor::{properties::ApActorProperties, Person}, @@ -29,9 +32,7 @@ use activitystreams_new::{ object::Tombstone, prelude::*, }; -use actix_web::{body::Body, web::Path, HttpResponse, Result}; -use diesel::PgConnection; -use failure::Error; +use actix_web::{body::Body, client::Client, web, HttpResponse}; use serde::Deserialize; #[derive(Deserialize)] @@ -39,11 +40,12 @@ pub struct UserQuery { user_name: String, } +#[async_trait::async_trait(?Send)] impl ToApub for User_ { type Response = PersonExt; // Turn a Lemmy Community into an ActivityPub group that can be sent out over the network. - fn to_apub(&self, _conn: &PgConnection) -> Result { + async fn to_apub(&self, _pool: &DbPool) -> Result { // TODO go through all these to_string and to_owned() let mut person = Person::default(); 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())) } - fn to_tombstone(&self) -> Result { + fn to_tombstone(&self) -> Result { unimplemented!() } } +#[async_trait::async_trait(?Send)] impl ActorType for User_ { fn actor_id(&self) -> String { 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. - 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 mut follow = Follow::new(self.actor_id.to_owned(), follow_actor_id); follow.set_context(context()).set_id(id.parse()?); let to = format!("{}/inbox", follow_actor_id); - insert_activity(&conn, self.id, &follow, true)?; + insert_activity(self.id, follow.clone(), true, pool).await?; - send_activity(&follow, self, vec![to])?; + send_activity(client, &follow, self, vec![to]).await?; 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 mut follow = Follow::new(self.actor_id.to_owned(), follow_actor_id); 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::()?, follow.into_any_base()?); 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(()) } - fn send_delete(&self, _creator: &User_, _conn: &PgConnection) -> Result<(), Error> { + async fn send_delete( + &self, + _creator: &User_, + _client: &Client, + _pool: &DbPool, + ) -> Result<(), LemmyError> { 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!() } - fn send_remove(&self, _creator: &User_, _conn: &PgConnection) -> Result<(), Error> { + async fn send_remove( + &self, + _creator: &User_, + _client: &Client, + _pool: &DbPool, + ) -> Result<(), LemmyError> { 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!() } - 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!() } - fn get_follower_inboxes(&self, _conn: &PgConnection) -> Result, Error> { + async fn get_follower_inboxes(&self, _pool: &DbPool) -> Result, LemmyError> { unimplemented!() } } +#[async_trait::async_trait(?Send)] impl FromApub for UserForm { type ApubType = PersonExt; /// Parse an ActivityPub person received from another instance into a Lemmy user. - fn from_apub(person: &PersonExt, _conn: &PgConnection) -> Result { + async fn from_apub(person: &PersonExt, _: &Client, _: &DbPool) -> Result { let oprops = &person.inner.object_props; let aprops = &person.ext_one; let public_key: &PublicKey = &person.ext_two.public_key; @@ -210,10 +249,14 @@ impl FromApub for UserForm { /// Return the user json over HTTP. pub async fn get_apub_user_http( - info: Path, + info: web::Path, db: DbPoolParam, -) -> Result, Error> { - let user = User_::find_by_email_or_username(&&db.get()?, &info.user_name)?; - let u = user.to_apub(&db.get().unwrap())?; +) -> Result, LemmyError> { + let user_name = info.into_inner().user_name; + 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)) } diff --git a/server/src/apub/user_inbox.rs b/server/src/apub/user_inbox.rs index f60a2ba9b..c63178e72 100644 --- a/server/src/apub/user_inbox.rs +++ b/server/src/apub/user_inbox.rs @@ -5,6 +5,7 @@ use crate::{ fetcher::{get_or_fetch_and_upsert_remote_community, get_or_fetch_and_upsert_remote_user}, FromApub, }, + blocking, db::{ activity::insert_activity, community::{CommunityFollower, CommunityFollowerForm}, @@ -17,16 +18,17 @@ use crate::{ naive_now, routes::{ChatServerParam, DbPoolParam}, websocket::{server::SendUserRoomMessage, UserOperation}, + DbPool, + LemmyError, }; use activitystreams::{ activity::{Accept, Create, Delete, Undo, Update}, object::Note, }; -use actix_web::{web, HttpRequest, HttpResponse, Result}; -use diesel::PgConnection; -use failure::{Error, _core::fmt::Debug}; +use actix_web::{client::Client, web, HttpRequest, HttpResponse}; use log::debug; use serde::Deserialize; +use std::fmt::Debug; #[serde(untagged)] #[derive(Deserialize, Debug)] @@ -43,51 +45,53 @@ pub async fn user_inbox( request: HttpRequest, input: web::Json, path: web::Path, + client: web::Data, db: DbPoolParam, chat_server: ChatServerParam, -) -> Result { +) -> Result { // TODO: would be nice if we could do the signature check here, but we cant access the actor property let input = input.into_inner(); - let conn = &db.get().unwrap(); let username = path.into_inner(); debug!("User {} received activity: {:?}", &username, &input); match input { - UserAcceptedObjects::Accept(a) => receive_accept(&a, &request, &username, &conn), + UserAcceptedObjects::Accept(a) => receive_accept(*a, &request, &username, &client, &db).await, 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) => { - receive_update_private_message(&u, &request, &conn, chat_server) + receive_update_private_message(*u, &request, &client, &db, chat_server).await } 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) => { - 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. -fn receive_accept( - accept: &Accept, +async fn receive_accept( + accept: Accept, request: &HttpRequest, username: &str, - conn: &PgConnection, -) -> Result { + client: &Client, + pool: &DbPool, +) -> Result { let community_uri = accept .accept_props .get_actor_xsd_any_uri() .unwrap() .to_string(); - let community = get_or_fetch_and_upsert_remote_community(&community_uri, conn)?; + let community = get_or_fetch_and_upsert_remote_community(&community_uri, client, pool).await?; 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 let community_follower_form = CommunityFollowerForm { @@ -96,18 +100,22 @@ fn receive_accept( }; // 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 Ok(HttpResponse::Ok().finish()) } -fn receive_create_private_message( - create: &Create, +async fn receive_create_private_message( + create: Create, request: &HttpRequest, - conn: &PgConnection, + client: &Client, + pool: &DbPool, chat_server: ChatServerParam, -) -> Result { +) -> Result { let note = create .create_props .get_object_base_box() @@ -122,36 +130,44 @@ fn receive_create_private_message( .unwrap() .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)?; - insert_activity(&conn, user.id, &create, false)?; + insert_activity(user.id, create, false, pool).await?; - let private_message = PrivateMessageForm::from_apub(¬e, &conn)?; - let inserted_private_message = PrivateMessage::create(&conn, &private_message)?; + let private_message = PrivateMessageForm::from_apub(¬e, client, pool).await?; - 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 { - message: message.to_owned(), - }; + let message = blocking(pool, move |conn| { + PrivateMessageView::read(conn, inserted_private_message.id) + }) + .await??; + + let res = PrivateMessageResponse { message }; + + let recipient_id = res.message.recipient_id; chat_server.do_send(SendUserRoomMessage { op: UserOperation::CreatePrivateMessage, response: res, - recipient_id: message.recipient_id, + recipient_id, my_id: None, }); Ok(HttpResponse::Ok().finish()) } -fn receive_update_private_message( - update: &Update, +async fn receive_update_private_message( + update: Update, request: &HttpRequest, - conn: &PgConnection, + client: &Client, + pool: &DbPool, chat_server: ChatServerParam, -) -> Result { +) -> Result { let note = update .update_props .get_object_base_box() @@ -166,37 +182,52 @@ fn receive_update_private_message( .unwrap() .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)?; - insert_activity(&conn, user.id, &update, false)?; + insert_activity(user.id, update, false, pool).await?; - let private_message = PrivateMessageForm::from_apub(¬e, &conn)?; - let private_message_id = PrivateMessage::read_from_apub_id(&conn, &private_message.ap_id)?.id; - PrivateMessage::update(conn, private_message_id, &private_message)?; + let private_message_form = PrivateMessageForm::from_apub(¬e, client, pool).await?; - 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 { - message: message.to_owned(), - }; + let private_message_id = private_message.id; + 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 { op: UserOperation::EditPrivateMessage, response: res, - recipient_id: message.recipient_id, + recipient_id, my_id: None, }); Ok(HttpResponse::Ok().finish()) } -fn receive_delete_private_message( - delete: &Delete, +async fn receive_delete_private_message( + delete: Delete, request: &HttpRequest, - conn: &PgConnection, + client: &Client, + pool: &DbPool, chat_server: ChatServerParam, -) -> Result { +) -> Result { let note = delete .delete_props .get_object_base_box() @@ -211,15 +242,21 @@ fn receive_delete_private_message( .unwrap() .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)?; - insert_activity(&conn, user.id, &delete, false)?; + insert_activity(user.id, delete, false, pool).await?; + + let private_message_form = PrivateMessageForm::from_apub(¬e, 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(¬e, &conn)?; - let private_message_id = PrivateMessage::read_from_apub_id(&conn, &private_message.ap_id)?.id; let private_message_form = PrivateMessageForm { - content: private_message.content, + content: private_message_form.content, recipient_id: private_message.recipient_id, creator_id: private_message.creator_id, deleted: Some(true), @@ -229,30 +266,40 @@ fn receive_delete_private_message( published: None, 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 { - message: message.to_owned(), - }; + 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 { op: UserOperation::EditPrivateMessage, response: res, - recipient_id: message.recipient_id, + recipient_id, my_id: None, }); Ok(HttpResponse::Ok().finish()) } -fn receive_undo_delete_private_message( - undo: &Undo, +async fn receive_undo_delete_private_message( + undo: Undo, request: &HttpRequest, - conn: &PgConnection, + client: &Client, + pool: &DbPool, chat_server: ChatServerParam, -) -> Result { +) -> Result { let delete = undo .undo_props .get_object_base_box() @@ -275,13 +322,19 @@ fn receive_undo_delete_private_message( .unwrap() .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)?; - insert_activity(&conn, user.id, &delete, false)?; + insert_activity(user.id, delete, false, pool).await?; + + let private_message = PrivateMessageForm::from_apub(¬e, 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(¬e, &conn)?; - let private_message_id = PrivateMessage::read_from_apub_id(&conn, &private_message.ap_id)?.id; let private_message_form = PrivateMessageForm { content: private_message.content, recipient_id: private_message.recipient_id, @@ -293,18 +346,25 @@ fn receive_undo_delete_private_message( published: None, 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 { - 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 { op: UserOperation::EditPrivateMessage, response: res, - recipient_id: message.recipient_id, + recipient_id, my_id: None, }); diff --git a/server/src/db/activity.rs b/server/src/db/activity.rs index ccd1e6824..8c2b0c742 100644 --- a/server/src/db/activity.rs +++ b/server/src/db/activity.rs @@ -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 failure::_core::fmt::Debug; use log::debug; use serde::{Deserialize, Serialize}; use serde_json::Value; +use std::fmt::Debug; #[derive(Queryable, Identifiable, PartialEq, Debug, Serialize, Deserialize)] #[table_name = "activity"] @@ -55,12 +55,28 @@ impl Crud for Activity { } } -pub fn insert_activity( +pub async fn insert_activity( + 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( conn: &PgConnection, user_id: i32, data: &T, local: bool, -) -> Result<(), failure::Error> +) -> Result<(), LemmyError> where T: Serialize + Debug, { diff --git a/server/src/db/code_migrations.rs b/server/src/db/code_migrations.rs index 6f1b656f8..67e0c4dcc 100644 --- a/server/src/db/code_migrations.rs +++ b/server/src/db/code_migrations.rs @@ -10,21 +10,22 @@ use crate::{ apub::{extensions::signatures::generate_actor_keypair, make_apub_endpoint, EndpointType}, db::Crud, naive_now, + LemmyError, }; use diesel::*; -use failure::Error; 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)?; community_updates_2020_04_02(&conn)?; post_updates_2020_04_03(&conn)?; comment_updates_2020_04_03(&conn)?; private_message_updates_2020_05_05(&conn)?; + Ok(()) } -fn user_updates_2020_04_02(conn: &PgConnection) -> Result<(), Error> { +fn user_updates_2020_04_02(conn: &PgConnection) -> Result<(), LemmyError> { use crate::schema::user_::dsl::*; info!("Running user_updates_2020_04_02"); @@ -75,7 +76,7 @@ fn user_updates_2020_04_02(conn: &PgConnection) -> Result<(), Error> { 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::*; info!("Running community_updates_2020_04_02"); @@ -119,7 +120,7 @@ fn community_updates_2020_04_02(conn: &PgConnection) -> Result<(), Error> { 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::*; info!("Running post_updates_2020_04_03"); @@ -143,7 +144,7 @@ fn post_updates_2020_04_03(conn: &PgConnection) -> Result<(), Error> { 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::*; info!("Running comment_updates_2020_04_03"); @@ -167,7 +168,7 @@ fn comment_updates_2020_04_03(conn: &PgConnection) -> Result<(), Error> { 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::*; info!("Running private_message_updates_2020_05_05"); diff --git a/server/src/db/comment.rs b/server/src/db/comment.rs index 8a2aa8893..7e76770f6 100644 --- a/server/src/db/comment.rs +++ b/server/src/db/comment.rs @@ -12,7 +12,7 @@ use crate::{ // ) // SELECT * FROM MyTree; -#[derive(Queryable, Associations, Identifiable, PartialEq, Debug, Serialize, Deserialize)] +#[derive(Clone, Queryable, Associations, Identifiable, PartialEq, Debug, Serialize, Deserialize)] #[belongs_to(Post)] #[table_name = "comment"] pub struct Comment { diff --git a/server/src/db/community.rs b/server/src/db/community.rs index 38ad07fcb..461ba473a 100644 --- a/server/src/db/community.rs +++ b/server/src/db/community.rs @@ -5,7 +5,7 @@ use crate::{ use diesel::{dsl::*, result::Error, *}; use serde::{Deserialize, Serialize}; -#[derive(Queryable, Identifiable, PartialEq, Debug, Serialize, Deserialize)] +#[derive(Clone, Queryable, Identifiable, PartialEq, Debug, Serialize, Deserialize)] #[table_name = "community"] pub struct Community { pub id: i32, diff --git a/server/src/db/password_reset_request.rs b/server/src/db/password_reset_request.rs index b92d70ed4..4a071f078 100644 --- a/server/src/db/password_reset_request.rs +++ b/server/src/db/password_reset_request.rs @@ -50,8 +50,8 @@ impl Crud for PasswordResetRequest { impl PasswordResetRequest { pub fn create_token(conn: &PgConnection, from_user_id: i32, token: &str) -> Result { let mut hasher = Sha256::new(); - hasher.input(token); - let token_hash: String = PasswordResetRequest::bytes_to_hex(hasher.result().to_vec()); + hasher.update(token); + let token_hash: String = PasswordResetRequest::bytes_to_hex(hasher.finalize().to_vec()); let form = PasswordResetRequestForm { user_id: from_user_id, @@ -62,8 +62,8 @@ impl PasswordResetRequest { } pub fn read_from_token(conn: &PgConnection, token: &str) -> Result { let mut hasher = Sha256::new(); - hasher.input(token); - let token_hash: String = PasswordResetRequest::bytes_to_hex(hasher.result().to_vec()); + hasher.update(token); + let token_hash: String = PasswordResetRequest::bytes_to_hex(hasher.finalize().to_vec()); password_reset_request .filter(token_encrypted.eq(token_hash)) .filter(published.gt(now - 1.days())) diff --git a/server/src/db/user.rs b/server/src/db/user.rs index eaf9d292b..4ca0a0419 100644 --- a/server/src/db/user.rs +++ b/server/src/db/user.rs @@ -10,7 +10,7 @@ use diesel::{dsl::*, result::Error, *}; use jsonwebtoken::{decode, encode, DecodingKey, EncodingKey, Header, TokenData, Validation}; use serde::{Deserialize, Serialize}; -#[derive(Queryable, Identifiable, PartialEq, Debug)] +#[derive(Clone, Queryable, Identifiable, PartialEq, Debug)] #[table_name = "user_"] pub struct User_ { pub id: i32, diff --git a/server/src/lib.rs b/server/src/lib.rs index d9654af6f..04f376fb4 100644 --- a/server/src/lib.rs +++ b/server/src/lib.rs @@ -26,20 +26,39 @@ pub extern crate serde_json; pub extern crate sha2; pub extern crate strum; +pub async fn blocking(pool: &DbPool, f: F) -> Result +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 apub; pub mod db; pub mod rate_limit; +pub mod request; pub mod routes; pub mod schema; pub mod settings; pub mod version; pub mod websocket; -use crate::settings::Settings; -use actix_web::dev::ConnectionInfo; +use crate::{ + request::{retry, RecvError}, + settings::Settings, +}; +use actix_web::{client::Client, dev::ConnectionInfo}; use chrono::{DateTime, FixedOffset, Local, NaiveDateTime, Utc}; -use isahc::prelude::*; use itertools::Itertools; use lettre::{ smtp::{ @@ -58,12 +77,35 @@ use rand::{distributions::Alphanumeric, thread_rng, Rng}; use regex::{Regex, RegexBuilder}; use serde::Deserialize; +pub type DbPool = diesel::r2d2::Pool>; pub type ConnectionId = usize; pub type PostId = i32; pub type CommunityId = i32; pub type UserId = i32; 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 From for LemmyError +where + T: Into, +{ + fn from(t: T) -> Self { + LemmyError { inner: t.into() } + } +} + pub fn to_datetime_utc(ndt: NaiveDateTime) -> DateTime { DateTime::::from_utc(ndt, Utc) } @@ -85,8 +127,10 @@ pub fn is_email_regex(test: &str) -> bool { EMAIL_REGEX.is_match(test) } -pub fn is_image_content_type(test: &str) -> Result<(), failure::Error> { - if isahc::get(test)? +pub async fn is_image_content_type(client: &Client, test: &str) -> Result<(), LemmyError> { + let response = retry(|| client.get(test).send()).await?; + + if response .headers() .get("Content-Type") .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(()) } 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, } -pub fn fetch_iframely(url: &str) -> Result { +pub async fn fetch_iframely(client: &Client, url: &str) -> Result { 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) } @@ -197,23 +246,30 @@ pub struct PictrsFile { delete_token: String, } -pub fn fetch_pictrs(image_url: &str) -> Result { - is_image_content_type(image_url)?; +pub async fn fetch_pictrs(client: &Client, image_url: &str) -> Result { + is_image_content_type(client, image_url).await?; let fetch_url = format!( "http://pictrs:8080/image/download?url={}", 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)?; - if res.msg == "ok" { - Ok(res) + + let mut response = retry(|| client.get(&fetch_url).send()).await?; + + let response: PictrsResponse = response + .json() + .await + .map_err(|e| RecvError(e.to_string()))?; + + if response.msg == "ok" { + Ok(response) } 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, ) -> ( Option, @@ -225,7 +281,7 @@ fn fetch_iframely_and_pictrs_data( Some(url) => { // Fetch iframely data 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), Err(e) => { error!("iframely err: {}", e); @@ -235,7 +291,7 @@ fn fetch_iframely_and_pictrs_data( // Fetch pictrs thumbnail 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()), 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 - None => match fetch_pictrs(&url) { + None => match fetch_pictrs(client, &url).await { Ok(res) => Some(res.files[0].file.to_owned()), 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 { conn_info - .remote() + .remote_addr() .unwrap_or("127.0.0.1:12345") .split(':') .next() @@ -327,21 +383,25 @@ mod tests { #[test] fn test_mentions_regex() { - let text = "Just read a great blog post by [@tedu@honk.teduangst.com](/u/test). And another by !test_community@fish.teduangst.com . Another [@lemmy@lemmy_alpha:8540](/u/fish)"; + let text = "Just read a great blog post by [@tedu@honk.teduangst.com](/u/test). And another by !test_community@fish.teduangst.com . Another [@lemmy@lemmy-alpha:8540](/u/fish)"; let mentions = scrape_text_for_mentions(text); assert_eq!(mentions[0].name, "tedu".to_string()); assert_eq!(mentions[0].domain, "honk.teduangst.com".to_string()); - assert_eq!(mentions[1].domain, "lemmy_alpha:8540".to_string()); + assert_eq!(mentions[1].domain, "lemmy-alpha:8540".to_string()); } #[test] fn test_image() { - assert!(is_image_content_type("https://1734811051.rsc.cdn77.org/data/images/full/365645/as-virus-kills-navajos-in-their-homes-tribal-women-provide-lifeline.jpg?w=600?w=650").is_ok()); - assert!(is_image_content_type( - "https://twitter.com/BenjaminNorton/status/1259922424272957440?s=20" - ) - .is_err()); + actix_rt::System::new("tset_image").block_on(async move { + 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" + ) + .await.is_err() + ); + }); } #[test] @@ -399,7 +459,7 @@ mod tests { // These helped with testing // #[test] // 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()); // } @@ -420,7 +480,7 @@ mod tests { lazy_static! { static ref EMAIL_REGEX: Regex = Regex::new(r"^[a-zA-Z0-9.!#$%&’*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$").unwrap(); - static ref SLUR_REGEX: Regex = RegexBuilder::new(r"(fag(g|got|tard)?|maricos?|cock\s?sucker(s|ing)?|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(); // TODO keep this old one, it didn't work with port well tho // static ref WEBFINGER_USER_REGEX: Regex = Regex::new(r"@(?P[\w.]+)@(?P[a-zA-Z0-9._-]+\.[a-zA-Z0-9_-]+)").unwrap(); diff --git a/server/src/main.rs b/server/src/main.rs index 2f53f3ac6..30be711fa 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -4,10 +4,13 @@ extern crate diesel_migrations; #[macro_use] pub extern crate lazy_static; +pub type DbPool = Pool>; + use crate::lemmy_server::actix_web::dev::Service; use actix::prelude::*; use actix_web::{ body::Body, + client::Client, dev::{ServiceRequest, ServiceResponse}, http::{ header::{CACHE_CONTROL, CONTENT_TYPE}, @@ -20,14 +23,16 @@ use diesel::{ PgConnection, }; use lemmy_server::{ + blocking, db::code_migrations::run_advanced_migrations, rate_limit::{rate_limiter::RateLimiter, RateLimit}, routes::{api, federation, feeds, index, nodeinfo, webfinger}, settings::Settings, websocket::server::*, + LemmyError, }; use regex::Regex; -use std::{io, sync::Arc}; +use std::sync::Arc; use tokio::sync::Mutex; lazy_static! { @@ -41,7 +46,7 @@ lazy_static! { embed_migrations!(); #[actix_rt::main] -async fn main() -> io::Result<()> { +async fn main() -> Result<(), LemmyError> { env_logger::init(); let settings = Settings::get(); @@ -53,9 +58,12 @@ async fn main() -> io::Result<()> { .unwrap_or_else(|_| panic!("Error connecting to {}", settings.get_database_url())); // Run the migrations from code - let conn = pool.get().unwrap(); - embedded_migrations::run(&conn).unwrap(); - run_advanced_migrations(&conn).unwrap(); + blocking(&pool, move |conn| { + embedded_migrations::run(conn)?; + run_advanced_migrations(conn)?; + Ok(()) as Result<(), LemmyError> + }) + .await??; // Set up the rate limiter let rate_limiter = RateLimit { @@ -63,7 +71,7 @@ async fn main() -> io::Result<()> { }; // 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!( "Starting http server at {}:{}", @@ -79,6 +87,7 @@ async fn main() -> io::Result<()> { .wrap(middleware::Logger::default()) .data(pool.clone()) .data(server.clone()) + .data(Client::default()) // The routes .configure(move |cfg| api::config(cfg, &rate_limiter)) .configure(federation::config) @@ -98,7 +107,9 @@ async fn main() -> io::Result<()> { }) .bind((settings.bind, settings.port))? .run() - .await + .await?; + + Ok(()) } fn add_cache_headers( diff --git a/server/src/rate_limit/mod.rs b/server/src/rate_limit/mod.rs index b4c2dc5db..e49a527e8 100644 --- a/server/src/rate_limit/mod.rs +++ b/server/src/rate_limit/mod.rs @@ -1,5 +1,5 @@ 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 futures::future::{ok, Ready}; use rate_limiter::{RateLimitType, RateLimiter}; @@ -15,6 +15,8 @@ pub mod rate_limiter; #[derive(Debug, Clone)] 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>, } @@ -57,17 +59,11 @@ impl RateLimited { fut: impl Future>, ) -> Result where - E: From, + E: From, { - let rate_limit: RateLimitConfig = actix_web::web::block(move || { - // needs to be in a web::block because the RwLock in settings is from stdlib - Ok(Settings::get().rate_limit) as Result<_, failure::Error> - }) - .await - .map_err(|e| match e { - actix_web::error::BlockingError::Error(e) => e, - _ => APIError::err("Operation canceled").into(), - })?; + // Does not need to be blocking because the RwLock in settings never held across await points, + // and the operation here locks only long enough to clone + let rate_limit: RateLimitConfig = Settings::get().rate_limit; // before { @@ -83,6 +79,7 @@ impl RateLimited { false, )?; + drop(limiter); return fut.await; } RateLimitType::Post => { diff --git a/server/src/rate_limit/rate_limiter.rs b/server/src/rate_limit/rate_limiter.rs index b3ac7093c..20a617c2f 100644 --- a/server/src/rate_limit/rate_limiter.rs +++ b/server/src/rate_limit/rate_limiter.rs @@ -1,6 +1,5 @@ use super::IPAddr; -use crate::api::APIError; -use failure::Error; +use crate::{api::APIError, LemmyError}; use log::debug; use std::{collections::HashMap, time::SystemTime}; use strum::IntoEnumIterator; @@ -11,7 +10,7 @@ pub struct RateLimitBucket { allowance: f64, } -#[derive(Eq, PartialEq, Hash, Debug, EnumIter, Copy, Clone)] +#[derive(Eq, PartialEq, Hash, Debug, EnumIter, Copy, Clone, AsRefStr)] pub enum RateLimitType { Message, Register, @@ -61,7 +60,7 @@ impl RateLimiter { rate: i32, per: i32, check_only: bool, - ) -> Result<(), Error> { + ) -> Result<(), LemmyError> { self.insert_ip(ip); if let Some(bucket) = self.buckets.get_mut(&type_) { if let Some(rate_limit) = bucket.get_mut(ip) { @@ -81,12 +80,21 @@ impl RateLimiter { if rate_limit.allowance < 1.0 { debug!( - "Rate limited IP: {}, time_passed: {}, allowance: {}", - ip, time_passed, rate_limit.allowance + "Rate limited type: {}, IP: {}, time_passed: {}, allowance: {}", + type_.as_ref(), + ip, + time_passed, + rate_limit.allowance ); Err( 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(), ) diff --git a/server/src/request.rs b/server/src/request.rs new file mode 100644 index 000000000..7d09b60df --- /dev/null +++ b/server/src/request.rs @@ -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: F) -> Result +where + F: Fn() -> Fut, + Fut: Future>, +{ + retry_custom(|| async { Ok((f)().await) }).await +} + +pub async fn retry_custom(f: F) -> Result +where + F: Fn() -> Fut, + Fut: Future, 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 +} diff --git a/server/src/routes/api.rs b/server/src/routes/api.rs index 6ee94691d..35e495fa5 100644 --- a/server/src/routes/api.rs +++ b/server/src/routes/api.rs @@ -4,7 +4,7 @@ use crate::{ routes::{ChatServerParam, DbPoolParam}, websocket::WebsocketInfo, }; -use actix_web::{error::ErrorBadRequest, *}; +use actix_web::{client::Client, error::ErrorBadRequest, *}; use serde::Serialize; 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( data: Request, + client: &Client, db: DbPoolParam, chat_server: ChatServerParam, ) -> Result @@ -162,9 +163,10 @@ where id: None, }; - let oper: Oper = Oper::new(data); + let oper: Oper = 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 .map(|json| HttpResponse::Ok().json(json)) .map_err(ErrorBadRequest)?; @@ -173,6 +175,7 @@ where async fn route_get( data: web::Query, + client: web::Data, db: DbPoolParam, chat_server: ChatServerParam, ) -> Result @@ -180,11 +183,12 @@ where Data: Serialize + Send + 'static, Oper: Perform, { - perform::(data.0, db, chat_server).await + perform::(data.0, &client, db, chat_server).await } async fn route_post( data: web::Json, + client: web::Data, db: DbPoolParam, chat_server: ChatServerParam, ) -> Result @@ -192,5 +196,5 @@ where Data: Serialize + Send + 'static, Oper: Perform, { - perform::(data.0, db, chat_server).await + perform::(data.0, &client, db, chat_server).await } diff --git a/server/src/routes/federation.rs b/server/src/routes/federation.rs index fe6e33657..20b5dc834 100644 --- a/server/src/routes/federation.rs +++ b/server/src/routes/federation.rs @@ -12,6 +12,8 @@ use crate::{ settings::Settings, }; use actix_web::*; +use http_signature_normalization_actix::digest::middleware::VerifyDigest; +use sha2::{Digest, Sha256}; pub fn config(cfg: &mut web::ServiceConfig) { 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)), ) // Inboxes dont work with the header guard for some reason. - .route("/c/{community_name}/inbox", web::post().to(community_inbox)) - .route("/u/{user_name}/inbox", web::post().to(user_inbox)) - .route("/inbox", web::post().to(shared_inbox)); + .service( + web::scope("/") + .wrap(VerifyDigest::new(Sha256::new())) + .route("/c/{community_name}/inbox", web::post().to(community_inbox)) + .route("/u/{user_name}/inbox", web::post().to(user_inbox)) + .route("/inbox", web::post().to(shared_inbox)), + ); } } diff --git a/server/src/routes/feeds.rs b/server/src/routes/feeds.rs index b76751a67..a1c2ba58f 100644 --- a/server/src/routes/feeds.rs +++ b/server/src/routes/feeds.rs @@ -1,4 +1,5 @@ use crate::{ + blocking, db::{ comment_view::{ReplyQueryBuilder, ReplyView}, community::Community, @@ -12,6 +13,7 @@ use crate::{ markdown_to_html, routes::DbPoolParam, settings::Settings, + LemmyError, }; use actix_web::{error::ErrorBadRequest, *}; use chrono::{DateTime, NaiveDateTime, Utc}; @@ -43,21 +45,20 @@ pub fn config(cfg: &mut web::ServiceConfig) { } async fn get_all_feed(info: web::Query, db: DbPoolParam) -> Result { - let res = web::block(move || { - let conn = db.get()?; - get_feed_all_data(&conn, &get_sort_type(info)?) - }) - .await - .map(|rss| { + let sort_type = get_sort_type(info).map_err(ErrorBadRequest)?; + + let rss = blocking(&db, move |conn| get_feed_all_data(conn, &sort_type)) + .await? + .map_err(ErrorBadRequest)?; + + Ok( HttpResponse::Ok() .content_type("application/rss+xml") - .body(rss) - }) - .map_err(ErrorBadRequest)?; - Ok(res) + .body(rss), + ) } -fn get_feed_all_data(conn: &PgConnection, sort_type: &SortType) -> Result { +fn get_feed_all_data(conn: &PgConnection, sort_type: &SortType) -> Result { let site_view = SiteView::read(&conn)?; let posts = PostQueryBuilder::create(&conn) @@ -85,37 +86,34 @@ async fn get_feed( info: web::Query, db: web::Data>>, ) -> Result { - let res = web::block(move || { - let conn = db.get()?; + let sort_type = get_sort_type(info).map_err(ErrorBadRequest)?; - let sort_type = get_sort_type(info)?; + let request_type = match path.0.as_ref() { + "u" => RequestType::User, + "c" => RequestType::Community, + "front" => RequestType::Front, + "inbox" => RequestType::Inbox, + _ => return Err(ErrorBadRequest(LemmyError::from(format_err!("wrong_type")))), + }; - let request_type = match path.0.as_ref() { - "u" => RequestType::User, - "c" => RequestType::Community, - "front" => RequestType::Front, - "inbox" => RequestType::Inbox, - _ => return Err(format_err!("wrong_type")), - }; + let param = path.1.to_owned(); - let param = path.1.to_owned(); - - match request_type { - RequestType::User => get_feed_user(&conn, &sort_type, param), - RequestType::Community => get_feed_community(&conn, &sort_type, param), - RequestType::Front => get_feed_front(&conn, &sort_type, param), - RequestType::Inbox => get_feed_inbox(&conn, param), - } + let builder = blocking(&db, move |conn| match request_type { + RequestType::User => get_feed_user(conn, &sort_type, param), + RequestType::Community => get_feed_community(conn, &sort_type, param), + RequestType::Front => get_feed_front(conn, &sort_type, param), + RequestType::Inbox => get_feed_inbox(conn, param), }) - .await - .map(|builder| builder.build().unwrap().to_string()) - .map(|rss| { + .await? + .map_err(ErrorBadRequest)?; + + let rss = builder.build().map_err(ErrorBadRequest)?.to_string(); + + Ok( HttpResponse::Ok() .content_type("application/rss+xml") - .body(rss) - }) - .map_err(ErrorBadRequest)?; - Ok(res) + .body(rss), + ) } fn get_sort_type(info: web::Query) -> Result { @@ -130,7 +128,7 @@ fn get_feed_user( conn: &PgConnection, sort_type: &SortType, user_name: String, -) -> Result { +) -> Result { let site_view = SiteView::read(&conn)?; let user = User_::find_by_username(&conn, &user_name)?; let user_url = user.get_profile_url(); @@ -156,7 +154,7 @@ fn get_feed_community( conn: &PgConnection, sort_type: &SortType, community_name: String, -) -> Result { +) -> Result { let site_view = SiteView::read(&conn)?; let community = Community::read_from_name(&conn, &community_name)?; @@ -185,7 +183,7 @@ fn get_feed_front( conn: &PgConnection, sort_type: &SortType, jwt: String, -) -> Result { +) -> Result { let site_view = SiteView::read(&conn)?; let user_id = Claims::decode(&jwt)?.claims.id; @@ -210,7 +208,7 @@ fn get_feed_front( Ok(channel_builder) } -fn get_feed_inbox(conn: &PgConnection, jwt: String) -> Result { +fn get_feed_inbox(conn: &PgConnection, jwt: String) -> Result { let site_view = SiteView::read(&conn)?; let user_id = Claims::decode(&jwt)?.claims.id; diff --git a/server/src/routes/nodeinfo.rs b/server/src/routes/nodeinfo.rs index db206a3e8..ff728fe3e 100644 --- a/server/src/routes/nodeinfo.rs +++ b/server/src/routes/nodeinfo.rs @@ -1,8 +1,10 @@ use crate::{ apub::get_apub_protocol_string, + blocking, db::site_view::SiteView, routes::DbPoolParam, version, + LemmyError, Settings, }; 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)); } -async fn node_info_well_known() -> Result, failure::Error> { +async fn node_info_well_known() -> Result, LemmyError> { let node_info = NodeInfoWellKnown { links: NodeInfoWellKnownLinks { rel: Url::parse("http://nodeinfo.diaspora.software/ns/schema/2.0")?, @@ -30,38 +32,34 @@ async fn node_info_well_known() -> Result, failure::Error> { } async fn node_info(db: DbPoolParam) -> Result { - let res = web::block(move || { - let conn = db.get()?; - let site_view = match SiteView::read(&conn) { - Ok(site_view) => site_view, - Err(_) => return Err(format_err!("not_found")), - }; - let protocols = if Settings::get().federation.enabled { - vec!["activitypub".to_string()] - } else { - vec![] - }; - Ok(NodeInfo { - version: "2.0".to_string(), - software: NodeInfoSoftware { - name: "lemmy".to_string(), - version: version::VERSION.to_string(), + let site_view = blocking(&db, SiteView::read) + .await? + .map_err(|_| ErrorBadRequest(LemmyError::from(format_err!("not_found"))))?; + + let protocols = if Settings::get().federation.enabled { + vec!["activitypub".to_string()] + } else { + vec![] + }; + + let json = NodeInfo { + version: "2.0".to_string(), + software: NodeInfoSoftware { + name: "lemmy".to_string(), + version: version::VERSION.to_string(), + }, + protocols, + usage: NodeInfoUsage { + users: NodeInfoUsers { + total: site_view.number_of_users, }, - protocols, - usage: NodeInfoUsage { - users: NodeInfoUsers { - total: site_view.number_of_users, - }, - local_posts: site_view.number_of_posts, - local_comments: site_view.number_of_comments, - open_registrations: site_view.open_registration, - }, - }) - }) - .await - .map(|json| HttpResponse::Ok().json(json)) - .map_err(ErrorBadRequest)?; - Ok(res) + local_posts: site_view.number_of_posts, + local_comments: site_view.number_of_comments, + open_registrations: site_view.open_registration, + }, + }; + + Ok(HttpResponse::Ok().json(json)) } #[derive(Serialize, Deserialize, Debug)] diff --git a/server/src/routes/webfinger.rs b/server/src/routes/webfinger.rs index 9fa01a147..af021dd5f 100644 --- a/server/src/routes/webfinger.rs +++ b/server/src/routes/webfinger.rs @@ -1,6 +1,8 @@ use crate::{ + blocking, db::{community::Community, user::User_}, routes::DbPoolParam, + LemmyError, Settings, }; use actix_web::{error::ErrorBadRequest, web::Query, *}; @@ -61,64 +63,58 @@ async fn get_webfinger_response( info: Query, db: DbPoolParam, ) -> Result { - let res = web::block(move || { - let conn = db.get()?; + let community_regex_parsed = WEBFINGER_COMMUNITY_REGEX + .captures(&info.resource) + .map(|c| c.get(1)) + .flatten(); - let community_regex_parsed = WEBFINGER_COMMUNITY_REGEX - .captures(&info.resource) - .map(|c| c.get(1)) - .flatten(); + let user_regex_parsed = WEBFINGER_USER_REGEX + .captures(&info.resource) + .map(|c| c.get(1)) + .flatten(); - let user_regex_parsed = WEBFINGER_USER_REGEX - .captures(&info.resource) - .map(|c| c.get(1)) - .flatten(); + let url = if let Some(community_name) = community_regex_parsed { + let community_name = community_name.as_str().to_owned(); + // Make sure the requested community exists. + blocking(&db, move |conn| { + Community::read_from_name(conn, &community_name) + }) + .await? + .map_err(|_| ErrorBadRequest(LemmyError::from(format_err!("not_found"))))? + .actor_id + } else if let Some(user_name) = user_regex_parsed { + let user_name = user_name.as_str().to_owned(); + // Make sure the requested user exists. + blocking(&db, move |conn| User_::read_from_name(conn, &user_name)) + .await? + .map_err(|_| ErrorBadRequest(LemmyError::from(format_err!("not_found"))))? + .actor_id + } else { + return Err(ErrorBadRequest(LemmyError::from(format_err!("not_found")))); + }; - let url = if let Some(community_name) = community_regex_parsed { - // Make sure the requested community exists. - let community = match Community::read_from_name(&conn, &community_name.as_str()) { - Ok(o) => o, - Err(_) => return Err(format_err!("not_found")), - }; - community.actor_id - } else if let Some(user_name) = user_regex_parsed { - // Make sure the requested user exists. - let user = match User_::read_from_name(&conn, &user_name.as_str()) { - Ok(o) => o, - Err(_) => return Err(format_err!("not_found")), - }; - user.actor_id - } else { - return Err(format_err!("not_found")); - }; + let json = WebFingerResponse { + subject: info.resource.to_owned(), + aliases: vec![url.to_owned()], + links: vec![ + WebFingerLink { + rel: Some("http://webfinger.net/rel/profile-page".to_string()), + type_: Some("text/html".to_string()), + href: Some(url.to_owned()), + template: None, + }, + WebFingerLink { + rel: Some("self".to_string()), + type_: Some("application/activity+json".to_string()), + href: Some(url), + template: None, + }, // TODO: this also needs to return the subscribe link once that's implemented + //{ + // "rel": "http://ostatus.org/schema/1.0/subscribe", + // "template": "https://my_instance.com/authorize_interaction?uri={uri}" + //} + ], + }; - let wf_res = WebFingerResponse { - subject: info.resource.to_owned(), - aliases: vec![url.to_owned()], - links: vec![ - WebFingerLink { - rel: Some("http://webfinger.net/rel/profile-page".to_string()), - type_: Some("text/html".to_string()), - href: Some(url.to_owned()), - template: None, - }, - WebFingerLink { - rel: Some("self".to_string()), - type_: Some("application/activity+json".to_string()), - href: Some(url), - template: None, - }, // TODO: this also needs to return the subscribe link once that's implemented - //{ - // "rel": "http://ostatus.org/schema/1.0/subscribe", - // "template": "https://my_instance.com/authorize_interaction?uri={uri}" - //} - ], - }; - - Ok(wf_res) - }) - .await - .map(|json| HttpResponse::Ok().json(json)) - .map_err(ErrorBadRequest)?; - Ok(res) + Ok(HttpResponse::Ok().json(json)) } diff --git a/server/src/settings.rs b/server/src/settings.rs index b3173457f..12ffaceab 100644 --- a/server/src/settings.rs +++ b/server/src/settings.rs @@ -1,5 +1,5 @@ +use crate::LemmyError; use config::{Config, ConfigError, Environment, File}; -use failure::Error; use serde::Deserialize; use std::{env, fs, net::IpAddr, sync::RwLock}; @@ -118,11 +118,11 @@ impl Settings { format!("{}/api/v1", self.hostname) } - pub fn read_config_file() -> Result { + pub fn read_config_file() -> Result { Ok(fs::read_to_string(CONFIG_FILE)?) } - pub fn save_config_file(data: &str) -> Result { + pub fn save_config_file(data: &str) -> Result { fs::write(CONFIG_FILE, data)?; // Reload the new settings diff --git a/server/src/version.rs b/server/src/version.rs index 0970c1759..ca1a5dc73 100644 --- a/server/src/version.rs +++ b/server/src/version.rs @@ -1 +1 @@ -pub const VERSION: &str = "v0.7.5"; +pub const VERSION: &str = "v0.7.11"; diff --git a/server/src/websocket/mod.rs b/server/src/websocket/mod.rs index 4eb43e49d..cdaf4f304 100644 --- a/server/src/websocket/mod.rs +++ b/server/src/websocket/mod.rs @@ -6,7 +6,6 @@ use diesel::{ r2d2::{ConnectionManager, Pool}, PgConnection, }; -use failure::Error; use log::{error, info}; use rand::{rngs::ThreadRng, Rng}; use serde::{Deserialize, Serialize}; diff --git a/server/src/websocket/server.rs b/server/src/websocket/server.rs index e4543ea1f..aef0abb8a 100644 --- a/server/src/websocket/server.rs +++ b/server/src/websocket/server.rs @@ -9,10 +9,13 @@ use crate::{ websocket::UserOperation, CommunityId, ConnectionId, + DbPool, IPAddr, + LemmyError, PostId, UserId, }; +use actix_web::client::Client; /// Chat server sends this messages to session #[derive(Message)] @@ -154,12 +157,16 @@ pub struct ChatServer { /// Rate limiting based on rate type and IP addr rate_limiter: RateLimit, + + /// An HTTP Client + client: Client, } impl ChatServer { pub fn startup( pool: Pool>, rate_limiter: RateLimit, + client: Client, ) -> ChatServer { ChatServer { sessions: HashMap::new(), @@ -169,6 +176,7 @@ impl ChatServer { rng: rand::thread_rng(), pool, rate_limiter, + client, } } @@ -236,7 +244,7 @@ impl ChatServer { response: &Response, post_id: PostId, my_id: Option, - ) -> Result<(), Error> + ) -> Result<(), LemmyError> where Response: Serialize, { @@ -260,7 +268,7 @@ impl ChatServer { response: &Response, community_id: CommunityId, my_id: Option, - ) -> Result<(), Error> + ) -> Result<(), LemmyError> where Response: Serialize, { @@ -283,7 +291,7 @@ impl ChatServer { op: &UserOperation, response: &Response, my_id: Option, - ) -> Result<(), Error> + ) -> Result<(), LemmyError> where Response: Serialize, { @@ -305,7 +313,7 @@ impl ChatServer { response: &Response, recipient_id: UserId, my_id: Option, - ) -> Result<(), Error> + ) -> Result<(), LemmyError> where Response: Serialize, { @@ -328,7 +336,7 @@ impl ChatServer { user_operation: &UserOperation, comment: &CommentResponse, my_id: Option, - ) -> Result<(), Error> { + ) -> Result<(), LemmyError> { let mut comment_reply_sent = comment.clone(); comment_reply_sent.comment.my_vote = None; comment_reply_sent.comment.user_id = None; @@ -366,7 +374,7 @@ impl ChatServer { user_operation: &UserOperation, post: &PostResponse, my_id: Option, - ) -> Result<(), Error> { + ) -> Result<(), LemmyError> { let community_id = post.post.community_id; // Don't send my data with it @@ -394,7 +402,7 @@ impl ChatServer { &mut self, msg: StandardMessage, ctx: &mut Context, - ) -> impl Future> { + ) -> impl Future> { let addr = ctx.address(); let pool = self.pool.clone(); let rate_limiter = self.rate_limiter.clone(); @@ -404,6 +412,7 @@ impl ChatServer { None => "blank_ip".to_string(), }; + let client = self.client.clone(); async move { let 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 args = Args { + client, + pool, + rate_limiter, + chatserver: addr, + id: msg.id, + ip, + op: user_operation.clone(), + data, + }; + match user_operation { // User ops - UserOperation::Login => { - do_user_operation::(pool, rate_limiter, addr, msg.id, ip, user_operation, data) - .await - } - UserOperation::Register => { - do_user_operation::(pool, rate_limiter, addr, msg.id, ip, user_operation, data) - .await - } - UserOperation::GetUserDetails => { - do_user_operation::( - pool, - rate_limiter, - addr, - msg.id, - ip, - user_operation, - data, - ) - .await - } - UserOperation::GetReplies => { - do_user_operation::( - pool, - rate_limiter, - addr, - msg.id, - ip, - user_operation, - data, - ) - .await - } - UserOperation::AddAdmin => { - do_user_operation::(pool, rate_limiter, addr, msg.id, ip, user_operation, data) - .await - } - UserOperation::BanUser => { - do_user_operation::(pool, rate_limiter, addr, msg.id, ip, user_operation, data) - .await - } - UserOperation::GetUserMentions => { - do_user_operation::( - pool, - rate_limiter, - addr, - msg.id, - ip, - user_operation, - data, - ) - .await - } - UserOperation::EditUserMention => { - do_user_operation::( - pool, - rate_limiter, - addr, - msg.id, - ip, - user_operation, - data, - ) - .await - } - UserOperation::MarkAllAsRead => { - do_user_operation::( - pool, - rate_limiter, - addr, - msg.id, - ip, - user_operation, - data, - ) - .await - } - UserOperation::DeleteAccount => { - do_user_operation::( - pool, - rate_limiter, - addr, - msg.id, - ip, - user_operation, - data, - ) - .await - } - UserOperation::PasswordReset => { - do_user_operation::( - pool, - rate_limiter, - addr, - msg.id, - ip, - user_operation, - data, - ) - .await - } - UserOperation::PasswordChange => { - do_user_operation::( - pool, - rate_limiter, - addr, - msg.id, - ip, - user_operation, - data, - ) - .await - } + UserOperation::Login => do_user_operation::(args).await, + UserOperation::Register => do_user_operation::(args).await, + UserOperation::GetUserDetails => do_user_operation::(args).await, + UserOperation::GetReplies => do_user_operation::(args).await, + UserOperation::AddAdmin => do_user_operation::(args).await, + UserOperation::BanUser => do_user_operation::(args).await, + UserOperation::GetUserMentions => do_user_operation::(args).await, + UserOperation::EditUserMention => do_user_operation::(args).await, + UserOperation::MarkAllAsRead => do_user_operation::(args).await, + UserOperation::DeleteAccount => do_user_operation::(args).await, + UserOperation::PasswordReset => do_user_operation::(args).await, + UserOperation::PasswordChange => do_user_operation::(args).await, UserOperation::CreatePrivateMessage => { - do_user_operation::( - pool, - rate_limiter, - addr, - msg.id, - ip, - user_operation, - data, - ) - .await - } - UserOperation::EditPrivateMessage => { - do_user_operation::( - pool, - rate_limiter, - addr, - msg.id, - ip, - user_operation, - data, - ) - .await - } - UserOperation::GetPrivateMessages => { - do_user_operation::( - pool, - rate_limiter, - addr, - msg.id, - ip, - user_operation, - data, - ) - .await - } - UserOperation::UserJoin => { - do_user_operation::(pool, rate_limiter, addr, msg.id, ip, user_operation, data) - .await - } - UserOperation::SaveUserSettings => { - do_user_operation::( - pool, - rate_limiter, - addr, - msg.id, - ip, - user_operation, - data, - ) - .await + do_user_operation::(args).await } + UserOperation::EditPrivateMessage => do_user_operation::(args).await, + UserOperation::GetPrivateMessages => do_user_operation::(args).await, + UserOperation::UserJoin => do_user_operation::(args).await, + UserOperation::SaveUserSettings => do_user_operation::(args).await, // Site ops - UserOperation::GetModlog => { - do_user_operation::(pool, rate_limiter, addr, msg.id, ip, user_operation, data) - .await - } - UserOperation::CreateSite => { - do_user_operation::( - pool, - rate_limiter, - addr, - msg.id, - ip, - user_operation, - data, - ) - .await - } - UserOperation::EditSite => { - do_user_operation::(pool, rate_limiter, addr, msg.id, ip, user_operation, data) - .await - } - UserOperation::GetSite => { - do_user_operation::(pool, rate_limiter, addr, msg.id, ip, user_operation, data) - .await - } - UserOperation::GetSiteConfig => { - do_user_operation::( - pool, - rate_limiter, - addr, - msg.id, - ip, - user_operation, - data, - ) - .await - } - UserOperation::SaveSiteConfig => { - do_user_operation::( - pool, - rate_limiter, - addr, - msg.id, - ip, - user_operation, - data, - ) - .await - } - UserOperation::Search => { - do_user_operation::(pool, rate_limiter, addr, msg.id, ip, user_operation, data) - .await - } - UserOperation::TransferCommunity => { - do_user_operation::( - pool, - rate_limiter, - addr, - msg.id, - ip, - user_operation, - data, - ) - .await - } - UserOperation::TransferSite => { - do_user_operation::( - pool, - rate_limiter, - addr, - msg.id, - ip, - user_operation, - data, - ) - .await - } - UserOperation::ListCategories => { - do_user_operation::( - pool, - rate_limiter, - addr, - msg.id, - ip, - user_operation, - data, - ) - .await - } + UserOperation::GetModlog => do_user_operation::(args).await, + UserOperation::CreateSite => do_user_operation::(args).await, + UserOperation::EditSite => do_user_operation::(args).await, + UserOperation::GetSite => do_user_operation::(args).await, + UserOperation::GetSiteConfig => do_user_operation::(args).await, + UserOperation::SaveSiteConfig => do_user_operation::(args).await, + UserOperation::Search => do_user_operation::(args).await, + UserOperation::TransferCommunity => do_user_operation::(args).await, + UserOperation::TransferSite => do_user_operation::(args).await, + UserOperation::ListCategories => do_user_operation::(args).await, // Community ops - UserOperation::GetCommunity => { - do_user_operation::( - pool, - rate_limiter, - addr, - msg.id, - ip, - user_operation, - data, - ) - .await - } - UserOperation::ListCommunities => { - do_user_operation::( - pool, - rate_limiter, - addr, - msg.id, - ip, - user_operation, - data, - ) - .await - } - UserOperation::CreateCommunity => { - do_user_operation::( - pool, - rate_limiter, - addr, - msg.id, - ip, - user_operation, - data, - ) - .await - } - UserOperation::EditCommunity => { - do_user_operation::( - pool, - rate_limiter, - addr, - msg.id, - ip, - user_operation, - data, - ) - .await - } - UserOperation::FollowCommunity => { - do_user_operation::( - pool, - rate_limiter, - addr, - msg.id, - ip, - user_operation, - data, - ) - .await - } + UserOperation::GetCommunity => do_user_operation::(args).await, + UserOperation::ListCommunities => do_user_operation::(args).await, + UserOperation::CreateCommunity => do_user_operation::(args).await, + UserOperation::EditCommunity => do_user_operation::(args).await, + UserOperation::FollowCommunity => do_user_operation::(args).await, UserOperation::GetFollowedCommunities => { - do_user_operation::( - pool, - rate_limiter, - addr, - msg.id, - ip, - user_operation, - data, - ) - .await - } - UserOperation::BanFromCommunity => { - do_user_operation::( - pool, - rate_limiter, - addr, - msg.id, - ip, - user_operation, - data, - ) - .await - } - UserOperation::AddModToCommunity => { - do_user_operation::( - pool, - rate_limiter, - addr, - msg.id, - ip, - user_operation, - data, - ) - .await + do_user_operation::(args).await } + UserOperation::BanFromCommunity => do_user_operation::(args).await, + UserOperation::AddModToCommunity => do_user_operation::(args).await, // Post ops - UserOperation::CreatePost => { - do_user_operation::( - pool, - rate_limiter, - addr, - msg.id, - ip, - user_operation, - data, - ) - .await - } - UserOperation::GetPost => { - do_user_operation::(pool, rate_limiter, addr, msg.id, ip, user_operation, data) - .await - } - UserOperation::GetPosts => { - do_user_operation::(pool, rate_limiter, addr, msg.id, ip, user_operation, data) - .await - } - UserOperation::EditPost => { - do_user_operation::(pool, rate_limiter, addr, msg.id, ip, user_operation, data) - .await - } - UserOperation::CreatePostLike => { - do_user_operation::( - pool, - rate_limiter, - addr, - msg.id, - ip, - user_operation, - data, - ) - .await - } - UserOperation::SavePost => { - do_user_operation::(pool, rate_limiter, addr, msg.id, ip, user_operation, data) - .await - } + UserOperation::CreatePost => do_user_operation::(args).await, + UserOperation::GetPost => do_user_operation::(args).await, + UserOperation::GetPosts => do_user_operation::(args).await, + UserOperation::EditPost => do_user_operation::(args).await, + UserOperation::CreatePostLike => do_user_operation::(args).await, + UserOperation::SavePost => do_user_operation::(args).await, // Comment ops - UserOperation::CreateComment => { - do_user_operation::( - pool, - rate_limiter, - addr, - msg.id, - ip, - user_operation, - data, - ) - .await - } - UserOperation::EditComment => { - do_user_operation::( - pool, - rate_limiter, - addr, - msg.id, - ip, - user_operation, - data, - ) - .await - } - UserOperation::SaveComment => { - do_user_operation::( - pool, - rate_limiter, - addr, - msg.id, - ip, - user_operation, - data, - ) - .await - } - UserOperation::GetComments => { - do_user_operation::( - pool, - rate_limiter, - addr, - msg.id, - ip, - user_operation, - data, - ) - .await - } - UserOperation::CreateCommentLike => { - do_user_operation::( - pool, - rate_limiter, - addr, - msg.id, - ip, - user_operation, - data, - ) - .await - } + UserOperation::CreateComment => do_user_operation::(args).await, + UserOperation::EditComment => do_user_operation::(args).await, + UserOperation::SaveComment => do_user_operation::(args).await, + UserOperation::GetComments => do_user_operation::(args).await, + UserOperation::CreateCommentLike => do_user_operation::(args).await, } } } } -async fn do_user_operation<'a, Data>( - pool: Pool>, +struct Args<'a> { + client: Client, + pool: DbPool, rate_limiter: RateLimit, chatserver: Addr, id: ConnectionId, ip: IPAddr, op: UserOperation, - data: &str, -) -> Result + data: &'a str, +} + +async fn do_user_operation<'a, 'b, Data>(args: Args<'b>) -> Result where for<'de> Data: Deserialize<'de> + 'a, Oper: Perform, { + let Args { + client, + pool, + rate_limiter, + chatserver, + id, + ip, + op, + data, + } = args; + let ws_info = WebsocketInfo { chatserver, id: Some(id), @@ -898,17 +534,14 @@ where let data = data.to_string(); let op2 = op.clone(); + let client = client.clone(); let fut = async move { - actix_web::web::block(move || { - let parsed_data: Data = serde_json::from_str(&data)?; - let res = Oper::new(parsed_data).perform(pool, Some(ws_info))?; - to_json_string(&op, &res) - }) - .await - .map_err(|e| match e { - actix_web::error::BlockingError::Error(e) => e, - _ => APIError::err("Operation canceled").into(), - }) + let pool = pool.clone(); + let parsed_data: Data = serde_json::from_str(&data)?; + let res = Oper::new(parsed_data, client) + .perform(&pool, Some(ws_info)) + .await?; + to_json_string(&op, &res) }; match op2 { @@ -1109,7 +742,7 @@ struct WebsocketResponse { data: T, } -fn to_json_string(op: &UserOperation, data: &Response) -> Result +fn to_json_string(op: &UserOperation, data: &Response) -> Result where Response: Serialize, { diff --git a/ui/assets/css/main.css b/ui/assets/css/main.css index c1f004d7a..fd65148c7 100644 --- a/ui/assets/css/main.css +++ b/ui/assets/css/main.css @@ -249,3 +249,18 @@ pre { white-space: pre-wrap; word-break: keep-all; } + +.form-control.search-input { + float: right !important; + transition: width 0.2s ease-out 0s !important; +} + +.show-input { + width: 13em !important; + +} +.hide-input { + background: transparent !important; + width: 0px !important; + padding: 0 !important; + } diff --git a/ui/src/api_tests/api.spec.ts b/ui/src/api_tests/api.spec.ts index 7337201c7..41710e11b 100644 --- a/ui/src/api_tests/api.spec.ts +++ b/ui/src/api_tests/api.spec.ts @@ -124,10 +124,10 @@ describe('main', () => { }); describe('follow_accept', () => { - test('/u/lemmy_alpha follows and accepts lemmy_beta/c/main', async () => { - // Make sure lemmy_beta/c/main is cached on lemmy_alpha + test('/u/lemmy_alpha follows and accepts lemmy-beta/c/main', async () => { + // Make sure lemmy-beta/c/main is cached on lemmy_alpha // 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, { method: 'GET', @@ -215,7 +215,7 @@ describe('main', () => { // Also make G follow B // 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, { method: 'GET', @@ -449,7 +449,7 @@ describe('main', () => { // Lemmy alpha responds to their own comment, but mentions lemmy beta. // 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 = { content: mentionContent, post_id: 2, @@ -550,7 +550,7 @@ describe('main', () => { expect(createCommunityRes.community.name).toBe(communityName); // 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, { method: 'GET', }).then(d => d.json()); @@ -826,7 +826,7 @@ describe('main', () => { expect(createCommunityRes.community.name).toBe(communityName); // 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, { method: 'GET', }).then(d => d.json()); @@ -1278,7 +1278,7 @@ describe('main', () => { // Create a test comment on Gamma, make sure it gets announced to alpha 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 = { content: commentContent, @@ -1417,7 +1417,7 @@ describe('main', () => { expect(createChildCommentRes.comment.content).toBe(childCommentContent); // 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, { method: 'GET', diff --git a/ui/src/components/comment-form.tsx b/ui/src/components/comment-form.tsx index 45974cc07..61ee3d77b 100644 --- a/ui/src/components/comment-form.tsx +++ b/ui/src/components/comment-form.tsx @@ -185,6 +185,7 @@ export class CommentForm extends Component { target="_blank" class="d-inline-block float-right text-muted font-weight-bold" title={i18n.t('formatting_help')} + rel="noopener" > @@ -262,7 +263,9 @@ export class CommentForm extends Component { // If its a comment edit, only check that its from your user, and that its a // 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.loading = false; diff --git a/ui/src/components/community.tsx b/ui/src/components/community.tsx index 373d8f807..c193532b0 100644 --- a/ui/src/components/community.tsx +++ b/ui/src/components/community.tsx @@ -217,6 +217,7 @@ export class Community extends Component { }`} target="_blank" title="RSS" + rel="noopener" > # diff --git a/ui/src/components/iframely-card.tsx b/ui/src/components/iframely-card.tsx index 3a89023f5..0d3f43f69 100644 --- a/ui/src/components/iframely-card.tsx +++ b/ui/src/components/iframely-card.tsx @@ -44,7 +44,12 @@ export class IFramelyCard extends Component< ) : ( - + {post.embed_title} @@ -55,6 +60,7 @@ export class IFramelyCard extends Component< class="text-muted font-italic" target="_blank" href={post.url} + rel="noopener" > {new URL(post.url).hostname} diff --git a/ui/src/components/inbox.tsx b/ui/src/components/inbox.tsx index edbacd518..3b963a79f 100644 --- a/ui/src/components/inbox.tsx +++ b/ui/src/components/inbox.tsx @@ -109,6 +109,7 @@ export class Inbox extends Component { href={`/feeds/inbox/${UserService.Instance.auth}.xml`} target="_blank" title="RSS" + rel="noopener" > # diff --git a/ui/src/components/login.tsx b/ui/src/components/login.tsx index ce04d0d4f..978993458 100644 --- a/ui/src/components/login.tsx +++ b/ui/src/components/login.tsx @@ -20,6 +20,11 @@ interface State { loginLoading: boolean; registerLoading: boolean; enable_nsfw: boolean; + mathQuestion: { + a: number; + b: number; + answer: number; + }; } export class Login extends Component { @@ -40,6 +45,11 @@ export class Login extends Component { loginLoading: false, registerLoading: false, 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) { @@ -215,6 +225,23 @@ export class Login extends Component { /> +
+ + +
+ +
+
{this.state.enable_nsfw && (
@@ -235,7 +262,11 @@ export class Login extends Component { )}
- + + )} + + {this.state.isLoggedIn ? ( + <> + + + + + ) : ( + + )}
); diff --git a/ui/src/components/post-form.tsx b/ui/src/components/post-form.tsx index 9f5aa363e..fdf6ebe47 100644 --- a/ui/src/components/post-form.tsx +++ b/ui/src/components/post-form.tsx @@ -222,6 +222,7 @@ export class PostForm extends Component { )}`} target="_blank" class="mr-2 d-inline-block float-right text-muted small font-weight-bold" + rel="noopener" > {i18n.t('archive_link')} @@ -302,6 +303,7 @@ export class PostForm extends Component { diff --git a/ui/src/components/post-listing.tsx b/ui/src/components/post-listing.tsx index b4cc4f928..3d6088427 100644 --- a/ui/src/components/post-listing.tsx +++ b/ui/src/components/post-listing.tsx @@ -197,6 +197,7 @@ export class PostListing extends Component { className="text-body" href={post.url} target="_blank" + rel="noopener" title={post.url} > {this.imgThumb(this.getImage(true))} @@ -227,6 +228,7 @@ export class PostListing extends Component { href={post.url} target="_blank" title={post.url} + rel="noopener" > @@ -303,6 +305,7 @@ export class PostListing extends Component { href={post.url} target="_blank" title={post.url} + rel="noopener" > {post.name} @@ -323,6 +326,7 @@ export class PostListing extends Component { href={post.url} target="_blank" title={post.url} + rel="noopener" > {hostname(post.url)} diff --git a/ui/src/components/private-message-form.tsx b/ui/src/components/private-message-form.tsx index 8cb7590e6..107823615 100644 --- a/ui/src/components/private-message-form.tsx +++ b/ui/src/components/private-message-form.tsx @@ -175,6 +175,7 @@ export class PrivateMessageForm extends Component< # @@ -236,6 +237,7 @@ export class PrivateMessageForm extends Component< diff --git a/ui/src/components/sort-select.tsx b/ui/src/components/sort-select.tsx index a6ce2ea98..05abdb20a 100644 --- a/ui/src/components/sort-select.tsx +++ b/ui/src/components/sort-select.tsx @@ -47,6 +47,7 @@ export class SortSelect extends Component { className="text-muted" href={sortingHelpUrl} target="_blank" + rel="noopener" title={i18n.t('sorting_help')} > diff --git a/ui/src/components/sponsors.tsx b/ui/src/components/sponsors.tsx index 7e5eed642..31a4ee5ef 100644 --- a/ui/src/components/sponsors.tsx +++ b/ui/src/components/sponsors.tsx @@ -6,10 +6,12 @@ import { repoUrl } from '../utils'; interface SilverUser { name: string; - link: string; + link?: string; } let general = [ + 'dude in phx', + 'twilight loki', 'Andrew Plaza', 'Jonathan Cremin', 'Arthur Nieuwland', @@ -19,7 +21,7 @@ let general = [ 'Andre Vallestero', 'NotTooHighToHack', ]; -let highlighted = ['Oskenso Kashi', 'Alex Benishek']; +let highlighted = ['DiscountFuneral', 'Oskenso Kashi', 'Alex Benishek']; let silver: Array = [ { name: 'Redjoker', @@ -89,9 +91,13 @@ export class Sponsors extends Component { {silver.map(s => (
- - 💎 {s.name} - + {s.link ? ( + + 💎 {s.name} + + ) : ( +
💎 {s.name}
+ )}
))} diff --git a/ui/src/components/symbols.tsx b/ui/src/components/symbols.tsx index cdb7436a4..77d7a0860 100644 --- a/ui/src/components/symbols.tsx +++ b/ui/src/components/symbols.tsx @@ -159,8 +159,8 @@ export class Symbols extends Component { /> - - + + diff --git a/ui/src/components/user.tsx b/ui/src/components/user.tsx index f635a1cd0..69914fd39 100644 --- a/ui/src/components/user.tsx +++ b/ui/src/components/user.tsx @@ -317,6 +317,7 @@ export class User extends Component { SortType[this.state.sort] }`} target="_blank" + rel="noopener" title="RSS" > @@ -463,6 +464,7 @@ export class User extends Component { !this.state.user.matrix_user_id && 'disabled' }`} target="_blank" + rel="noopener" href={`https://matrix.to/#/${this.state.user.matrix_user_id}`} > {i18n.t('send_secure_message')} @@ -586,7 +588,11 @@ export class User extends Component {
diff --git a/ui/src/i18next.ts b/ui/src/i18next.ts index 5fa8f4e88..7a341ceab 100644 --- a/ui/src/i18next.ts +++ b/ui/src/i18next.ts @@ -24,6 +24,7 @@ import { gl } from './translations/gl'; import { tr } from './translations/tr'; import { hu } from './translations/hu'; import { uk } from './translations/uk'; +import { sq } from './translations/sq'; // https://github.com/nimbusec-oss/inferno-i18next/blob/master/tests/T.test.js#L66 const resources = { @@ -51,6 +52,7 @@ const resources = { tr, hu, uk, + sq, }; function format(value: any, format: any, lng: any): any { diff --git a/ui/src/utils.ts b/ui/src/utils.ts index 3bb7e9255..73e55b471 100644 --- a/ui/src/utils.ts +++ b/ui/src/utils.ts @@ -21,6 +21,7 @@ import 'moment/locale/gl'; import 'moment/locale/tr'; import 'moment/locale/hu'; import 'moment/locale/uk'; +import 'moment/locale/sq'; import { UserOperation, @@ -83,6 +84,7 @@ export const languages = [ { code: 'fi', name: 'Suomi' }, { code: 'fr', name: 'Français' }, { code: 'sv', name: 'Svenska' }, + { code: 'sq', name: 'Shqip' }, { code: 'tr', name: 'Türkçe' }, { code: 'uk', name: 'українська мова' }, { code: 'ru', name: 'Русский' }, @@ -414,6 +416,8 @@ export function getMomentLanguage(): string { lang = 'hu'; } else if (lang.startsWith('uk')) { lang = 'uk'; + } else if (lang.startsWith('sq')) { + lang = 'sq'; } else { lang = 'en'; } diff --git a/ui/src/version.ts b/ui/src/version.ts index 01d3be811..942ebfbed 100644 --- a/ui/src/version.ts +++ b/ui/src/version.ts @@ -1 +1 @@ -export const version: string = 'v0.7.5'; +export const version: string = 'v0.7.11'; diff --git a/ui/translations/ar.json b/ui/translations/ar.json index f963439ef..99140cdf0 100644 --- a/ui/translations/ar.json +++ b/ui/translations/ar.json @@ -44,12 +44,12 @@ "remove_as_admin": "إزالة كمدير", "appoint_as_admin": "تعيين كمدير", "remove": "إزالة", - "removed": "تمت إزالته", + "removed": "أزاله المشرف", "reason": "السبب", "mark_as_read": "تعيين كمقروء", "mark_as_unread": "تعيين كغير مقروء بعد", "delete": "حذف", - "deleted": "تم حذفه", + "deleted": "حذفه صاحبه", "restore": "استعادة", "ban": "طرد", "ban_from_site": "طرده مِن الموقع", @@ -219,5 +219,6 @@ "banned_users": "المستخدمون المحظورون", "reset_password_mail_sent": "لقد أرسِلت إليك رسالة إلكترونية لتصفير كلمتك السرية.", "upvote": "صوّت إيجابيا", - "downvote": "صوّت سلبيا" + "downvote": "صوّت سلبيا", + "select_a_community": "اختر مجتمعًا" } diff --git a/ui/translations/de.json b/ui/translations/de.json index ef42d4189..3199bc7ad 100644 --- a/ui/translations/de.json +++ b/ui/translations/de.json @@ -50,14 +50,14 @@ "remove_as_admin": "Als Administrator entfernen", "appoint_as_admin": "Zum Administrator ernennen", "remove": "entfernen", - "removed": "entfernt", + "removed": "entfernt durch die Moderation", "locked": "gesperrt", "stickied": "angeheftet", "reason": "Grund", "mark_as_read": "als gelesen markieren", "mark_as_unread": "als ungelesen markieren", "delete": "löschen", - "deleted": "gelöscht", + "deleted": "vom Ersteller gelöscht", "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.", "restore": "wiederherstellen", @@ -150,7 +150,7 @@ "theme": "Aussehen", "sponsors": "Sponsoren", "sponsors_of_lemmy": "Sponsoren von Lemmy", - "sponsor_message": "Lemmy ist freie <1>Open-Source 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 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_liberapay": "Auf Liberapay unterstützen", "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_plural": "{{count}} Stimmen", "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." } diff --git a/ui/translations/el.json b/ui/translations/el.json index 7d7d14978..4dab7c884 100644 --- a/ui/translations/el.json +++ b/ui/translations/el.json @@ -102,7 +102,7 @@ "category": "Κατηγορία", "subscribers": "Εγγεγραμμένοι", "both": "Και οι δύο", - "saved": "Αποθηκεύτηκε", + "saved": "Αποθηκευμένα", "prev": "Προηγούμενο", "next": "Επόμενο", "sidebar": "Πλευρικό μενού", @@ -116,7 +116,7 @@ "mark_all_as_read": "επισήμανση όλων ως διαβασμένα", "type": "Είδος", "unread": "Μη διαβασμένα", - "url": "Ενιαίος Εντοπιστής Πόρων (URL)", + "url": "URL", "subscribed": "Εγγεγραμμένος", "week": "Εβδομάδα", "month": "Μήνας", @@ -151,8 +151,8 @@ "reset_password_mail_sent": "Μόλις στάλθηκε ένα μήνυμα ηλεκτρονικού ταχυδρομείου για την επαναφορά του κωδικού σας.", "password_change": "Αλλαγή κωδικού", "new_password": "Νέος κωδικός", - "no_email_setup": "Αυτός ο διακομιστής δεν έχει εγκαταστήσει σωστά το ηλεκτρονικό ταχυδρομείο.", - "email": "Ηλεκτρονικό ταχυδρομείο", + "no_email_setup": "Αυτός ο διακομιστής δεν έχει εγκαταστήσει σωστά το email.", + "email": "Email", "matrix_user_id": "Χρήστης Matrix", "private_message_disclaimer": "Προσοχή: τα προσωπικά μηνύματα στο Lemmy δεν είναι ασφαλή. Παρακαλούμε δημιουργήστε έναν λογαριασμό στο <1>Riot.im για ασφαλή επικοινωνία.", "send_notifications_to_email": "Αποστολή ειδοποιήσεων στη διεύθυνση ηλεκτρονικού ταχυδρομείου", @@ -200,7 +200,7 @@ "monero": "Monero", "code": "Κώδικας", "by": "από", - "to": "μέχρι", + "to": "προς", "from": "από", "transfer_community": "μεταφορά κοινότητας", "transfer_site": "μεταφορά ιστότοπου", diff --git a/ui/translations/en.json b/ui/translations/en.json index 6d2d1c5dc..62b11ce4a 100644 --- a/ui/translations/en.json +++ b/ui/translations/en.json @@ -264,5 +264,6 @@ "time": "Time", "action": "Action", "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" } diff --git a/ui/translations/eo.json b/ui/translations/eo.json index 5bde84f46..78821bf03 100644 --- a/ui/translations/eo.json +++ b/ui/translations/eo.json @@ -1,33 +1,34 @@ { - "post": "Poŝti", - "remove_post": "Fortiri Poŝton", - "no_posts": "Ne Poŝtoj.", - "create_a_post": "Verki Poŝton", - "create_post": "Verki Poŝton", - "number_of_posts": "{{count}} Poŝtoj", - "posts": "Poŝtoj", - "related_posts": "Tiuj poŝtoj eble rilatas", - "cross_posts": "Tiuj ligilo ankaŭ estas poŝtinta al:", - "cross_post": "laŭapoŝto", + "post": "Afiŝi", + "remove_post": "Forigi afiŝon", + "no_posts": "Neniuj afiŝoj.", + "create_a_post": "Verki afiŝon", + "create_post": "Verki afiŝon", + "number_of_posts": "{{count}} afiŝo", + "number_of_posts_plural": "{{count}} afiŝoj", + "posts": "Afiŝoj", + "related_posts": "Ĉi tiuj afiŝoj eble rilatas", + "cross_posts": "Tiu ligilo ankaŭ estas afiŝita al:", + "cross_post": "transafiŝo", "comments": "Komentoj", - "number_of_comments": "{{count}} Komento", - "number_of_comments_plural": "{{count}} Komentoj", - "remove_comment": "Fortiri Komentojn", + "number_of_comments": "{{count}} komento", + "number_of_comments_plural": "{{count}} komentoj", + "remove_comment": "Forigi komenton", "communities": "Komunumoj", "users": "Uzantoj", "create_a_community": "Krei komunumon", - "create_community": "Krei Komunumon", - "remove_community": "Forigi Komunumon", - "subscribed_to_communities": "Abonita al <1>komunumoj", - "trending_communities": "Furora <1>komunumoj", + "create_community": "Krei komunumon", + "remove_community": "Forigi komunumon", + "subscribed_to_communities": "Abonanta <1>komunumojn", + "trending_communities": "Furoraj <1>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", - "reply": "repliki", - "cancel": "nuligi", + "reply": "respondi", + "cancel": "Nuligi", "unlock": "malŝlosi", "lock": "ŝlosi", - "link": "ligi", + "link": "ligilo", "mod": "moderanto", "mods": "moderantoj", "moderates": "Moderigas", @@ -37,17 +38,17 @@ "modlog": "Moderlogo", "admin": "administranto", "admins": "administrantoj", - "remove_as_admin": "forigi per administranto", - "appoint_as_admin": "nomumi per administranto", - "remove": "fortiri", + "remove_as_admin": "forigi kiel administranto", + "appoint_as_admin": "nomumi administranto", + "remove": "forigi", "removed": "fortirita", "locked": "ŝlosita", "reason": "Kialo", - "mark_as_read": "marki kiel legita", - "mark_as_unread": "marki kiel nelegita", + "mark_as_read": "marki legita", + "mark_as_unread": "marki nelegita", "delete": "forigi", - "deleted": "forigita", - "restore": "restaŭri", + "deleted": "forigita de la kreinto", + "restore": "revenigi", "ban": "forbari", "ban_from_site": "forbari de retejo", "unban": "malforbari", @@ -55,11 +56,14 @@ "save": "konservi", "unsave": "malkonservi", "create": "krei", - "username": "Uzantnomo", - "email_or_username": "Retadreso aŭ Uzantnomo", - "number_of_users": "{{count}} Uzantoj", - "number_of_subscribers": "{{count}} Abonantoj", - "number_of_points": "{{count}} Voĉdonoj", + "username": "Uzantonomo", + "email_or_username": "Retpoŝtadreso aŭ uzantonomo", + "number_of_users": "{{count}} uzanto", + "number_of_users_plural": "{{count}} uzantoj", + "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", "title": "Titolo", "category": "Kategorio", @@ -69,10 +73,10 @@ "unsubscribe": "Malaboni", "subscribe": "Aboni", "subscribed": "Abonita", - "prev": "Antaŭe", - "next": "Poste", - "sidebar": "Flankstango", - "sort_type": "Klasi per kia", + "prev": "Malpluen", + "next": "Pluen", + "sidebar": "Flankobreto", + "sort_type": "Ordigilo", "hot": "Varmaj", "new": "Novaj", "top_day": "Supraj tagaj", @@ -84,46 +88,46 @@ "api": "API", "inbox": "Ricevujo", "inbox_for": "Ricevujo de <1>{{user}}", - "mark_all_as_read": "marki ĉiujn kiel legitaj", + "mark_all_as_read": "marki ĉiujn legitaj", "type": "Tipo", "unread": "Nelegitaj", - "reply_sent": "Repliko sendis", + "reply_sent": "Respondo sendiĝis", "search": "Serĉi", "overview": "Resumo", "view": "Rigardi", - "logout": "Elsaluti", - "login_sign_up": "Ensaluti / Registriĝi", - "login": "Ensaluti", + "logout": "Adiaŭi", + "login_sign_up": "Saluti / Registriĝi", + "login": "Saluti", "sign_up": "Registriĝi", - "notifications_error": "Labortablaj avizoj estas nehavebla en via retumilo. Provu Firefox-on aŭ Chrome-on.", - "unread_messages": "Nelegitaj Mesaĝoj", + "notifications_error": "Labortablaj avizoj estas nehaveblaj en via foliumilo. Provu foliumilojn Firefox aŭ Chrome.", + "unread_messages": "Nelegitaj mesaĝoj", "password": "Pasvorto", - "verify_password": "Konfirmu Vian Pasvorton", - "email": "Retadreso", - "optional": "Fakultativa", + "verify_password": "Konfirmu vian pasvorton", + "email": "Retpoŝtadreso", + "optional": "Malnepra", "expires": "Finiĝos", "url": "URL", "body": "Ĉefparto", - "copy_suggested_title": "kopii la sugestiitan titolon: {{title}}", + "copy_suggested_title": "kopii la proponitan titolon: {{title}}", "community": "Komunumo", - "expand_here": "Ekspansii ĉi tie", + "expand_here": "Etendi ĉi tie", "subscribe_to_communities": "Aboni al iuj <1>komunumoj.", "chat": "Babilo", - "recent_comments": "Freŝaj Komentoj", - "no_results": "Ne rezultoj.", + "recent_comments": "Freŝaj komentoj", + "no_results": "Neniuj rezultoj.", "setup": "Agordi", - "lemmy_instance_setup": "Agordi Instancon de Lemmy", - "setup_admin": "Agordi Retejan Administranton", + "lemmy_instance_setup": "Agordi nodon de Lemmy", + "setup_admin": "Agordi administranton de retejo", "your_site": "via retejo", "modified": "modifita", - "nsfw": "NSFW", - "show_nsfw": "Vidigi NSFW-an enhavon", + "nsfw": "Konsterna", + "show_nsfw": "Montri konsternan enhavon", "sponsors": "Subtenantoj", "sponsors_of_lemmy": "Subtenantoj de Lemmy", - "sponsor_message": "Lemmy estas senpaga, <1>liberkoda 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 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", - "general_sponsors": "Ĝeneralaj Subtenantoj estas tiuj ke donacis inter $10 kaj $39 al Lemmy.", - "crypto": "Crypto", + "general_sponsors": "Ĝeneralaj subtenantoj estas tiuj, kiuj donacis inter $10 kaj $39 al Lemmy.", + "crypto": "Ĉifroteĥnikaro", "bitcoin": "Bitcoin", "ethereum": "Ethereum", "monero": "Monero", @@ -133,45 +137,124 @@ "to": "al", "transfer_community": "transdoni la komunumon", "transfer_site": "transdoni la retejon", - "powered_by": "Konstruis per", - "landing_0": "Lemmy estas <1>ligila agregatilo / Reddit anstataŭo ke intenciĝas funkci en la <2>federacio-universo.<3>ĝi estas mem-gastigebla, havas nuna-ĝisdatigajn komentarojn, kaj estas malgrandega (<4>~80kB). Federacio en la ActivityPub-an reton estas planizita. <5>Estas <6>fruega beta versio, kaj multaj trajtoj estas nune difektaj aŭ mankaj. <7>Sugestias novajn trajtojn aŭ raportas cimojn <8>ĉi tie.<9>Faris per <10>Rust, <11>Actix, <12>Inferno, <13>Typescript.", - "not_logged_in": "Ne estas ensalutinta.", + "powered_by": "Konstruita per", + "landing": "Lemmy estas <1>amasigilo de ligiloj / alternativo de Reddit, intencita funkcii en la <2>federuniverso.<3>ĝi estas mem-gastigebla, havas tuj-ĝisdatigojn de komentaroj, kaj estas malgrandega (<4>~80kB). Federado en la reto de ActivityPub estas planita. <5>Ĉi tio estas <6>tre frua beta-versio, kaj multaj funkcioj estas nune difektaj aŭ mankaj. <7>Proponu novajn funkciojn aŭ raportu erarojn <8>ĉi tie.<9>Konstruita per <10>Rust, <11>Actix, <12>Inferno, <13>Typescript.", + "not_logged_in": "Nesalutinta.", "community_ban": "Vi estas forbarita de la komunumo.", "site_ban": "Vi estas forbarita de la retejo", "couldnt_create_comment": "Ne povis krei la komenton.", "couldnt_like_comment": "Ne povis ŝati la komenton.", - "couldnt_update_comment": "Ne povis ĝisdatigi komenton.", - "couldnt_save_comment": "Ne povis konservi komenton.", + "couldnt_update_comment": "Ne povis ĝisdatigi la komenton.", + "couldnt_save_comment": "Ne povis konservi 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.", "couldnt_find_community": "Ne povis trovi la komunumon.", "couldnt_update_community": "Ne povis ĝisdatigi la komunumon.", "community_already_exists": "Komunumo jam ekzistas.", "community_moderator_already_exists": "Komunuma moderanto jam ekzistas.", - "community_follower_already_exists": "Komunuma sekvanto.", - "community_user_already_banned": "Komunuma uzanto jam estas forbarita.", - "couldnt_create_post": "Ne povis krei la poŝton.", - "couldnt_like_post": "Ne povis ŝati la poŝton.", - "couldnt_find_post": "Ne povis trovi la poŝton.", - "couldnt_get_posts": "Ne povis irpreni poŝtojn", - "couldnt_update_post": "Ne povis ĝisdatigi la poŝton", - "couldnt_save_post": "Ne povis konservi la poŝton.", - "no_slurs": "Ne bigotaj vortoj.", + "community_follower_already_exists": "Abonanto de komunumo jam ekzistas.", + "community_user_already_banned": "Uzanto de komunumo jam estas forbarita.", + "couldnt_create_post": "Ne povis krei la afiŝon.", + "couldnt_like_post": "Ne povis ŝati la afiŝon.", + "couldnt_find_post": "Ne povis trovi la afiŝon.", + "couldnt_get_posts": "Ne povis akiri afiŝojn", + "couldnt_update_post": "Ne povis ĝisdatigi la afiŝon", + "couldnt_save_post": "Ne povis konservi la afiŝon.", + "no_slurs": "Neniuj fivortoj.", "not_an_admin": "Ne estas administranto.", "site_already_exists": "Retejo jam ekzistas.", "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.", "passwords_dont_match": "Pasvortoj ne samas.", "admin_already_created": "Pardonu, jam estas administranto.", "user_already_exists": "Uzanto jam ekzistas.", "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", "message": "Mesaĝo", - "number_of_communities": "{{count}} Komunumo", - "number_of_communities_plural": "{{count}} Komunumoj", + "number_of_communities": "{{count}} komunumo", + "number_of_communities_plural": "{{count}} komunumoj", "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 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" } diff --git a/ui/translations/it.json b/ui/translations/it.json index 2e9a48d8f..cf8c0ea69 100644 --- a/ui/translations/it.json +++ b/ui/translations/it.json @@ -56,7 +56,7 @@ "mark_as_read": "segna come letto", "mark_as_unread": "segna come non letto", "delete": "cancella", - "deleted": "eliminato dall'autore del commento", + "deleted": "eliminato dal creatore", "delete_account": "Cancella Account", "delete_account_confirm": "Attenzione: stai per cancellare permanentemente tutti i tuoi dati. Inserisci la tua password per confermare questa azione.", "restore": "ripristina", @@ -151,7 +151,7 @@ "ethereum": "Ethereum", "monero": "Monero", "code": "Codice", - "joined": "Iscritto da", + "joined": "Iscritto", "by": "di", "to": "su", "transfer_community": "trasferisci comunità", @@ -175,7 +175,7 @@ "couldnt_update_community": "Impossibile aggiornare la comunità.", "community_already_exists": "La comunità esiste già.", "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.", "couldnt_create_post": "Impossibile creare la pubblicazione.", "couldnt_like_post": "Impossibile apprezzare la pubblicazione.", diff --git a/ui/translations/sq.json b/ui/translations/sq.json new file mode 100644 index 000000000..8504a1b97 --- /dev/null +++ b/ui/translations/sq.json @@ -0,0 +1,260 @@ +{ + "remove_post": "Hiqe Postimin", + "no_posts": "Nuk ka Postime.", + "create_a_post": "Krijo Postim", + "create_post": "Krijo Postim", + "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 Komunitet", + "remove_community": "Fshije Komunitetin", + "subscribed_to_communities": "Jeni abonuar në <1>communities", + "trending_communities": "Komunitetet në trend <1>communities", + "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": "Shkruaji 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": "riktheje", + "ban": "dëboje", + "ban_from_site": "dëboje nga faqja", + "save": "ruaj", + "unsave": "hiqe", + "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": "publiko", + "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 autori", + "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": "Ndalo Abonimin", + "prev": "E mëparshmja", + "to": "në", + "by": "nga", + "joined": "U anëtarësuat", + "code": "Kodi", + "monero": "Monero", + "bitcoin": "Bitcoin", + "crypto": "Crypto", + "general_sponsors": "Sponsorët General janë ata që dhuruan $10 deri $39 për Lemmy.", + "donate": "Dhuroni", + "donate_to_lemmy": "Dhuroji Lemmy-it", + "support_on_open_collective": "Na përkrahni në OpenCollective", + "support_on_liberapay": "Na përkrahni në Liberapay", + "support_on_patreon": "Na përkrahni në Patreon", + "sponsors_of_lemmy": "Sponsorët e Lemmy", + "sponsors": "Sponsorët", + "show_nsfw": "Shfaq përmbajtje NSFW", + "modified": "e modifikuar", + "your_site": "faqja jote", + "couldnt_update_community": "Komuniteti nuk mundi të përditësohej.", + "couldnt_find_community": "Komuniteti nuk mund të gjendej.", + "no_post_edit_allowed": "Nuk lejohet redaktimi i postimit.", + "no_comment_edit_allowed": "Nuk të lejohet redaktimi i komentit.", + "couldnt_get_comments": "Nuk mund të merrnim komentet.", + "couldnt_update_comment": "Përditësimi i komentit nuk ishte i mundshëm.", + "couldnt_like_comment": "Pëlqimi i komentit nuk ishte i mundshëm.", + "couldnt_create_comment": "Krijimi i komentit nuk ishte i mundshëm.", + "site_saved": "Faqja është ruajtur.", + "logged_in": "Jeni kyçur.", + "not_logged_in": "Nuk jeni kyçur.", + "powered_by": "Fuqizuar nga", + "no": "jo", + "yes": "po", + "are_you_sure": "a je i sigurt?", + "transfer_site": "faqe transferimi", + "transfer_community": "komunitet transferimi", + "block_leaving": "A je i sigurt se do të dalësh?", + "emoji_picker": "Zgjedh Emoji", + "action": "Veprim", + "time": "Koha", + "couldnt_update_private_message": "Nuk mundëm të përditësonim mesazhin privat.", + "no_private_message_edit_allowed": "Nuk lejohet redaktimi i mesazhit privat.", + "couldnt_create_private_message": "Nuk mund të krijohej mesazhi privat.", + "system_err_login": "Gabim sistemi. Provo të shkyçesh dhe të kyçesh përsëri.", + "couldnt_update_user": "Përditësimi i përdoruesit nuk ishte i mundshëm.", + "invalid_username": "Emri virtual është invalid.", + "passwords_dont_match": "Fjalëkalimet nuk janë të njëjta.", + "password_incorrect": "Fjalëkalimi është i pasaktë.", + "couldnt_find_that_username_or_email": "Nuk mund të gjendej ky emër virtual ose email.", + "couldnt_update_site": "Faqja nuk mund të përditësohej.", + "not_an_admin": "Nuk je administrator.", + "no_slurs": "Nuk lejohen sharjet.", + "couldnt_save_post": "Postimi nuk u ruajt.", + "couldnt_update_post": "Postimi nuk mundi të përditësohej", + "couldnt_get_posts": "Nuk mund të merreshin postimet", + "couldnt_find_post": "Nuk mund të gjendeshin postimet.", + "couldnt_like_post": "Nuk mund të pëlqehej postimi.", + "post_title_too_long": "Titulli i postimit ishte shumë i gjatë.", + "couldnt_create_post": "Nuk mund të krijohej postimi.", + "no_community_edit_allowed": "Nuk të lejohet redaktimi i komunitetit.", + "couldnt_save_comment": "Ruajtja e komentit nuk ishte e mundshme.", + "landing": "Lemmy është një <1>grumbullues linqesh / alternativë e reddit-it, e hartuar që të punoj në <2>fediverse-in.<3>Është self-hostable, ka komente që përditësohen në kohë reale, dhe është shumë i vogël (<4>~80kB). Federimi në ActivityPub është i planifikuar në të ardhmen e afërt. <5>Ky është <6>një version shumë i hershëm beta, dhe shumë funksione janë të prishura ose mungojnë. <7>Jep sugjerime të reja ose raporto probleme <8>këtu.<9>Krijuar me <10>Rust, <11>Actix, <12>Inferno, <13>Typescript.", + "from": "nga", + "ethereum": "Ethereum", + "silver_sponsors": "Sponsorët e Argjendtë janë ata që dhuruan $40 për Lemmy.", + "sponsor_message": "Lemmy është softuer <1>open-source, falas, pa reklama, pagesë, apo kapital të sipërmarrjes, përgjithmonë. Donacionet tuaja mbështesin zhvillimin konstant të projektit. Falënderojmë njerëzit në vijim:", + "unban": "anuloje dëbimin", + "unban_from_site": "anuloje dëbimin nga faqja", + "banned_users": "Pëdoruesit e dëbuar", + "banned": "të dëbuar", + "sidebar": "Informatat anësore", + "sort_type": "Radhit sipas", + "new": "Të rejat", + "old": "Të vjetrat", + "week": "Javës", + "month": "Muajit", + "year": "Vitit", + "api": "API", + "inbox": "Kutia e njoftimeve", + "inbox_for": "Kutia e njoftimeve për <1>{{user}}", + "type": "Lloji", + "unread": "Të palexuara", + "message_sent": "Mesazhi u dërgua", + "search": "Kërko", + "view": "Shiko", + "logout": "Shkyçu", + "top": "Më të pëqlyerat", + "login_sign_up": "Kyçu / Regjistrohu", + "login": "Kyçu", + "sign_up": "Regjistrohu", + "hot": "Popullore", + "top_day": "Më të pëlqyerat e ditës", + "docs": "Dokumentimi", + "replies": "Përgjigjet", + "mentions": "Përmendur", + "reply_sent": "Përgjigja u dërgua", + "messages": "Mesazhet", + "password": "Fjalëkalimi", + "old_password": "Fjalëkalimi i vjetër", + "forgot_password": "harrova fjalëkalimin", + "password_change": "Ndrysho Fjalëkalimin", + "new_password": "Fjalëkalimi i ri", + "no_email_setup": "Ky server nuk e ka konfiguruar Email-in saktësisht.", + "email": "Email", + "matrix_user_id": "Përdorues i Matrix-it", + "send_notifications_to_email": "Dërgo njoftimet në Email", + "expires": "Skadon", + "language": "Gjuha", + "enable_downvotes": "Lejo votat negative", + "number_of_upvotes": "{{count}} Votë pozitive", + "number_of_upvotes_plural": "{{count}} Vota pozitive", + "number_of_downvotes": "{{count}} Votë negative", + "number_of_downvotes_plural": "{{count}} Vota negative", + "open_registration": "Hape regjistrimin", + "enable_nsfw": "Lejo përmbajtje NSFW", + "url": "URL", + "body": "Teksti", + "community": "Komuniteti", + "expand_here": "Zgjero këtu", + "subscribe_to_communities": "Abonohu në disa <1>communities.", + "verify_password": "Konfirmo Fjalëkalimin", + "optional": "E padetyrueshme", + "browser_default": "Parazgjedhur nga shfletuesi", + "downvotes_disabled": "Votat negative janë të çaktivizuara", + "upvote": "Votë pozitive", + "downvote": "Votë negative", + "registration_closed": "Regjistrimi u mbyll", + "chat": "Chat", + "recent_comments": "Komentet më të reja", + "no_results": "S'ka rezultate.", + "setup": "Konfigurimi", + "setup_admin": "Emëro një administrator të faqes", + "nsfw": "NSFW", + "theme": "Pamja", + "lemmy_instance_setup": "Konfigurimi i një instance të Lemmy-it", + "community_ban": "Jeni dëbuar nga ky komunitet.", + "site_ban": "Jeni dëbuar nga kjo faqe", + "community_already_exists": "Komuniteti ekziston më.", + "community_moderator_already_exists": "Moderatori i komunitetit tashmë ekziston.", + "community_follower_already_exists": "Ndjekësi i komunitetit tashmë ekziston.", + "community_user_already_banned": "Anëtari i komunitetit tashmë është dëbuar.", + "site_already_exists": "Faqja tashmë ekziston.", + "user_already_exists": "Përdoruesi tashmë ekziston.", + "email_already_exists": "Email-i tashmë ekziston.", + "admin_already_created": "Kërkojmë ndjesë, por tashmë është një administrator.", + "all": "Gjithçka", + "mark_all_as_read": "shëno të gjitha si të lexuara", + "overview": "Shiko në përgjithësi", + "notifications_error": "Njoftimet në desktop nuk janë të mundshme në shfletuesin tuaj. Provoni Firefox ose Chrome.", + "unread_messages": "Mesazhet e palexuara", + "reset_password_mail_sent": "Një Email është dërguar për të ndryshuar fjalëkalimin tuaj.", + "private_message_disclaimer": "Paralajmërim: Mesazhet private në Lemmy nuk janë të siguruara. Ju lutem krijoni një account në <1>Riot.im për të dërguar mesazhe të sigurta.", + "copy_suggested_title": "kopjo titullin e sugjeruar: {{title}}" +} diff --git a/ui/translations/uk.json b/ui/translations/uk.json index 0967ef424..2db580a61 100644 --- a/ui/translations/uk.json +++ b/ui/translations/uk.json @@ -1 +1,269 @@ -{} +{ + "post": "Запис", + "remove_post": "Видалити запис", + "no_posts": "Немає записів.", + "create_a_post": "Створити запис", + "create_post": "Створити запис", + "number_of_posts_0": "{{count}} запис", + "number_of_posts_1": "{{count}} запис", + "number_of_posts_2": "{{count}} записів", + "posts": "Записи", + "related_posts": "Связані записи", + "comments": "Коментарі", + "number_of_comments_0": "{{count}} комментарів", + "number_of_comments_1": "{{count}} комментар", + "number_of_comments_2": "{{count}} комментарів", + "remove_comment": "Видалити коментар", + "communities": "Спільноти", + "users": "Користувачі", + "create_a_community": "Створити спільноту", + "create_community": "Створити спільноту", + "remove_community": "Видалити спільноту", + "subscribed_to_communities": "Підписані на <1>спільноти", + "trending_communities": "<1>Спільноти в тренді", + "list_of_communities": "Список спільнот", + "community_reqs": "маленькими буквами, підкреслення і без пробілів.", + "edit": "редагувати", + "reply": "відповісти", + "cancel": "Відміна", + "unlock": "розблокувати", + "lock": "заблокувати", + "link": "посилання", + "mod": "модератор", + "mods": "модератори", + "moderates": "Модерація", + "settings": "Налаштування", + "remove_as_mod": "зняти з модераторов", + "appoint_as_mod": "назначити модератором", + "modlog": "Модлог", + "admin": "адміністратор", + "admins": "адміністратори", + "remove_as_admin": "зняти з адміністраторів", + "appoint_as_admin": "назначити адміністратором", + "remove": "прибрати", + "removed": "прибрано модератором", + "locked": "заблоковоано", + "reason": "Причина", + "mark_as_read": "позначити як прочитані", + "mark_as_unread": "позначити як непрочитані", + "delete": "видалити", + "deleted": "видалено автором", + "restore": "відновити", + "ban": "забанити", + "ban_from_site": "забанити на сайті", + "unban": "розбанити", + "unban_from_site": "розбанити на сайті", + "save": "зберегти", + "unsave": "видалити зі збережених", + "create": "створити", + "username": "Ім'я користувача", + "email_or_username": "email або ім'я користувача", + "number_of_users_0": "{{count}} користувачів", + "number_of_users_1": "{{count}} користувач", + "number_of_users_2": "{{count}} користувачів", + "number_of_subscribers_0": "{{count}} підписників", + "number_of_subscribers_1": "{{count}} підписник", + "number_of_subscribers_2": "{{count}} підписників", + "number_of_points_0": "{{count}} балів", + "number_of_points_1": "{{count}} бала", + "number_of_points_2": "{{count}} балів", + "name": "Ім'я", + "title": "Назва", + "category": "Категорія", + "subscribers": "Підписники", + "both": "Обидва", + "saved": "Збережено", + "unsubscribe": "Відписатися", + "subscribe": "Підписатися", + "subscribed": "Підписані", + "prev": "Назад", + "next": "Далі", + "sidebar": "Бокова панель", + "sort_type": "Тип сортування", + "hot": "Популярне", + "new": "Нове", + "top_day": "Найкраще за день", + "week": "Неділя", + "month": "Місяць", + "year": "Рід", + "all": "Все", + "top": "Найкраще", + "api": "API", + "inbox": "Вхідні", + "inbox_for": "Вхідня повідомлення для <1>{{user}}", + "mark_all_as_read": "позначити все як прочитане", + "type": "Тип", + "unread": "Не прочитано", + "reply_sent": "Відповідь відправлено", + "search": "Пошук", + "overview": "Переглянути", + "view": "Перегляд", + "logout": "Вийти", + "login_sign_up": "Ввійти / Реєстрація", + "login": "Ввійти", + "sign_up": "Реєстрація", + "notifications_error": "Повідомленя в браузері недоступні для Вашого браузера. Спробуйте Firefox або Chromium.", + "unread_messages": "Непрочитані повідомлення", + "password": "Пароль", + "verify_password": "Повторіть пароль", + "email": "email", + "optional": "необов'язково", + "expires": "спливає", + "url": "URL", + "body": "Тіло", + "copy_suggested_title": "запропонована назва: {{title}}", + "community": "Спільнота", + "expand_here": "Розширити тут", + "subscribe_to_communities": "Підпишіться на деякі <1>спільноти.", + "chat": "Чат", + "no_results": "Немає результатів.", + "setup": "Встановлення", + "lemmy_instance_setup": "Встановлення інстансу Lemmy", + "setup_admin": "Налаштування адміністратора сайту", + "your_site": "ваш сайт", + "modified": "змінено", + "nsfw": "NSFW", + "show_nsfw": "Показувати NSFW-контент", + "sponsors": "Спонсори", + "sponsors_of_lemmy": "Спонсори Lemmy", + "sponsor_message": "Lemmy це безкоштовний, <1>відкритий софт, без реклами, монетизації чи венчурного капіталу. Назавжди. Ваші пожертви йдуть напряму на розвиток проетку. Дякую нищевказаним людям:", + "support_on_patreon": "Підтримати на Patreon", + "general_sponsors": "Генеральні спонсори - це ті, хто задонатив Lemmy від $10 до $39.", + "crypto": "Крипта", + "bitcoin": "Bitcoin", + "ethereum": "Ethereum", + "code": "Код", + "joined": "Приєдналися", + "powered_by": "Працює на", + "landing_0": "Lemmy - це <1>агрегатор посилань / альтернатива reddit, призначений для роботи в <2>федіверсі.<3>Це самодостаттня система, з обновлюваним коментарями, і це дуже маленька система (<4>~80 Кб). Федерація в мережі ActivityPub знаходиться в розробці. <5>Це <6>дуже рання бета-версія, і багато функцій відсутні або поломані. <7>Пропонувати нову функції або повідомляти про баги можна <8>тут.<9>Зроблено на <10>Rust, <11>Actix, <12>Inferno, <13>Typescript.", + "not_logged_in": "Не авторизовані.", + "community_ban": "Ви були заблоковані в цій спільноті.", + "site_ban": "Ви були заблоковані на данному сайті", + "couldnt_create_comment": "Не вдалося створити коментар.", + "couldnt_like_comment": "Не вдалося лайкнути коментар.", + "couldnt_update_comment": "Не вдалося обновити коментар.", + "couldnt_save_comment": "Не вдалося зберегти коментар.", + "no_comment_edit_allowed": "Неможливо відредагувати коментар.", + "no_post_edit_allowed": "Неможливо відредагувати запис.", + "no_community_edit_allowed": "Неможливо відредагувати спільноту.", + "couldnt_find_community": "Не вдалося знайти спільноту.", + "couldnt_update_community": "Не вдалося обновити спільноту.", + "community_already_exists": "Спільнота вже існує.", + "community_moderator_already_exists": "Модератор спільноти вже існує.", + "community_follower_already_exists": "Підписник спільноти вже існує.", + "community_user_already_banned": "Член спільноти вже забаниний..", + "couldnt_create_post": "Не вдалося створити запис.", + "couldnt_like_post": "Не вдалося лайкнути запис.", + "couldnt_find_post": "Не вдалося знайти запис.", + "couldnt_get_posts": "Не вдалося знайти записи", + "couldnt_update_post": "Не вдалося обновити запис", + "couldnt_save_post": "Не вдалося зберегти запис.", + "no_slurs": "Без образ.", + "not_an_admin": "Не адміністратор.", + "site_already_exists": "Сайт вже існує.", + "couldnt_update_site": "Не вдалося оновити сайт.", + "couldnt_find_that_username_or_email": "Не вдалося знайти ім'я користувача чи email.", + "password_incorrect": "Неправильний пароль.", + "passwords_dont_match": "Паролі не співпадають.", + "admin_already_created": "Пробачте, вже є адміністратор.", + "user_already_exists": "Користувач вже існує.", + "couldnt_update_user": "Не вдалося оновити користувача.", + "system_err_login": "Системна помилка. Спробуйте вийти та зайти назад.", + "create_private_message": "Створити приватне повідомлення", + "send_secure_message": "Послати зашифроване повідомлення", + "send_message": "Послати повідомлення", + "message": "Повідомлення", + "avatar": "Аватар", + "show_avatars": "Показувати аватари", + "formatting_help": "Допомога у верстанні тексту", + "sticky": "запкріпити", + "stickied": "закріплений", + "delete_account": "Видалити акаунт", + "delete_account_confirm": "Попередження: ця дія повністю знищить всі данні вашего акаунта. Введіть свій пароль для підтвердження.", + "docs": "Документація", + "replies": "Відповіді", + "mentions": "Згадування", + "message_sent": "Повідомлення відправлено", + "old_password": "Діючий пароль", + "forgot_password": "я забув(ла) пароль", + "reset_password_mail_sent": "Лист для відновлення пароля було надіслано.", + "private_message_disclaimer": "Повідомлення: Приватні повідомлення Lemmy на данний момент не зашифровані. Для безпечної комунікації створіть акаунт на <1>Riot.im.", + "send_notifications_to_email": "Посилати повідомлення на e-mail адресу", + "language": "Мова", + "browser_default": "Браузер по замовчуванню", + "open_registration": "Відрита реєстрація", + "registration_closed": "Реєстрацію закрито", + "recent_comments": "Недавні коментарі", + "cross_posts": "Це посилання було опубліковано в таких спільнотах:", + "cross_post": "Опубліковано в інших спільнотах", + "cross_posted_to": "Також опубліковано в: ", + "support_on_liberapay": "Підтримати на Librepay", + "donate_to_lemmy": "Підтримати Lemmy", + "transfer_community": "передати спільноту", + "yes": "так", + "no": "ні", + "preview": "Попередній перегляд", + "upload_image": "завантажити зображення", + "upload_avatar": "Завантажити аватар", + "messages": "Повідомлення", + "new_password": "Новий пароль", + "theme": "Візуальна тема", + "post_title_too_long": "Довжина назви перебільшує допустимий ліміт.", + "time": "Час", + "action": "Дія", + "view_source": "сирцевий код", + "more": "більше", + "sorting_help": "допомога по сортуванню", + "by": "від", + "number_of_communities_0": "{{count}} спільнот", + "number_of_communities_1": "{{count}} спільнот", + "number_of_communities_2": "{{count}} спільнот", + "creator": "автор", + "old": "Старе", + "to": "в", + "admin_settings": "Налаштування адміна", + "banned_users": "Забанані користувачі", + "support_on_open_collective": "Піддтримка на OpenCollective", + "site_saved": "Сайт збережено.", + "enable_nsfw": "Ввімкнути NSFW", + "donate": "Пожертвувати", + "unsticky": "відклеїти", + "site_config": "Конфігурація сайта", + "banned": "забанений", + "password_change": "Зміна паролю", + "no_email_setup": "На цьому сервері неправильно налаштовано email.", + "matrix_user_id": "Matrix айді користувача", + "are_you_sure": "ви впевненні?", + "archive_link": "активувати посилання", + "logged_in": "Ввійти в систему.", + "couldnt_get_comments": "Не вдалося отримати коментар.", + "from": "від", + "transfer_site": "трансфер сайту", + "show_context": "Показати контекст", + "email_already_exists": "E-mail вже існує.", + "couldnt_create_private_message": "Не вдалося отримати особисте повідомлення.", + "no_private_message_edit_allowed": "Не можна редагувати особисті повідомлення.", + "couldnt_update_private_message": "Не вдалося оновити особисте повідомлення.", + "block_leaving": "Ви впевненні що хочете покинути?", + "number_online_0": "{{count}} Користувачів онлайн", + "number_online_1": "{{count}} Користувач онлайн", + "number_online_2": "{{count}} Користувачів онлайн", + "invalid_community_name": "Неправильне ім'я користувача.", + "picture_deleted": "Зображення видалені.", + "click_to_delete_picture": "Натисніть, щоб видалити зображення.", + "downvotes_disabled": "Від'ємне голосування вимкненно.", + "upvote": "Голосувати за", + "enable_downvotes": "Ввімкнути від'ємне голосування", + "downvote": "Голосувати проти", + "number_of_upvotes_0": "{{count}} голосів за", + "number_of_upvotes_1": "{{count}} голос за", + "number_of_upvotes_2": "{{count}} голосів за", + "number_of_downvotes_0": "{{count}} голосів проти", + "number_of_downvotes_1": "{{count}} голос проти", + "number_of_downvotes_2": "{{count}} голосів проти", + "silver_sponsors": "Срібні спонсори - це ті, хто пожертував $40 для Lemmy.", + "monero": "Monero", + "emoji_picker": "Обрати емодзі", + "select_a_community": "Обрати спільноту", + "invalid_username": "Неправильне ім'я користувача." +} diff --git a/ui/translations/zh.json b/ui/translations/zh.json index def3b05cb..ddf402226 100644 --- a/ui/translations/zh.json +++ b/ui/translations/zh.json @@ -24,29 +24,29 @@ "unlock": "解锁", "lock": "加锁", "link": "链接", - "mod": "监管人", - "mods": "监管人", + "mod": "社群管理", + "mods": "社群管理", "moderates": "监管", - "remove_as_mod": "添加监管人", - "appoint_as_mod": "移除监管人", - "modlog": "监管记录", - "admin": "管理权限", - "admins": "管理权限", - "remove_as_admin": "移除管理权限", - "appoint_as_admin": "添加管理权限", + "remove_as_mod": "删除社群管理", + "appoint_as_mod": "任命为社群管理", + "modlog": "审核记录", + "admin": "总管理员", + "admins": "总管理员", + "remove_as_admin": "移除管理员权限", + "appoint_as_admin": "添加管理员权限", "remove": "移除", - "removed": "已被管理员移除", - "locked": "已加锁", + "removed": "已被社群管理移除", + "locked": "已锁定", "reason": "原因", - "mark_as_read": "标记未读", - "mark_as_unread": "标记已读", + "mark_as_read": "标记为已读", + "mark_as_unread": "标记为未读", "delete": "删除", "deleted": "作者已删除", "restore": "恢复", "ban": "禁止", "ban_from_site": "禁止此站点", - "unban": "取消", - "unban_from_site": "取消禁止", + "unban": "取消禁止", + "unban_from_site": "取消禁止网站", "save": "保存", "unsave": "取消保存", "create": "创建", @@ -54,7 +54,7 @@ "email_or_username": "邮箱或用户名", "number_of_users": "{{count}} 名用户", "number_of_subscribers": "{{count}} 名订阅者", - "number_of_points": "{{count}} 点数", + "number_of_points": "{{count}} 积分", "name": "名字", "title": "标题", "category": "分类", @@ -70,13 +70,13 @@ "sort_type": "排序方式", "hot": "最热", "new": "最新", - "top_day": "今日", + "top_day": "每日推荐", "week": "周", "month": "月", "year": "年", "all": "所有", - "top": "最热", - "api": "应用程式介面", + "top": "推荐", + "api": "API", "inbox": "收件箱", "inbox_for": "<1>{{user}} 收件箱", "mark_all_as_read": "标记所有已读", @@ -97,7 +97,7 @@ "email": "邮箱", "optional": "选项", "expires": "过期", - "url": "网址", + "url": "相关网址", "body": "内容", "copy_suggested_title": "复制建议的标题: {{title}}", "community": "社群", @@ -160,7 +160,7 @@ "show_nsfw": "显示工作场所不宜内容", "theme": "主题", "from": "由", - "donate_to_lemmy": "向Lemmy捐赠", + "donate_to_lemmy": "向 Lemmy 捐赠", "donate": "捐赠", "monero": "门罗币", "to": "发布到", @@ -185,21 +185,21 @@ "unsticky": "取消固定", "archive_link": "链接归档", "settings": "设定", - "stickied": "已固定", + "stickied": "已置顶", "delete_account": "删除账号", - "delete_account_confirm": "警告:此操作将永久删除你的数据。输入密码进行确认。", + "delete_account_confirm": "警告!此操作将永久删除你的数据,请输入密码进行确认。", "cross_posts": "此链接也已被发布到:", "cross_post": "复数发布", "cross_posted_to": "已被复数发布到: ", "users": "用户", "banned": "已被禁止", - "creator": "发布者", + "creator": "作者", "number_of_communities": "{{count}} 个社群", - "old": "最旧", + "old": "最早", "docs": "文档", "upload_avatar": "上传头像", "replies": "回复", - "number_online": "{{count}}名在线用户", + "number_online": "{{count}} 名在线用户", "mentions": "提到", "message_sent": "已发送信息", "old_password": "当前密码", @@ -214,12 +214,12 @@ "send_notifications_to_email": "向邮箱发送通知", "language": "语言", "browser_default": "浏览器默认语言", - "downvotes_disabled": "点踩功能已禁用", - "enable_downvotes": "启用点踩功能", + "downvotes_disabled": "反对投票已禁用", + "enable_downvotes": "启用反对投票功能", "upvote": "点赞", "number_of_upvotes": "{{count}} 个赞", - "downvote": "点踩", - "number_of_downvotes": "{{count}} 个踩", + "downvote": "反对", + "number_of_downvotes": "{{count}} 个反对", "open_registration": "开放注册", "registration_closed": "注册功能已关闭", "recent_comments": "最新评论", @@ -241,5 +241,9 @@ "banned_users": "被禁止用户", "site_saved": "网站已保存", "emoji_picker": "选择表情", - "invalid_username": "用户名无效" + "invalid_username": "用户名无效", + "invalid_community_name": "无效的名称", + "click_to_delete_picture": "点击删除图片", + "picture_deleted": "图片已删除", + "select_a_community": "选择一个社群" }