mirror of
https://github.com/LemmyNet/lemmy.git
synced 2024-10-01 01:36:12 -04:00
Merge branch 'master' into arrudaricardo-issue-#814
This commit is contained in:
commit
b1b755713e
2
ansible/VERSION
vendored
2
ansible/VERSION
vendored
@ -1 +1 @@
|
||||
v0.7.5
|
||||
v0.7.8
|
||||
|
4
ansible/ansible.cfg
vendored
4
ansible/ansible.cfg
vendored
@ -1,6 +1,6 @@
|
||||
[defaults]
|
||||
inventory=inventory
|
||||
interpreter_python=/usr/bin/python3
|
||||
inventory = inventory
|
||||
interpreter_python = /usr/bin/python3
|
||||
|
||||
[ssh_connection]
|
||||
pipelining = True
|
||||
|
10
ansible/inventory.example
vendored
10
ansible/inventory.example
vendored
@ -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
|
||||
|
78
ansible/lemmy.yml
vendored
78
ansible/lemmy.yml
vendored
@ -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|version_compare('20.04', '<')
|
||||
|
||||
- name: install certbot-nginx on ubuntu > 20
|
||||
apt:
|
||||
pkg:
|
||||
- 'python3-certbot-nginx'
|
||||
when: ansible_distribution == 'Ubuntu' and ansible_distribution_version|version_compare('20.04', '>=')
|
||||
|
||||
- name: request initial letsencrypt certificate
|
||||
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'"
|
||||
|
86
ansible/lemmy_dev.yml
vendored
86
ansible/lemmy_dev.yml
vendored
@ -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'"
|
||||
|
2
ansible/templates/docker-compose.yml
vendored
2
ansible/templates/docker-compose.yml
vendored
@ -35,7 +35,7 @@ services:
|
||||
restart: always
|
||||
|
||||
iframely:
|
||||
image: jolt/iframely:v1.4.3
|
||||
image: dogbin/iframely:latest
|
||||
ports:
|
||||
- "127.0.0.1:8061:80"
|
||||
volumes:
|
||||
|
26
ansible/uninstall.yml
vendored
26
ansible/uninstall.yml
vendored
@ -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
|
||||
|
||||
|
4
docker/dev/docker-compose.yml
vendored
4
docker/dev/docker-compose.yml
vendored
@ -20,6 +20,8 @@ services:
|
||||
|
||||
postgres:
|
||||
image: postgres:12-alpine
|
||||
ports:
|
||||
- "127.0.0.1:5432:5432"
|
||||
environment:
|
||||
- POSTGRES_USER=lemmy
|
||||
- POSTGRES_PASSWORD=password
|
||||
@ -38,7 +40,7 @@ services:
|
||||
restart: always
|
||||
|
||||
iframely:
|
||||
image: jolt/iframely:v1.4.3
|
||||
image: dogbin/iframely:latest
|
||||
ports:
|
||||
- "127.0.0.1:8061:80"
|
||||
volumes:
|
||||
|
14
docker/federation-test/run-tests.sh
vendored
14
docker/federation-test/run-tests.sh
vendored
@ -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
|
||||
|
19
docker/federation-test/servers.sh
vendored
Executable file
19
docker/federation-test/servers.sh
vendored
Executable file
@ -0,0 +1,19 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
sudo rm -rf volumes
|
||||
|
||||
pushd ../../server/
|
||||
cargo build
|
||||
popd
|
||||
|
||||
pushd ../../ui
|
||||
yarn
|
||||
popd
|
||||
|
||||
mkdir -p volumes/pictrs_{alpha,beta,gamma}
|
||||
sudo chown -R 991:991 volumes/pictrs_{alpha,beta,gamma}
|
||||
|
||||
sudo docker build ../../ --file ../federation/Dockerfile --tag lemmy-federation:latest
|
||||
|
||||
sudo docker-compose --file ../federation/docker-compose.yml --project-directory . up
|
10
docker/federation-test/tests.sh
vendored
Executable file
10
docker/federation-test/tests.sh
vendored
Executable file
@ -0,0 +1,10 @@
|
||||
#!/bin/bash
|
||||
set -xe
|
||||
|
||||
pushd ../../ui
|
||||
echo "Waiting for Lemmy to start..."
|
||||
while [[ "$(curl -s -o /dev/null -w '%{http_code}' 'localhost:8540/api/v1/site')" != "200" ]]; do sleep 1; done
|
||||
while [[ "$(curl -s -o /dev/null -w '%{http_code}' 'localhost:8550/api/v1/site')" != "200" ]]; do sleep 1; done
|
||||
while [[ "$(curl -s -o /dev/null -w '%{http_code}' 'localhost:8560/api/v1/site')" != "200" ]]; do sleep 1; done
|
||||
yarn api-test || true
|
||||
popd
|
2
docker/federation/Dockerfile
vendored
2
docker/federation/Dockerfile
vendored
@ -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
|
||||
|
58
docker/federation/docker-compose.yml
vendored
58
docker/federation/docker-compose.yml
vendored
@ -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,13 +105,8 @@ 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: jolt/iframely:v1.4.3
|
||||
image: dogbin/iframely:latest
|
||||
volumes:
|
||||
- ../iframely.config.local.js:/iframely/config.local.js:ro
|
||||
|
12
docker/federation/nginx.conf
vendored
12
docker/federation/nginx.conf
vendored
@ -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;
|
||||
|
4
docker/prod/docker-compose.yml
vendored
4
docker/prod/docker-compose.yml
vendored
@ -12,7 +12,7 @@ services:
|
||||
restart: always
|
||||
|
||||
lemmy:
|
||||
image: dessalines/lemmy:v0.7.5
|
||||
image: dessalines/lemmy:v0.7.8
|
||||
ports:
|
||||
- "127.0.0.1:8536:8536"
|
||||
restart: always
|
||||
@ -35,7 +35,7 @@ services:
|
||||
restart: always
|
||||
|
||||
iframely:
|
||||
image: jolt/iframely:v1.4.3
|
||||
image: dogbin/iframely:latest
|
||||
ports:
|
||||
- "127.0.0.1:8061:80"
|
||||
volumes:
|
||||
|
44
docs/src/contributing_federation_development.md
vendored
44
docs/src/contributing_federation_development.md
vendored
@ -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/
|
||||
|
569
server/Cargo.lock
generated
vendored
569
server/Cargo.lock
generated
vendored
File diff suppressed because it is too large
Load Diff
15
server/Cargo.toml
vendored
15
server/Cargo.toml
vendored
@ -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"
|
||||
|
@ -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<CommentView>,
|
||||
}
|
||||
|
||||
#[async_trait::async_trait(?Send)]
|
||||
impl Perform for Oper<CreateComment> {
|
||||
type Response = CommentResponse;
|
||||
|
||||
fn perform(
|
||||
async fn perform(
|
||||
&self,
|
||||
pool: Pool<ConnectionManager<PgConnection>>,
|
||||
pool: &DbPool,
|
||||
websocket_info: Option<WebsocketInfo>,
|
||||
) -> Result<CommentResponse, Error> {
|
||||
) -> Result<CommentResponse, LemmyError> {
|
||||
let data: &CreateComment = &self.data;
|
||||
|
||||
let claims = match Claims::decode(&data.auth) {
|
||||
@ -114,20 +113,6 @@ impl Perform for Oper<CreateComment> {
|
||||
|
||||
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<CreateComment> {
|
||||
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<CreateComment> {
|
||||
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<CreateComment> {
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait(?Send)]
|
||||
impl Perform for Oper<EditComment> {
|
||||
type Response = CommentResponse;
|
||||
|
||||
fn perform(
|
||||
async fn perform(
|
||||
&self,
|
||||
pool: Pool<ConnectionManager<PgConnection>>,
|
||||
pool: &DbPool,
|
||||
websocket_info: Option<WebsocketInfo>,
|
||||
) -> Result<CommentResponse, Error> {
|
||||
) -> Result<CommentResponse, LemmyError> {
|
||||
let data: &EditComment = &self.data;
|
||||
|
||||
let claims = match Claims::decode(&data.auth) {
|
||||
@ -215,30 +231,44 @@ impl Perform for Oper<EditComment> {
|
||||
|
||||
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<i32> = 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<EditComment> {
|
||||
|
||||
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<EditComment> {
|
||||
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<EditComment> {
|
||||
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<EditComment> {
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait(?Send)]
|
||||
impl Perform for Oper<SaveComment> {
|
||||
type Response = CommentResponse;
|
||||
|
||||
fn perform(
|
||||
async fn perform(
|
||||
&self,
|
||||
pool: Pool<ConnectionManager<PgConnection>>,
|
||||
pool: &DbPool,
|
||||
_websocket_info: Option<WebsocketInfo>,
|
||||
) -> Result<CommentResponse, Error> {
|
||||
) -> Result<CommentResponse, LemmyError> {
|
||||
let data: &SaveComment = &self.data;
|
||||
|
||||
let claims = match Claims::decode(&data.auth) {
|
||||
@ -352,21 +405,23 @@ impl Perform for Oper<SaveComment> {
|
||||
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<SaveComment> {
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait(?Send)]
|
||||
impl Perform for Oper<CreateCommentLike> {
|
||||
type Response = CommentResponse;
|
||||
|
||||
fn perform(
|
||||
async fn perform(
|
||||
&self,
|
||||
pool: Pool<ConnectionManager<PgConnection>>,
|
||||
pool: &DbPool,
|
||||
websocket_info: Option<WebsocketInfo>,
|
||||
) -> Result<CommentResponse, Error> {
|
||||
) -> Result<CommentResponse, LemmyError> {
|
||||
let data: &CreateCommentLike = &self.data;
|
||||
|
||||
let claims = match Claims::decode(&data.auth) {
|
||||
@ -394,36 +450,42 @@ impl Perform for Oper<CreateCommentLike> {
|
||||
|
||||
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<CreateCommentLike> {
|
||||
};
|
||||
|
||||
// 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<CreateCommentLike> {
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait(?Send)]
|
||||
impl Perform for Oper<GetComments> {
|
||||
type Response = GetCommentsResponse;
|
||||
|
||||
fn perform(
|
||||
async fn perform(
|
||||
&self,
|
||||
pool: Pool<ConnectionManager<PgConnection>>,
|
||||
pool: &DbPool,
|
||||
websocket_info: Option<WebsocketInfo>,
|
||||
) -> Result<GetCommentsResponse, Error> {
|
||||
) -> Result<GetCommentsResponse, LemmyError> {
|
||||
let data: &GetComments = &self.data;
|
||||
|
||||
let user_claims: Option<Claims> = match &data.auth {
|
||||
@ -509,19 +578,23 @@ impl Perform for Oper<GetComments> {
|
||||
let type_ = ListingType::from_str(&data.type_)?;
|
||||
let 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<GetComments> {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn send_local_notifs(
|
||||
conn: &PgConnection,
|
||||
pub async fn send_local_notifs(
|
||||
mentions: Vec<MentionData>,
|
||||
comment: Comment,
|
||||
user: User_,
|
||||
post: Post,
|
||||
pool: &DbPool,
|
||||
) -> Result<Vec<i32>, LemmyError> {
|
||||
let ids = blocking(pool, move |conn| {
|
||||
do_send_local_notifs(conn, &mentions, &comment, &user, &post)
|
||||
})
|
||||
.await?;
|
||||
|
||||
Ok(ids)
|
||||
}
|
||||
|
||||
fn do_send_local_notifs(
|
||||
conn: &diesel::PgConnection,
|
||||
mentions: &[MentionData],
|
||||
comment: &Comment,
|
||||
user: &User_,
|
||||
|
@ -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<GetCommunity> {
|
||||
type Response = GetCommunityResponse;
|
||||
|
||||
fn perform(
|
||||
async fn perform(
|
||||
&self,
|
||||
pool: Pool<ConnectionManager<PgConnection>>,
|
||||
pool: &DbPool,
|
||||
websocket_info: Option<WebsocketInfo>,
|
||||
) -> Result<GetCommunityResponse, Error> {
|
||||
) -> Result<GetCommunityResponse, LemmyError> {
|
||||
let data: &GetCommunity = &self.data;
|
||||
|
||||
let user_id: Option<i32> = match &data.auth {
|
||||
@ -159,33 +158,38 @@ impl Perform for Oper<GetCommunity> {
|
||||
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<CommunityModeratorView> = 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<GetCommunity> {
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait(?Send)]
|
||||
impl Perform for Oper<CreateCommunity> {
|
||||
type Response = CommunityResponse;
|
||||
|
||||
fn perform(
|
||||
async fn perform(
|
||||
&self,
|
||||
pool: Pool<ConnectionManager<PgConnection>>,
|
||||
pool: &DbPool,
|
||||
_websocket_info: Option<WebsocketInfo>,
|
||||
) -> Result<CommunityResponse, Error> {
|
||||
) -> Result<CommunityResponse, LemmyError> {
|
||||
let data: &CreateCommunity = &self.data;
|
||||
|
||||
let claims = match Claims::decode(&data.auth) {
|
||||
@ -255,10 +260,9 @@ impl Perform for Oper<CreateCommunity> {
|
||||
|
||||
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<CreateCommunity> {
|
||||
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<CreateCommunity> {
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait(?Send)]
|
||||
impl Perform for Oper<EditCommunity> {
|
||||
type Response = CommunityResponse;
|
||||
|
||||
fn perform(
|
||||
async fn perform(
|
||||
&self,
|
||||
pool: Pool<ConnectionManager<PgConnection>>,
|
||||
pool: &DbPool,
|
||||
websocket_info: Option<WebsocketInfo>,
|
||||
) -> Result<CommunityResponse, Error> {
|
||||
) -> Result<CommunityResponse, LemmyError> {
|
||||
let data: &EditCommunity = &self.data;
|
||||
|
||||
if let Err(slurs) = slur_check(&data.name) {
|
||||
@ -353,28 +360,34 @@ impl Perform for Oper<EditCommunity> {
|
||||
|
||||
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<i32> = 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<EditCommunity> {
|
||||
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<EditCommunity> {
|
||||
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<EditCommunity> {
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait(?Send)]
|
||||
impl Perform for Oper<ListCommunities> {
|
||||
type Response = ListCommunitiesResponse;
|
||||
|
||||
fn perform(
|
||||
async fn perform(
|
||||
&self,
|
||||
pool: Pool<ConnectionManager<PgConnection>>,
|
||||
pool: &DbPool,
|
||||
_websocket_info: Option<WebsocketInfo>,
|
||||
) -> Result<ListCommunitiesResponse, Error> {
|
||||
) -> Result<ListCommunitiesResponse, LemmyError> {
|
||||
let data: &ListCommunities = &self.data;
|
||||
|
||||
let user_claims: Option<Claims> = match &data.auth {
|
||||
@ -483,29 +514,33 @@ impl Perform for Oper<ListCommunities> {
|
||||
|
||||
let sort = SortType::from_str(&data.sort)?;
|
||||
|
||||
let 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<FollowCommunity> {
|
||||
type Response = CommunityResponse;
|
||||
|
||||
fn perform(
|
||||
async fn perform(
|
||||
&self,
|
||||
pool: Pool<ConnectionManager<PgConnection>>,
|
||||
pool: &DbPool,
|
||||
_websocket_info: Option<WebsocketInfo>,
|
||||
) -> Result<CommunityResponse, Error> {
|
||||
) -> Result<CommunityResponse, LemmyError> {
|
||||
let data: &FollowCommunity = &self.data;
|
||||
|
||||
let claims = match Claims::decode(&data.auth) {
|
||||
@ -515,9 +550,8 @@ impl Perform for Oper<FollowCommunity> {
|
||||
|
||||
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<FollowCommunity> {
|
||||
|
||||
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<FollowCommunity> {
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait(?Send)]
|
||||
impl Perform for Oper<GetFollowedCommunities> {
|
||||
type Response = GetFollowedCommunitiesResponse;
|
||||
|
||||
fn perform(
|
||||
async fn perform(
|
||||
&self,
|
||||
pool: Pool<ConnectionManager<PgConnection>>,
|
||||
pool: &DbPool,
|
||||
_websocket_info: Option<WebsocketInfo>,
|
||||
) -> Result<GetFollowedCommunitiesResponse, Error> {
|
||||
) -> Result<GetFollowedCommunitiesResponse, LemmyError> {
|
||||
let data: &GetFollowedCommunities = &self.data;
|
||||
|
||||
let claims = match Claims::decode(&data.auth) {
|
||||
@ -577,27 +622,29 @@ impl Perform for Oper<GetFollowedCommunities> {
|
||||
|
||||
let user_id = claims.id;
|
||||
|
||||
let conn = pool.get()?;
|
||||
|
||||
let communities: Vec<CommunityFollowerView> =
|
||||
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<BanFromCommunity> {
|
||||
type Response = BanFromCommunityResponse;
|
||||
|
||||
fn perform(
|
||||
async fn perform(
|
||||
&self,
|
||||
pool: Pool<ConnectionManager<PgConnection>>,
|
||||
pool: &DbPool,
|
||||
websocket_info: Option<WebsocketInfo>,
|
||||
) -> Result<BanFromCommunityResponse, Error> {
|
||||
) -> Result<BanFromCommunityResponse, LemmyError> {
|
||||
let data: &BanFromCommunity = &self.data;
|
||||
|
||||
let claims = match Claims::decode(&data.auth) {
|
||||
@ -612,18 +659,16 @@ impl Perform for Oper<BanFromCommunity> {
|
||||
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<BanFromCommunity> {
|
||||
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<BanFromCommunity> {
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait(?Send)]
|
||||
impl Perform for Oper<AddModToCommunity> {
|
||||
type Response = AddModToCommunityResponse;
|
||||
|
||||
fn perform(
|
||||
async fn perform(
|
||||
&self,
|
||||
pool: Pool<ConnectionManager<PgConnection>>,
|
||||
pool: &DbPool,
|
||||
websocket_info: Option<WebsocketInfo>,
|
||||
) -> Result<AddModToCommunityResponse, Error> {
|
||||
) -> Result<AddModToCommunityResponse, LemmyError> {
|
||||
let data: &AddModToCommunity = &self.data;
|
||||
|
||||
let claims = match Claims::decode(&data.auth) {
|
||||
@ -684,18 +731,16 @@ impl Perform for Oper<AddModToCommunity> {
|
||||
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<AddModToCommunity> {
|
||||
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<AddModToCommunity> {
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait(?Send)]
|
||||
impl Perform for Oper<TransferCommunity> {
|
||||
type Response = GetCommunityResponse;
|
||||
|
||||
fn perform(
|
||||
async fn perform(
|
||||
&self,
|
||||
pool: Pool<ConnectionManager<PgConnection>>,
|
||||
pool: &DbPool,
|
||||
_websocket_info: Option<WebsocketInfo>,
|
||||
) -> Result<GetCommunityResponse, Error> {
|
||||
) -> Result<GetCommunityResponse, LemmyError> {
|
||||
let data: &TransferCommunity = &self.data;
|
||||
|
||||
let claims = match Claims::decode(&data.auth) {
|
||||
@ -741,12 +791,14 @@ impl Perform for Oper<TransferCommunity> {
|
||||
|
||||
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<TransferCommunity> {
|
||||
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<TransferCommunity> {
|
||||
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<TransferCommunity> {
|
||||
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()),
|
||||
};
|
||||
|
@ -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<T> {
|
||||
data: T,
|
||||
client: Client,
|
||||
}
|
||||
|
||||
impl<Data> Oper<Data> {
|
||||
pub fn new(data: Data) -> Oper<Data> {
|
||||
Oper { data }
|
||||
pub fn new(data: Data, client: Client) -> Oper<Data> {
|
||||
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<ConnectionManager<PgConnection>>,
|
||||
pool: &DbPool,
|
||||
websocket_info: Option<WebsocketInfo>,
|
||||
) -> Result<Self::Response, Error>;
|
||||
) -> Result<Self::Response, LemmyError>;
|
||||
}
|
||||
|
@ -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<CreatePost> {
|
||||
type Response = PostResponse;
|
||||
|
||||
fn perform(
|
||||
async fn perform(
|
||||
&self,
|
||||
pool: Pool<ConnectionManager<PgConnection>>,
|
||||
pool: &DbPool,
|
||||
websocket_info: Option<WebsocketInfo>,
|
||||
) -> Result<PostResponse, Error> {
|
||||
) -> Result<PostResponse, LemmyError> {
|
||||
let data: &CreatePost = &self.data;
|
||||
|
||||
let claims = match Claims::decode(&data.auth) {
|
||||
@ -139,22 +138,23 @@ impl Perform for Oper<CreatePost> {
|
||||
|
||||
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<CreatePost> {
|
||||
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<CreatePost> {
|
||||
}
|
||||
};
|
||||
|
||||
let updated_post = match Post::update_ap_id(&conn, inserted_post.id) {
|
||||
Ok(post) => post,
|
||||
Err(_e) => return Err(APIError::err("couldnt_create_post").into()),
|
||||
};
|
||||
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<CreatePost> {
|
||||
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<CreatePost> {
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait(?Send)]
|
||||
impl Perform for Oper<GetPost> {
|
||||
type Response = GetPostResponse;
|
||||
|
||||
fn perform(
|
||||
async fn perform(
|
||||
&self,
|
||||
pool: Pool<ConnectionManager<PgConnection>>,
|
||||
pool: &DbPool,
|
||||
websocket_info: Option<WebsocketInfo>,
|
||||
) -> Result<GetPostResponse, Error> {
|
||||
) -> Result<GetPostResponse, LemmyError> {
|
||||
let data: &GetPost = &self.data;
|
||||
|
||||
let user_id: Option<i32> = match &data.auth {
|
||||
@ -252,25 +260,38 @@ impl Perform for Oper<GetPost> {
|
||||
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<GetPost> {
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait(?Send)]
|
||||
impl Perform for Oper<GetPosts> {
|
||||
type Response = GetPostsResponse;
|
||||
|
||||
fn perform(
|
||||
async fn perform(
|
||||
&self,
|
||||
pool: Pool<ConnectionManager<PgConnection>>,
|
||||
pool: &DbPool,
|
||||
websocket_info: Option<WebsocketInfo>,
|
||||
) -> Result<GetPostsResponse, Error> {
|
||||
) -> Result<GetPostsResponse, LemmyError> {
|
||||
let data: &GetPosts = &self.data;
|
||||
|
||||
let user_claims: Option<Claims> = match &data.auth {
|
||||
@ -336,17 +358,21 @@ impl Perform for Oper<GetPosts> {
|
||||
let type_ = ListingType::from_str(&data.type_)?;
|
||||
let 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<GetPosts> {
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait(?Send)]
|
||||
impl Perform for Oper<CreatePostLike> {
|
||||
type Response = PostResponse;
|
||||
|
||||
fn perform(
|
||||
async fn perform(
|
||||
&self,
|
||||
pool: Pool<ConnectionManager<PgConnection>>,
|
||||
pool: &DbPool,
|
||||
websocket_info: Option<WebsocketInfo>,
|
||||
) -> Result<PostResponse, Error> {
|
||||
) -> Result<PostResponse, LemmyError> {
|
||||
let data: &CreatePostLike = &self.data;
|
||||
|
||||
let claims = match Claims::decode(&data.auth) {
|
||||
@ -387,24 +414,27 @@ impl Perform for Oper<CreatePostLike> {
|
||||
|
||||
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<CreatePostLike> {
|
||||
};
|
||||
|
||||
// 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<CreatePostLike> {
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait(?Send)]
|
||||
impl Perform for Oper<EditPost> {
|
||||
type Response = PostResponse;
|
||||
|
||||
fn perform(
|
||||
async fn perform(
|
||||
&self,
|
||||
pool: Pool<ConnectionManager<PgConnection>>,
|
||||
pool: &DbPool,
|
||||
websocket_info: Option<WebsocketInfo>,
|
||||
) -> Result<PostResponse, Error> {
|
||||
) -> Result<PostResponse, LemmyError> {
|
||||
let data: &EditPost = &self.data;
|
||||
|
||||
if let Err(slurs) = slur_check(&data.name) {
|
||||
@ -481,37 +519,46 @@ impl Perform for Oper<EditPost> {
|
||||
|
||||
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<i32> = 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<EditPost> {
|
||||
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<EditPost> {
|
||||
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<EditPost> {
|
||||
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<EditPost> {
|
||||
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<EditPost> {
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait(?Send)]
|
||||
impl Perform for Oper<SavePost> {
|
||||
type Response = PostResponse;
|
||||
|
||||
fn perform(
|
||||
async fn perform(
|
||||
&self,
|
||||
pool: Pool<ConnectionManager<PgConnection>>,
|
||||
pool: &DbPool,
|
||||
_websocket_info: Option<WebsocketInfo>,
|
||||
) -> Result<PostResponse, Error> {
|
||||
) -> Result<PostResponse, LemmyError> {
|
||||
let data: &SavePost = &self.data;
|
||||
|
||||
let claims = match Claims::decode(&data.auth) {
|
||||
@ -630,21 +688,23 @@ impl Perform for Oper<SavePost> {
|
||||
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 })
|
||||
}
|
||||
|
@ -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<ListCategories> {
|
||||
type Response = ListCategoriesResponse;
|
||||
|
||||
fn perform(
|
||||
async fn perform(
|
||||
&self,
|
||||
pool: Pool<ConnectionManager<PgConnection>>,
|
||||
pool: &DbPool,
|
||||
_websocket_info: Option<WebsocketInfo>,
|
||||
) -> Result<ListCategoriesResponse, Error> {
|
||||
) -> Result<ListCategoriesResponse, LemmyError> {
|
||||
let _data: &ListCategories = &self.data;
|
||||
|
||||
let conn = pool.get()?;
|
||||
|
||||
let categories: Vec<Category> = 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<GetModlog> {
|
||||
type Response = GetModlogResponse;
|
||||
|
||||
fn perform(
|
||||
async fn perform(
|
||||
&self,
|
||||
pool: Pool<ConnectionManager<PgConnection>>,
|
||||
pool: &DbPool,
|
||||
_websocket_info: Option<WebsocketInfo>,
|
||||
) -> Result<GetModlogResponse, Error> {
|
||||
) -> Result<GetModlogResponse, LemmyError> {
|
||||
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<GetModlog> {
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait(?Send)]
|
||||
impl Perform for Oper<CreateSite> {
|
||||
type Response = SiteResponse;
|
||||
|
||||
fn perform(
|
||||
async fn perform(
|
||||
&self,
|
||||
pool: Pool<ConnectionManager<PgConnection>>,
|
||||
pool: &DbPool,
|
||||
_websocket_info: Option<WebsocketInfo>,
|
||||
) -> Result<SiteResponse, Error> {
|
||||
) -> Result<SiteResponse, LemmyError> {
|
||||
let data: &CreateSite = &self.data;
|
||||
|
||||
let claims = match Claims::decode(&data.auth) {
|
||||
@ -266,10 +257,9 @@ impl Perform for Oper<CreateSite> {
|
||||
|
||||
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<CreateSite> {
|
||||
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<EditSite> {
|
||||
type Response = SiteResponse;
|
||||
fn perform(
|
||||
async fn perform(
|
||||
&self,
|
||||
pool: Pool<ConnectionManager<PgConnection>>,
|
||||
pool: &DbPool,
|
||||
websocket_info: Option<WebsocketInfo>,
|
||||
) -> Result<SiteResponse, Error> {
|
||||
) -> Result<SiteResponse, LemmyError> {
|
||||
let data: &EditSite = &self.data;
|
||||
|
||||
let claims = match Claims::decode(&data.auth) {
|
||||
@ -320,14 +311,13 @@ impl Perform for Oper<EditSite> {
|
||||
|
||||
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<EditSite> {
|
||||
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<EditSite> {
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait(?Send)]
|
||||
impl Perform for Oper<GetSite> {
|
||||
type Response = GetSiteResponse;
|
||||
|
||||
fn perform(
|
||||
async fn perform(
|
||||
&self,
|
||||
pool: Pool<ConnectionManager<PgConnection>>,
|
||||
pool: &DbPool,
|
||||
websocket_info: Option<WebsocketInfo>,
|
||||
) -> Result<GetSiteResponse, Error> {
|
||||
) -> Result<GetSiteResponse, LemmyError> {
|
||||
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<GetSite> {
|
||||
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<GetSite> {
|
||||
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<GetSite> {
|
||||
}
|
||||
}
|
||||
|
||||
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<GetSite> {
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait(?Send)]
|
||||
impl Perform for Oper<Search> {
|
||||
type Response = SearchResponse;
|
||||
|
||||
fn perform(
|
||||
async fn perform(
|
||||
&self,
|
||||
pool: Pool<ConnectionManager<PgConnection>>,
|
||||
pool: &DbPool,
|
||||
_websocket_info: Option<WebsocketInfo>,
|
||||
) -> Result<SearchResponse, Error> {
|
||||
) -> Result<SearchResponse, LemmyError> {
|
||||
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<Search> {
|
||||
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<Search> {
|
||||
|
||||
// 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<Search> {
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait(?Send)]
|
||||
impl Perform for Oper<TransferSite> {
|
||||
type Response = GetSiteResponse;
|
||||
|
||||
fn perform(
|
||||
async fn perform(
|
||||
&self,
|
||||
pool: Pool<ConnectionManager<PgConnection>>,
|
||||
pool: &DbPool,
|
||||
_websocket_info: Option<WebsocketInfo>,
|
||||
) -> Result<GetSiteResponse, Error> {
|
||||
) -> Result<GetSiteResponse, LemmyError> {
|
||||
let data: &TransferSite = &self.data;
|
||||
|
||||
let claims = match Claims::decode(&data.auth) {
|
||||
@ -587,9 +621,7 @@ impl Perform for Oper<TransferSite> {
|
||||
|
||||
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<TransferSite> {
|
||||
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<TransferSite> {
|
||||
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<TransferSite> {
|
||||
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<TransferSite> {
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait(?Send)]
|
||||
impl Perform for Oper<GetSiteConfig> {
|
||||
type Response = GetSiteConfigResponse;
|
||||
|
||||
fn perform(
|
||||
async fn perform(
|
||||
&self,
|
||||
pool: Pool<ConnectionManager<PgConnection>>,
|
||||
pool: &DbPool,
|
||||
_websocket_info: Option<WebsocketInfo>,
|
||||
) -> Result<GetSiteConfigResponse, Error> {
|
||||
) -> Result<GetSiteConfigResponse, LemmyError> {
|
||||
let data: &GetSiteConfig = &self.data;
|
||||
|
||||
let claims = match Claims::decode(&data.auth) {
|
||||
@ -658,10 +691,8 @@ impl Perform for Oper<GetSiteConfig> {
|
||||
|
||||
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<i32> = admins.into_iter().map(|m| m.id).collect();
|
||||
|
||||
if !admin_ids.contains(&user_id) {
|
||||
@ -674,14 +705,15 @@ impl Perform for Oper<GetSiteConfig> {
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait(?Send)]
|
||||
impl Perform for Oper<SaveSiteConfig> {
|
||||
type Response = GetSiteConfigResponse;
|
||||
|
||||
fn perform(
|
||||
async fn perform(
|
||||
&self,
|
||||
pool: Pool<ConnectionManager<PgConnection>>,
|
||||
pool: &DbPool,
|
||||
_websocket_info: Option<WebsocketInfo>,
|
||||
) -> Result<GetSiteConfigResponse, Error> {
|
||||
) -> Result<GetSiteConfigResponse, LemmyError> {
|
||||
let data: &SaveSiteConfig = &self.data;
|
||||
|
||||
let claims = match Claims::decode(&data.auth) {
|
||||
@ -691,10 +723,8 @@ impl Perform for Oper<SaveSiteConfig> {
|
||||
|
||||
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<i32> = admins.into_iter().map(|m| m.id).collect();
|
||||
|
||||
if !admin_ids.contains(&user_id) {
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -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<String>,
|
||||
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<A>(
|
||||
pub async fn send_activity_to_community<A>(
|
||||
creator: &User_,
|
||||
conn: &PgConnection,
|
||||
community: &Community,
|
||||
to: Vec<String>,
|
||||
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<A>(activity: &A, actor: &dyn ActorType, to: Vec<String>) -> Result<(), Error>
|
||||
pub async fn send_activity<A>(
|
||||
client: &Client,
|
||||
activity: &A,
|
||||
actor: &dyn ActorType,
|
||||
to: Vec<String>,
|
||||
) -> Result<(), LemmyError>
|
||||
where
|
||||
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(())
|
||||
}
|
||||
|
@ -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<CommentQuery>,
|
||||
db: DbPoolParam,
|
||||
) -> Result<HttpResponse<Body>, Error> {
|
||||
) -> Result<HttpResponse<Body>, LemmyError> {
|
||||
let id = info.comment_id.parse::<i32>()?;
|
||||
let comment = Comment::read(&&db.get()?, id)?;
|
||||
let comment = blocking(&db, move |conn| Comment::read(conn, id)).await??;
|
||||
|
||||
if !comment.deleted {
|
||||
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<Note, Error> {
|
||||
async fn to_apub(&self, pool: &DbPool) -> Result<Note, LemmyError> {
|
||||
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<Tombstone, Error> {
|
||||
fn to_tombstone(&self) -> Result<Tombstone, LemmyError> {
|
||||
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<CommentForm, Error> {
|
||||
async fn from_apub(
|
||||
note: &Note,
|
||||
client: &Client,
|
||||
pool: &DbPool,
|
||||
) -> Result<CommentForm, LemmyError> {
|
||||
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<i32> = 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<MentionsAndAddresses, Error> {
|
||||
client: &Client,
|
||||
pool: &DbPool,
|
||||
) -> Result<MentionsAndAddresses, LemmyError> {
|
||||
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::<Vec<MentionData>>();
|
||||
|
||||
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
|
||||
|
@ -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<GroupExt, Error> {
|
||||
async fn to_apub(&self, pool: &DbPool) -> Result<GroupExt, LemmyError> {
|
||||
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<Tombstone, Error> {
|
||||
fn to_tombstone(&self) -> Result<Tombstone, LemmyError> {
|
||||
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<Vec<String>, 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<Vec<String>, 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<Self, Error> {
|
||||
async fn from_apub(group: &GroupExt, client: &Client, pool: &DbPool) -> Result<Self, LemmyError> {
|
||||
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<CommunityQuery>,
|
||||
info: web::Path<CommunityQuery>,
|
||||
db: DbPoolParam,
|
||||
) -> Result<HttpResponse<Body>, Error> {
|
||||
let community = Community::read_from_name(&&db.get()?, &info.community_name)?;
|
||||
) -> Result<HttpResponse<Body>, 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<CommunityQuery>,
|
||||
info: web::Path<CommunityQuery>,
|
||||
db: DbPoolParam,
|
||||
) -> Result<HttpResponse<Body>, Error> {
|
||||
let community = Community::read_from_name(&&db.get()?, &info.community_name)?;
|
||||
) -> Result<HttpResponse<Body>, 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<A>(
|
||||
pub async fn do_announce<A>(
|
||||
activity: A,
|
||||
community: &Community,
|
||||
sender: &dyn ActorType,
|
||||
conn: &PgConnection,
|
||||
) -> Result<HttpResponse, Error>
|
||||
client: &Client,
|
||||
pool: &DbPool,
|
||||
) -> Result<HttpResponse, LemmyError>
|
||||
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())
|
||||
}
|
||||
|
@ -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<Follow, Error> {
|
||||
fn follow(&self) -> Result<Follow, LemmyError> {
|
||||
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<CommunityAcceptedObjects>,
|
||||
path: web::Path<String>,
|
||||
db: DbPoolParam,
|
||||
client: web::Data<Client>,
|
||||
_chat_server: ChatServerParam,
|
||||
) -> Result<HttpResponse, Error> {
|
||||
) -> Result<HttpResponse, LemmyError> {
|
||||
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<HttpResponse, Error> {
|
||||
insert_activity(&conn, user.id, &follow, false)?;
|
||||
async fn handle_follow(
|
||||
follow: Follow,
|
||||
user: User_,
|
||||
community: Community,
|
||||
client: &Client,
|
||||
db: DbPoolParam,
|
||||
) -> Result<HttpResponse, LemmyError> {
|
||||
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<HttpResponse, Error> {
|
||||
insert_activity(&conn, user.id, &undo, false)?;
|
||||
async fn handle_undo_follow(
|
||||
undo: Undo,
|
||||
user: User_,
|
||||
community: Community,
|
||||
db: DbPoolParam,
|
||||
) -> Result<HttpResponse, LemmyError> {
|
||||
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())
|
||||
}
|
||||
|
@ -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<GroupExtension, Error> {
|
||||
) -> Result<GroupExtension, LemmyError> {
|
||||
let category = Category::read(conn, category_id)?;
|
||||
let group_category = GroupCategory {
|
||||
identifier: category_id.to_string(),
|
||||
|
@ -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<Keypair, Error> {
|
||||
pub fn generate_actor_keypair() -> Result<Keypair, LemmyError> {
|
||||
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<Keypair, Error> {
|
||||
}
|
||||
|
||||
/// Signs request headers with the given keypair.
|
||||
pub fn sign(request: &Builder, actor: &dyn ActorType) -> Result<String, Error> {
|
||||
pub async fn sign(
|
||||
request: ClientRequest,
|
||||
actor: &dyn ActorType,
|
||||
activity: String,
|
||||
) -> Result<DigestClient<String>, LemmyError> {
|
||||
let signing_key_id = format!("{}#main-key", actor.actor_id());
|
||||
let 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::<Result<BTreeMap<String, String>, 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::<Result<BTreeMap<String, String>, 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<bool, Error> {
|
||||
.verify(|signature, signing_string| -> Result<bool, LemmyError> {
|
||||
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())
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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<NodeInfo, Error> {
|
||||
async fn _fetch_node_info(client: &Client, domain: &str) -> Result<NodeInfo, LemmyError> {
|
||||
let well_known_uri = Url::parse(&format!(
|
||||
"{}://{}/.well-known/nodeinfo",
|
||||
get_apub_protocol_string(),
|
||||
domain
|
||||
))?;
|
||||
let well_known = fetch_remote_object::<NodeInfoWellKnown>(&well_known_uri)?;
|
||||
Ok(fetch_remote_object::<NodeInfo>(&well_known.links.href)?)
|
||||
|
||||
let well_known = fetch_remote_object::<NodeInfoWellKnown>(client, &well_known_uri).await?;
|
||||
let nodeinfo = fetch_remote_object::<NodeInfo>(client, &well_known.links.href).await?;
|
||||
|
||||
Ok(nodeinfo)
|
||||
}
|
||||
|
||||
/// Fetch any type of ActivityPub object, handling things like HTTP headers, deserialisation,
|
||||
/// timeouts etc.
|
||||
pub fn fetch_remote_object<Response>(url: &Url) -> Result<Response, Error>
|
||||
pub async fn fetch_remote_object<Response>(
|
||||
client: &Client,
|
||||
url: &Url,
|
||||
) -> Result<Response, LemmyError>
|
||||
where
|
||||
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<SearchResponse, Error> {
|
||||
pub async fn search_by_apub_id(
|
||||
query: &str,
|
||||
client: &Client,
|
||||
pool: &DbPool,
|
||||
) -> Result<SearchResponse, LemmyError> {
|
||||
// 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<SearchRespo
|
||||
let split2 = split[0].split('!').collect::<Vec<&str>>();
|
||||
(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<SearchRespo
|
||||
communities: vec![],
|
||||
users: vec![],
|
||||
};
|
||||
match fetch_remote_object::<SearchAcceptedObjects>(&query_url)? {
|
||||
|
||||
let response = match fetch_remote_object::<SearchAcceptedObjects>(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<SearchRespo
|
||||
.next()
|
||||
.unwrap()
|
||||
.to_string();
|
||||
|
||||
// TODO: also fetch parent comments if any
|
||||
let post = fetch_remote_object(&Url::parse(&post_url)?)?;
|
||||
upsert_post(&PostForm::from_apub(&post, conn)?, conn)?;
|
||||
let c = upsert_comment(&CommentForm::from_apub(&c, conn)?, conn)?;
|
||||
response.comments = vec![CommentView::read(conn, c.id, None)?];
|
||||
let post = fetch_remote_object(client, &Url::parse(&post_url)?).await?;
|
||||
let post_form = PostForm::from_apub(&post, client, pool).await?;
|
||||
let comment_form = CommentForm::from_apub(&c, client, pool).await?;
|
||||
|
||||
blocking(pool, move |conn| upsert_post(&post_form, conn)).await??;
|
||||
let c = blocking(pool, move |conn| upsert_comment(&comment_form, conn)).await??;
|
||||
response.comments =
|
||||
vec![blocking(pool, move |conn| CommentView::read(conn, c.id, None)).await??];
|
||||
|
||||
response
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
/// Check if a remote user exists, create if not found, if its too old update it.Fetch a user, insert/update it in the database and return the user.
|
||||
pub fn get_or_fetch_and_upsert_remote_user(
|
||||
pub async fn get_or_fetch_and_upsert_remote_user(
|
||||
apub_id: &str,
|
||||
conn: &PgConnection,
|
||||
) -> Result<User_, Error> {
|
||||
match User_::read_from_actor_id(&conn, &apub_id) {
|
||||
Ok(u) => {
|
||||
// If its older than a day, re-fetch it
|
||||
if !u.local && should_refetch_actor(u.last_refreshed_at) {
|
||||
debug!("Fetching and updating from remote user: {}", apub_id);
|
||||
let person = fetch_remote_object::<PersonExt>(&Url::parse(apub_id)?)?;
|
||||
let mut uf = UserForm::from_apub(&person, &conn)?;
|
||||
uf.last_refreshed_at = Some(naive_now());
|
||||
Ok(User_::update(&conn, u.id, &uf)?)
|
||||
} else {
|
||||
Ok(u)
|
||||
}
|
||||
client: &Client,
|
||||
pool: &DbPool,
|
||||
) -> Result<User_, LemmyError> {
|
||||
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::<PersonExt>(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::<PersonExt>(&Url::parse(apub_id)?)?;
|
||||
let uf = UserForm::from_apub(&person, &conn)?;
|
||||
Ok(User_::create(conn, &uf)?)
|
||||
let person = fetch_remote_object::<PersonExt>(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<Community, Error> {
|
||||
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::<GroupExt>(&Url::parse(apub_id)?)?;
|
||||
let mut cf = CommunityForm::from_apub(&group, conn)?;
|
||||
cf.last_refreshed_at = Some(naive_now());
|
||||
Ok(Community::update(&conn, c.id, &cf)?)
|
||||
} else {
|
||||
Ok(c)
|
||||
}
|
||||
client: &Client,
|
||||
pool: &DbPool,
|
||||
) -> Result<Community, LemmyError> {
|
||||
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::<GroupExt>(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::<GroupExt>(&Url::parse(apub_id)?)?;
|
||||
let cf = CommunityForm::from_apub(&group, conn)?;
|
||||
let community = Community::create(conn, &cf)?;
|
||||
let group = fetch_remote_object::<GroupExt>(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::<Vec<User_>>();
|
||||
|
||||
for mod_ in creator_and_moderators {
|
||||
let community_moderator_form = CommunityModeratorForm {
|
||||
community_id: community.id,
|
||||
user_id: mod_.id,
|
||||
};
|
||||
CommunityModerator::join(&conn, &community_moderator_form)?;
|
||||
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<Post, Error> {
|
||||
fn upsert_post(post_form: &PostForm, conn: &PgConnection) -> Result<Post, LemmyError> {
|
||||
let existing = Post::read_from_apub_id(conn, &post_form.ap_id);
|
||||
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<Post, Error> {
|
||||
match Post::read_from_apub_id(conn, post_ap_id) {
|
||||
client: &Client,
|
||||
pool: &DbPool,
|
||||
) -> Result<Post, LemmyError> {
|
||||
let post_ap_id_owned = post_ap_id.to_owned();
|
||||
let post = blocking(pool, move |conn| {
|
||||
Post::read_from_apub_id(conn, &post_ap_id_owned)
|
||||
})
|
||||
.await?;
|
||||
|
||||
match post {
|
||||
Ok(p) => Ok(p),
|
||||
Err(NotFound {}) => {
|
||||
debug!("Fetching and creating remote post: {}", post_ap_id);
|
||||
let post = fetch_remote_object::<PageExt>(&Url::parse(post_ap_id)?)?;
|
||||
let post_form = PostForm::from_apub(&post, conn)?;
|
||||
Ok(Post::create(conn, &post_form)?)
|
||||
let post = fetch_remote_object::<PageExt>(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<Comment, Error> {
|
||||
fn upsert_comment(comment_form: &CommentForm, conn: &PgConnection) -> Result<Comment, LemmyError> {
|
||||
let existing = Comment::read_from_apub_id(conn, &comment_form.ap_id);
|
||||
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<Comment, Error> {
|
||||
match Comment::read_from_apub_id(conn, comment_ap_id) {
|
||||
client: &Client,
|
||||
pool: &DbPool,
|
||||
) -> Result<Comment, LemmyError> {
|
||||
let comment_ap_id_owned = comment_ap_id.to_owned();
|
||||
let comment = blocking(pool, move |conn| {
|
||||
Comment::read_from_apub_id(conn, &comment_ap_id_owned)
|
||||
})
|
||||
.await?;
|
||||
|
||||
match comment {
|
||||
Ok(p) => Ok(p),
|
||||
Err(NotFound {}) => {
|
||||
debug!(
|
||||
"Fetching and creating remote comment and its parents: {}",
|
||||
comment_ap_id
|
||||
);
|
||||
let comment = fetch_remote_object::<Note>(&Url::parse(comment_ap_id)?)?;
|
||||
let comment_form = CommentForm::from_apub(&comment, conn)?;
|
||||
Ok(Comment::create(conn, &comment_form)?)
|
||||
let comment = fetch_remote_object::<Note>(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<Vec<Post>, Error> {
|
||||
// fn fetch_community_outbox(community: &Community, conn: &PgConnection) -> Result<Vec<Post>, LemmyError> {
|
||||
// let outbox_url = Url::parse(&community.get_outbox_url())?;
|
||||
// let outbox = fetch_remote_object::<OrderedCollection>(&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<PostForm, Error> {
|
||||
// .map(|obox: &BaseBox| -> Result<PostForm, LemmyError> {
|
||||
// let page = obox.clone().to_concrete::<Page>()?;
|
||||
// PostForm::from_page(&page, conn)
|
||||
// })
|
||||
// .map(|pf| upsert_post(&pf?, conn))
|
||||
// .collect::<Result<Vec<Post>, Error>>()?,
|
||||
// .collect::<Result<Vec<Post>, LemmyError>>()?,
|
||||
// )
|
||||
// }
|
||||
|
@ -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<Self::Response, Error>;
|
||||
fn to_tombstone(&self) -> Result<Tombstone, Error>;
|
||||
async fn to_apub(&self, pool: &DbPool) -> Result<Self::Response, LemmyError>;
|
||||
fn to_tombstone(&self) -> Result<Tombstone, LemmyError>;
|
||||
}
|
||||
|
||||
/// Updated is actually the deletion time
|
||||
@ -129,7 +143,7 @@ fn create_tombstone(
|
||||
object_id: &str,
|
||||
updated: Option<NaiveDateTime>,
|
||||
former_type: String,
|
||||
) -> Result<Tombstone, Error> {
|
||||
) -> Result<Tombstone, LemmyError> {
|
||||
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<Self, Error>
|
||||
async fn from_apub(
|
||||
apub: &Self::ApubType,
|
||||
client: &Client,
|
||||
pool: &DbPool,
|
||||
) -> Result<Self, LemmyError>
|
||||
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<Vec<String>, Error>;
|
||||
async fn get_follower_inboxes(&self, pool: &DbPool) -> Result<Vec<String>, 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<String, Error> {
|
||||
pub async fn fetch_webfinger_url(
|
||||
mention: &MentionData,
|
||||
client: &Client,
|
||||
) -> Result<String, LemmyError> {
|
||||
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<String, Error> {
|
||||
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<String, Error> {
|
||||
link
|
||||
.href
|
||||
.to_owned()
|
||||
.ok_or_else(|| format_err!("No href found."))
|
||||
.ok_or_else(|| format_err!("No href found.").into())
|
||||
}
|
||||
|
@ -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<PostQuery>,
|
||||
info: web::Path<PostQuery>,
|
||||
db: DbPoolParam,
|
||||
) -> Result<HttpResponse<Body>, Error> {
|
||||
) -> Result<HttpResponse<Body>, LemmyError> {
|
||||
let id = info.post_id.parse::<i32>()?;
|
||||
let post = Post::read(&&db.get()?, id)?;
|
||||
let post = blocking(&db, move |conn| Post::read(conn, id)).await??;
|
||||
|
||||
if !post.deleted {
|
||||
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<PageExt, Error> {
|
||||
async fn to_apub(&self, pool: &DbPool) -> Result<PageExt, LemmyError> {
|
||||
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<Tombstone, Error> {
|
||||
fn to_tombstone(&self) -> Result<Tombstone, LemmyError> {
|
||||
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<PostForm, Error> {
|
||||
async fn from_apub(
|
||||
page: &PageExt,
|
||||
client: &Client,
|
||||
pool: &DbPool,
|
||||
) -> Result<PostForm, LemmyError> {
|
||||
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(())
|
||||
}
|
||||
}
|
||||
|
@ -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<Note, Error> {
|
||||
async fn to_apub(&self, pool: &DbPool) -> Result<Note, LemmyError> {
|
||||
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<Tombstone, Error> {
|
||||
fn to_tombstone(&self) -> Result<Tombstone, LemmyError> {
|
||||
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<PrivateMessageForm, Error> {
|
||||
async fn from_apub(
|
||||
note: &Note,
|
||||
client: &Client,
|
||||
pool: &DbPool,
|
||||
) -> Result<PrivateMessageForm, LemmyError> {
|
||||
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!()
|
||||
}
|
||||
}
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -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<PersonExt, Error> {
|
||||
async fn to_apub(&self, _pool: &DbPool) -> Result<PersonExt, LemmyError> {
|
||||
// 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<Tombstone, Error> {
|
||||
fn to_tombstone(&self) -> Result<Tombstone, LemmyError> {
|
||||
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::<XsdAnyUri>()?, 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<Vec<String>, Error> {
|
||||
async fn get_follower_inboxes(&self, _pool: &DbPool) -> Result<Vec<String>, 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<Self, Error> {
|
||||
async fn from_apub(person: &PersonExt, _: &Client, _: &DbPool) -> Result<Self, LemmyError> {
|
||||
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<UserQuery>,
|
||||
info: web::Path<UserQuery>,
|
||||
db: DbPoolParam,
|
||||
) -> Result<HttpResponse<Body>, Error> {
|
||||
let user = User_::find_by_email_or_username(&&db.get()?, &info.user_name)?;
|
||||
let u = user.to_apub(&db.get().unwrap())?;
|
||||
) -> Result<HttpResponse<Body>, 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))
|
||||
}
|
||||
|
@ -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<UserAcceptedObjects>,
|
||||
path: web::Path<String>,
|
||||
client: web::Data<Client>,
|
||||
db: DbPoolParam,
|
||||
chat_server: ChatServerParam,
|
||||
) -> Result<HttpResponse, Error> {
|
||||
) -> Result<HttpResponse, LemmyError> {
|
||||
// TODO: would be nice if we could do the signature check here, but we cant access the actor property
|
||||
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<HttpResponse, Error> {
|
||||
client: &Client,
|
||||
pool: &DbPool,
|
||||
) -> Result<HttpResponse, LemmyError> {
|
||||
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<HttpResponse, Error> {
|
||||
) -> Result<HttpResponse, LemmyError> {
|
||||
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<HttpResponse, Error> {
|
||||
) -> Result<HttpResponse, LemmyError> {
|
||||
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<HttpResponse, Error> {
|
||||
) -> Result<HttpResponse, LemmyError> {
|
||||
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<HttpResponse, Error> {
|
||||
) -> Result<HttpResponse, LemmyError> {
|
||||
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,
|
||||
});
|
||||
|
||||
|
@ -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<ActivityForm> for Activity {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn insert_activity<T>(
|
||||
pub async fn insert_activity<T>(
|
||||
user_id: i32,
|
||||
data: T,
|
||||
local: bool,
|
||||
pool: &DbPool,
|
||||
) -> Result<(), LemmyError>
|
||||
where
|
||||
T: Serialize + Debug + Send + 'static,
|
||||
{
|
||||
blocking(pool, move |conn| {
|
||||
do_insert_activity(conn, user_id, &data, local)
|
||||
})
|
||||
.await??;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn do_insert_activity<T>(
|
||||
conn: &PgConnection,
|
||||
user_id: i32,
|
||||
data: &T,
|
||||
local: bool,
|
||||
) -> Result<(), failure::Error>
|
||||
) -> Result<(), LemmyError>
|
||||
where
|
||||
T: Serialize + Debug,
|
||||
{
|
||||
|
@ -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");
|
||||
|
@ -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 {
|
||||
|
@ -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,
|
||||
|
@ -50,8 +50,8 @@ impl Crud<PasswordResetRequestForm> for PasswordResetRequest {
|
||||
impl PasswordResetRequest {
|
||||
pub fn create_token(conn: &PgConnection, from_user_id: i32, token: &str) -> Result<Self, Error> {
|
||||
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<Self, Error> {
|
||||
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()))
|
||||
|
@ -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,
|
||||
|
@ -26,20 +26,39 @@ pub extern crate serde_json;
|
||||
pub extern crate sha2;
|
||||
pub extern crate strum;
|
||||
|
||||
pub async fn blocking<F, T>(pool: &DbPool, f: F) -> Result<T, LemmyError>
|
||||
where
|
||||
F: FnOnce(&diesel::PgConnection) -> T + Send + 'static,
|
||||
T: Send + 'static,
|
||||
{
|
||||
let pool = pool.clone();
|
||||
let res = actix_web::web::block(move || {
|
||||
let conn = pool.get()?;
|
||||
let res = (f)(&conn);
|
||||
Ok(res) as Result<_, LemmyError>
|
||||
})
|
||||
.await?;
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
pub mod api;
|
||||
pub mod 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<diesel::r2d2::ConnectionManager<diesel::PgConnection>>;
|
||||
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<T> From<T> for LemmyError
|
||||
where
|
||||
T: Into<failure::Error>,
|
||||
{
|
||||
fn from(t: T) -> Self {
|
||||
LemmyError { inner: t.into() }
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_datetime_utc(ndt: NaiveDateTime) -> DateTime<Utc> {
|
||||
DateTime::<Utc>::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<String>,
|
||||
}
|
||||
|
||||
pub fn fetch_iframely(url: &str) -> Result<IframelyResponse, failure::Error> {
|
||||
pub async fn fetch_iframely(client: &Client, url: &str) -> Result<IframelyResponse, LemmyError> {
|
||||
let fetch_url = format!("http://iframely/oembed?url={}", url);
|
||||
let 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<PictrsResponse, failure::Error> {
|
||||
is_image_content_type(image_url)?;
|
||||
pub async fn fetch_pictrs(client: &Client, image_url: &str) -> Result<PictrsResponse, LemmyError> {
|
||||
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<String>,
|
||||
) -> (
|
||||
Option<String>,
|
||||
@ -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<name>[\w.]+)@(?P<domain>[a-zA-Z0-9._-]+\.[a-zA-Z0-9_-]+)").unwrap();
|
||||
|
@ -4,10 +4,13 @@ extern crate diesel_migrations;
|
||||
#[macro_use]
|
||||
pub extern crate lazy_static;
|
||||
|
||||
pub type DbPool = Pool<ConnectionManager<PgConnection>>;
|
||||
|
||||
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<S>(
|
||||
|
@ -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<Mutex<RateLimiter>>,
|
||||
}
|
||||
|
||||
@ -57,17 +59,11 @@ impl RateLimited {
|
||||
fut: impl Future<Output = Result<T, E>>,
|
||||
) -> Result<T, E>
|
||||
where
|
||||
E: From<failure::Error>,
|
||||
E: From<LemmyError>,
|
||||
{
|
||||
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 => {
|
||||
|
@ -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(),
|
||||
)
|
||||
|
51
server/src/request.rs
Normal file
51
server/src/request.rs
Normal file
@ -0,0 +1,51 @@
|
||||
use crate::LemmyError;
|
||||
use std::future::Future;
|
||||
|
||||
#[derive(Clone, Debug, Fail)]
|
||||
#[fail(display = "Error sending request, {}", _0)]
|
||||
struct SendError(pub String);
|
||||
|
||||
#[derive(Clone, Debug, Fail)]
|
||||
#[fail(display = "Error receiving response, {}", _0)]
|
||||
pub struct RecvError(pub String);
|
||||
|
||||
pub async fn retry<F, Fut, T>(f: F) -> Result<T, LemmyError>
|
||||
where
|
||||
F: Fn() -> Fut,
|
||||
Fut: Future<Output = Result<T, actix_web::client::SendRequestError>>,
|
||||
{
|
||||
retry_custom(|| async { Ok((f)().await) }).await
|
||||
}
|
||||
|
||||
pub async fn retry_custom<F, Fut, T>(f: F) -> Result<T, LemmyError>
|
||||
where
|
||||
F: Fn() -> Fut,
|
||||
Fut: Future<Output = Result<Result<T, actix_web::client::SendRequestError>, LemmyError>>,
|
||||
{
|
||||
let mut response = Err(format_err!("connect timeout").into());
|
||||
|
||||
for _ in 0u8..3 {
|
||||
match (f)().await? {
|
||||
Ok(t) => return Ok(t),
|
||||
Err(e) => {
|
||||
if is_connect_timeout(&e) {
|
||||
response = Err(SendError(e.to_string()).into());
|
||||
continue;
|
||||
}
|
||||
return Err(SendError(e.to_string()).into());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
response
|
||||
}
|
||||
|
||||
fn is_connect_timeout(e: &actix_web::client::SendRequestError) -> bool {
|
||||
if let actix_web::client::SendRequestError::Connect(e) = e {
|
||||
if let actix_web::client::ConnectError::Timeout = e {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
@ -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<Request>(
|
||||
data: Request,
|
||||
client: &Client,
|
||||
db: DbPoolParam,
|
||||
chat_server: ChatServerParam,
|
||||
) -> Result<HttpResponse, Error>
|
||||
@ -162,9 +163,10 @@ where
|
||||
id: None,
|
||||
};
|
||||
|
||||
let oper: Oper<Request> = Oper::new(data);
|
||||
let oper: Oper<Request> = Oper::new(data, client.clone());
|
||||
|
||||
let res = web::block(move || oper.perform(db.get_ref().to_owned(), Some(ws_info)))
|
||||
let res = oper
|
||||
.perform(&db, Some(ws_info))
|
||||
.await
|
||||
.map(|json| HttpResponse::Ok().json(json))
|
||||
.map_err(ErrorBadRequest)?;
|
||||
@ -173,6 +175,7 @@ where
|
||||
|
||||
async fn route_get<Data>(
|
||||
data: web::Query<Data>,
|
||||
client: web::Data<Client>,
|
||||
db: DbPoolParam,
|
||||
chat_server: ChatServerParam,
|
||||
) -> Result<HttpResponse, Error>
|
||||
@ -180,11 +183,12 @@ where
|
||||
Data: Serialize + Send + 'static,
|
||||
Oper<Data>: Perform,
|
||||
{
|
||||
perform::<Data>(data.0, db, chat_server).await
|
||||
perform::<Data>(data.0, &client, db, chat_server).await
|
||||
}
|
||||
|
||||
async fn route_post<Data>(
|
||||
data: web::Json<Data>,
|
||||
client: web::Data<Client>,
|
||||
db: DbPoolParam,
|
||||
chat_server: ChatServerParam,
|
||||
) -> Result<HttpResponse, Error>
|
||||
@ -192,5 +196,5 @@ where
|
||||
Data: Serialize + Send + 'static,
|
||||
Oper<Data>: Perform,
|
||||
{
|
||||
perform::<Data>(data.0, db, chat_server).await
|
||||
perform::<Data>(data.0, &client, db, chat_server).await
|
||||
}
|
||||
|
@ -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)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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<Params>, db: DbPoolParam) -> Result<HttpResponse, Error> {
|
||||
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<String, failure::Error> {
|
||||
fn get_feed_all_data(conn: &PgConnection, sort_type: &SortType) -> Result<String, LemmyError> {
|
||||
let site_view = SiteView::read(&conn)?;
|
||||
|
||||
let posts = PostQueryBuilder::create(&conn)
|
||||
@ -85,37 +86,34 @@ async fn get_feed(
|
||||
info: web::Query<Params>,
|
||||
db: web::Data<Pool<ConnectionManager<PgConnection>>>,
|
||||
) -> Result<HttpResponse, Error> {
|
||||
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<Params>) -> Result<SortType, ParseError> {
|
||||
@ -130,7 +128,7 @@ fn get_feed_user(
|
||||
conn: &PgConnection,
|
||||
sort_type: &SortType,
|
||||
user_name: String,
|
||||
) -> Result<ChannelBuilder, failure::Error> {
|
||||
) -> Result<ChannelBuilder, LemmyError> {
|
||||
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<ChannelBuilder, failure::Error> {
|
||||
) -> Result<ChannelBuilder, LemmyError> {
|
||||
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<ChannelBuilder, failure::Error> {
|
||||
) -> Result<ChannelBuilder, LemmyError> {
|
||||
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<ChannelBuilder, failure::Error> {
|
||||
fn get_feed_inbox(conn: &PgConnection, jwt: String) -> Result<ChannelBuilder, LemmyError> {
|
||||
let site_view = SiteView::read(&conn)?;
|
||||
let user_id = Claims::decode(&jwt)?.claims.id;
|
||||
|
||||
|
@ -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<HttpResponse<Body>, failure::Error> {
|
||||
async fn node_info_well_known() -> Result<HttpResponse<Body>, 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<HttpResponse<Body>, failure::Error> {
|
||||
}
|
||||
|
||||
async fn node_info(db: DbPoolParam) -> Result<HttpResponse, Error> {
|
||||
let res = web::block(move || {
|
||||
let conn = db.get()?;
|
||||
let site_view = match SiteView::read(&conn) {
|
||||
Ok(site_view) => site_view,
|
||||
Err(_) => return Err(format_err!("not_found")),
|
||||
};
|
||||
let protocols = if Settings::get().federation.enabled {
|
||||
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)]
|
||||
|
@ -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<Params>,
|
||||
db: DbPoolParam,
|
||||
) -> Result<HttpResponse, Error> {
|
||||
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))
|
||||
}
|
||||
|
@ -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<String, Error> {
|
||||
pub fn read_config_file() -> Result<String, LemmyError> {
|
||||
Ok(fs::read_to_string(CONFIG_FILE)?)
|
||||
}
|
||||
|
||||
pub fn save_config_file(data: &str) -> Result<String, Error> {
|
||||
pub fn save_config_file(data: &str) -> Result<String, LemmyError> {
|
||||
fs::write(CONFIG_FILE, data)?;
|
||||
|
||||
// Reload the new settings
|
||||
|
@ -1 +1 @@
|
||||
pub const VERSION: &str = "v0.7.5";
|
||||
pub const VERSION: &str = "v0.7.8";
|
||||
|
@ -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};
|
||||
|
@ -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<ConnectionManager<PgConnection>>,
|
||||
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<ConnectionId>,
|
||||
) -> Result<(), Error>
|
||||
) -> Result<(), LemmyError>
|
||||
where
|
||||
Response: Serialize,
|
||||
{
|
||||
@ -260,7 +268,7 @@ impl ChatServer {
|
||||
response: &Response,
|
||||
community_id: CommunityId,
|
||||
my_id: Option<ConnectionId>,
|
||||
) -> Result<(), Error>
|
||||
) -> Result<(), LemmyError>
|
||||
where
|
||||
Response: Serialize,
|
||||
{
|
||||
@ -283,7 +291,7 @@ impl ChatServer {
|
||||
op: &UserOperation,
|
||||
response: &Response,
|
||||
my_id: Option<ConnectionId>,
|
||||
) -> Result<(), Error>
|
||||
) -> Result<(), LemmyError>
|
||||
where
|
||||
Response: Serialize,
|
||||
{
|
||||
@ -305,7 +313,7 @@ impl ChatServer {
|
||||
response: &Response,
|
||||
recipient_id: UserId,
|
||||
my_id: Option<ConnectionId>,
|
||||
) -> Result<(), Error>
|
||||
) -> Result<(), LemmyError>
|
||||
where
|
||||
Response: Serialize,
|
||||
{
|
||||
@ -328,7 +336,7 @@ impl ChatServer {
|
||||
user_operation: &UserOperation,
|
||||
comment: &CommentResponse,
|
||||
my_id: Option<ConnectionId>,
|
||||
) -> 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<ConnectionId>,
|
||||
) -> 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<Self>,
|
||||
) -> impl Future<Output = Result<String, Error>> {
|
||||
) -> impl Future<Output = Result<String, LemmyError>> {
|
||||
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::<Login>(pool, rate_limiter, addr, msg.id, ip, user_operation, data)
|
||||
.await
|
||||
}
|
||||
UserOperation::Register => {
|
||||
do_user_operation::<Register>(pool, rate_limiter, addr, msg.id, ip, user_operation, data)
|
||||
.await
|
||||
}
|
||||
UserOperation::GetUserDetails => {
|
||||
do_user_operation::<GetUserDetails>(
|
||||
pool,
|
||||
rate_limiter,
|
||||
addr,
|
||||
msg.id,
|
||||
ip,
|
||||
user_operation,
|
||||
data,
|
||||
)
|
||||
.await
|
||||
}
|
||||
UserOperation::GetReplies => {
|
||||
do_user_operation::<GetReplies>(
|
||||
pool,
|
||||
rate_limiter,
|
||||
addr,
|
||||
msg.id,
|
||||
ip,
|
||||
user_operation,
|
||||
data,
|
||||
)
|
||||
.await
|
||||
}
|
||||
UserOperation::AddAdmin => {
|
||||
do_user_operation::<AddAdmin>(pool, rate_limiter, addr, msg.id, ip, user_operation, data)
|
||||
.await
|
||||
}
|
||||
UserOperation::BanUser => {
|
||||
do_user_operation::<BanUser>(pool, rate_limiter, addr, msg.id, ip, user_operation, data)
|
||||
.await
|
||||
}
|
||||
UserOperation::GetUserMentions => {
|
||||
do_user_operation::<GetUserMentions>(
|
||||
pool,
|
||||
rate_limiter,
|
||||
addr,
|
||||
msg.id,
|
||||
ip,
|
||||
user_operation,
|
||||
data,
|
||||
)
|
||||
.await
|
||||
}
|
||||
UserOperation::EditUserMention => {
|
||||
do_user_operation::<EditUserMention>(
|
||||
pool,
|
||||
rate_limiter,
|
||||
addr,
|
||||
msg.id,
|
||||
ip,
|
||||
user_operation,
|
||||
data,
|
||||
)
|
||||
.await
|
||||
}
|
||||
UserOperation::MarkAllAsRead => {
|
||||
do_user_operation::<MarkAllAsRead>(
|
||||
pool,
|
||||
rate_limiter,
|
||||
addr,
|
||||
msg.id,
|
||||
ip,
|
||||
user_operation,
|
||||
data,
|
||||
)
|
||||
.await
|
||||
}
|
||||
UserOperation::DeleteAccount => {
|
||||
do_user_operation::<DeleteAccount>(
|
||||
pool,
|
||||
rate_limiter,
|
||||
addr,
|
||||
msg.id,
|
||||
ip,
|
||||
user_operation,
|
||||
data,
|
||||
)
|
||||
.await
|
||||
}
|
||||
UserOperation::PasswordReset => {
|
||||
do_user_operation::<PasswordReset>(
|
||||
pool,
|
||||
rate_limiter,
|
||||
addr,
|
||||
msg.id,
|
||||
ip,
|
||||
user_operation,
|
||||
data,
|
||||
)
|
||||
.await
|
||||
}
|
||||
UserOperation::PasswordChange => {
|
||||
do_user_operation::<PasswordChange>(
|
||||
pool,
|
||||
rate_limiter,
|
||||
addr,
|
||||
msg.id,
|
||||
ip,
|
||||
user_operation,
|
||||
data,
|
||||
)
|
||||
.await
|
||||
}
|
||||
UserOperation::Login => do_user_operation::<Login>(args).await,
|
||||
UserOperation::Register => do_user_operation::<Register>(args).await,
|
||||
UserOperation::GetUserDetails => do_user_operation::<GetUserDetails>(args).await,
|
||||
UserOperation::GetReplies => do_user_operation::<GetReplies>(args).await,
|
||||
UserOperation::AddAdmin => do_user_operation::<AddAdmin>(args).await,
|
||||
UserOperation::BanUser => do_user_operation::<BanUser>(args).await,
|
||||
UserOperation::GetUserMentions => do_user_operation::<GetUserMentions>(args).await,
|
||||
UserOperation::EditUserMention => do_user_operation::<EditUserMention>(args).await,
|
||||
UserOperation::MarkAllAsRead => do_user_operation::<MarkAllAsRead>(args).await,
|
||||
UserOperation::DeleteAccount => do_user_operation::<DeleteAccount>(args).await,
|
||||
UserOperation::PasswordReset => do_user_operation::<PasswordReset>(args).await,
|
||||
UserOperation::PasswordChange => do_user_operation::<PasswordChange>(args).await,
|
||||
UserOperation::CreatePrivateMessage => {
|
||||
do_user_operation::<CreatePrivateMessage>(
|
||||
pool,
|
||||
rate_limiter,
|
||||
addr,
|
||||
msg.id,
|
||||
ip,
|
||||
user_operation,
|
||||
data,
|
||||
)
|
||||
.await
|
||||
}
|
||||
UserOperation::EditPrivateMessage => {
|
||||
do_user_operation::<EditPrivateMessage>(
|
||||
pool,
|
||||
rate_limiter,
|
||||
addr,
|
||||
msg.id,
|
||||
ip,
|
||||
user_operation,
|
||||
data,
|
||||
)
|
||||
.await
|
||||
}
|
||||
UserOperation::GetPrivateMessages => {
|
||||
do_user_operation::<GetPrivateMessages>(
|
||||
pool,
|
||||
rate_limiter,
|
||||
addr,
|
||||
msg.id,
|
||||
ip,
|
||||
user_operation,
|
||||
data,
|
||||
)
|
||||
.await
|
||||
}
|
||||
UserOperation::UserJoin => {
|
||||
do_user_operation::<UserJoin>(pool, rate_limiter, addr, msg.id, ip, user_operation, data)
|
||||
.await
|
||||
}
|
||||
UserOperation::SaveUserSettings => {
|
||||
do_user_operation::<SaveUserSettings>(
|
||||
pool,
|
||||
rate_limiter,
|
||||
addr,
|
||||
msg.id,
|
||||
ip,
|
||||
user_operation,
|
||||
data,
|
||||
)
|
||||
.await
|
||||
do_user_operation::<CreatePrivateMessage>(args).await
|
||||
}
|
||||
UserOperation::EditPrivateMessage => do_user_operation::<EditPrivateMessage>(args).await,
|
||||
UserOperation::GetPrivateMessages => do_user_operation::<GetPrivateMessages>(args).await,
|
||||
UserOperation::UserJoin => do_user_operation::<UserJoin>(args).await,
|
||||
UserOperation::SaveUserSettings => do_user_operation::<SaveUserSettings>(args).await,
|
||||
|
||||
// Site ops
|
||||
UserOperation::GetModlog => {
|
||||
do_user_operation::<GetModlog>(pool, rate_limiter, addr, msg.id, ip, user_operation, data)
|
||||
.await
|
||||
}
|
||||
UserOperation::CreateSite => {
|
||||
do_user_operation::<CreateSite>(
|
||||
pool,
|
||||
rate_limiter,
|
||||
addr,
|
||||
msg.id,
|
||||
ip,
|
||||
user_operation,
|
||||
data,
|
||||
)
|
||||
.await
|
||||
}
|
||||
UserOperation::EditSite => {
|
||||
do_user_operation::<EditSite>(pool, rate_limiter, addr, msg.id, ip, user_operation, data)
|
||||
.await
|
||||
}
|
||||
UserOperation::GetSite => {
|
||||
do_user_operation::<GetSite>(pool, rate_limiter, addr, msg.id, ip, user_operation, data)
|
||||
.await
|
||||
}
|
||||
UserOperation::GetSiteConfig => {
|
||||
do_user_operation::<GetSiteConfig>(
|
||||
pool,
|
||||
rate_limiter,
|
||||
addr,
|
||||
msg.id,
|
||||
ip,
|
||||
user_operation,
|
||||
data,
|
||||
)
|
||||
.await
|
||||
}
|
||||
UserOperation::SaveSiteConfig => {
|
||||
do_user_operation::<SaveSiteConfig>(
|
||||
pool,
|
||||
rate_limiter,
|
||||
addr,
|
||||
msg.id,
|
||||
ip,
|
||||
user_operation,
|
||||
data,
|
||||
)
|
||||
.await
|
||||
}
|
||||
UserOperation::Search => {
|
||||
do_user_operation::<Search>(pool, rate_limiter, addr, msg.id, ip, user_operation, data)
|
||||
.await
|
||||
}
|
||||
UserOperation::TransferCommunity => {
|
||||
do_user_operation::<TransferCommunity>(
|
||||
pool,
|
||||
rate_limiter,
|
||||
addr,
|
||||
msg.id,
|
||||
ip,
|
||||
user_operation,
|
||||
data,
|
||||
)
|
||||
.await
|
||||
}
|
||||
UserOperation::TransferSite => {
|
||||
do_user_operation::<TransferSite>(
|
||||
pool,
|
||||
rate_limiter,
|
||||
addr,
|
||||
msg.id,
|
||||
ip,
|
||||
user_operation,
|
||||
data,
|
||||
)
|
||||
.await
|
||||
}
|
||||
UserOperation::ListCategories => {
|
||||
do_user_operation::<ListCategories>(
|
||||
pool,
|
||||
rate_limiter,
|
||||
addr,
|
||||
msg.id,
|
||||
ip,
|
||||
user_operation,
|
||||
data,
|
||||
)
|
||||
.await
|
||||
}
|
||||
UserOperation::GetModlog => do_user_operation::<GetModlog>(args).await,
|
||||
UserOperation::CreateSite => do_user_operation::<CreateSite>(args).await,
|
||||
UserOperation::EditSite => do_user_operation::<EditSite>(args).await,
|
||||
UserOperation::GetSite => do_user_operation::<GetSite>(args).await,
|
||||
UserOperation::GetSiteConfig => do_user_operation::<GetSiteConfig>(args).await,
|
||||
UserOperation::SaveSiteConfig => do_user_operation::<SaveSiteConfig>(args).await,
|
||||
UserOperation::Search => do_user_operation::<Search>(args).await,
|
||||
UserOperation::TransferCommunity => do_user_operation::<TransferCommunity>(args).await,
|
||||
UserOperation::TransferSite => do_user_operation::<TransferSite>(args).await,
|
||||
UserOperation::ListCategories => do_user_operation::<ListCategories>(args).await,
|
||||
|
||||
// Community ops
|
||||
UserOperation::GetCommunity => {
|
||||
do_user_operation::<GetCommunity>(
|
||||
pool,
|
||||
rate_limiter,
|
||||
addr,
|
||||
msg.id,
|
||||
ip,
|
||||
user_operation,
|
||||
data,
|
||||
)
|
||||
.await
|
||||
}
|
||||
UserOperation::ListCommunities => {
|
||||
do_user_operation::<ListCommunities>(
|
||||
pool,
|
||||
rate_limiter,
|
||||
addr,
|
||||
msg.id,
|
||||
ip,
|
||||
user_operation,
|
||||
data,
|
||||
)
|
||||
.await
|
||||
}
|
||||
UserOperation::CreateCommunity => {
|
||||
do_user_operation::<CreateCommunity>(
|
||||
pool,
|
||||
rate_limiter,
|
||||
addr,
|
||||
msg.id,
|
||||
ip,
|
||||
user_operation,
|
||||
data,
|
||||
)
|
||||
.await
|
||||
}
|
||||
UserOperation::EditCommunity => {
|
||||
do_user_operation::<EditCommunity>(
|
||||
pool,
|
||||
rate_limiter,
|
||||
addr,
|
||||
msg.id,
|
||||
ip,
|
||||
user_operation,
|
||||
data,
|
||||
)
|
||||
.await
|
||||
}
|
||||
UserOperation::FollowCommunity => {
|
||||
do_user_operation::<FollowCommunity>(
|
||||
pool,
|
||||
rate_limiter,
|
||||
addr,
|
||||
msg.id,
|
||||
ip,
|
||||
user_operation,
|
||||
data,
|
||||
)
|
||||
.await
|
||||
}
|
||||
UserOperation::GetCommunity => do_user_operation::<GetCommunity>(args).await,
|
||||
UserOperation::ListCommunities => do_user_operation::<ListCommunities>(args).await,
|
||||
UserOperation::CreateCommunity => do_user_operation::<CreateCommunity>(args).await,
|
||||
UserOperation::EditCommunity => do_user_operation::<EditCommunity>(args).await,
|
||||
UserOperation::FollowCommunity => do_user_operation::<FollowCommunity>(args).await,
|
||||
UserOperation::GetFollowedCommunities => {
|
||||
do_user_operation::<GetFollowedCommunities>(
|
||||
pool,
|
||||
rate_limiter,
|
||||
addr,
|
||||
msg.id,
|
||||
ip,
|
||||
user_operation,
|
||||
data,
|
||||
)
|
||||
.await
|
||||
}
|
||||
UserOperation::BanFromCommunity => {
|
||||
do_user_operation::<BanFromCommunity>(
|
||||
pool,
|
||||
rate_limiter,
|
||||
addr,
|
||||
msg.id,
|
||||
ip,
|
||||
user_operation,
|
||||
data,
|
||||
)
|
||||
.await
|
||||
}
|
||||
UserOperation::AddModToCommunity => {
|
||||
do_user_operation::<AddModToCommunity>(
|
||||
pool,
|
||||
rate_limiter,
|
||||
addr,
|
||||
msg.id,
|
||||
ip,
|
||||
user_operation,
|
||||
data,
|
||||
)
|
||||
.await
|
||||
do_user_operation::<GetFollowedCommunities>(args).await
|
||||
}
|
||||
UserOperation::BanFromCommunity => do_user_operation::<BanFromCommunity>(args).await,
|
||||
UserOperation::AddModToCommunity => do_user_operation::<AddModToCommunity>(args).await,
|
||||
|
||||
// Post ops
|
||||
UserOperation::CreatePost => {
|
||||
do_user_operation::<CreatePost>(
|
||||
pool,
|
||||
rate_limiter,
|
||||
addr,
|
||||
msg.id,
|
||||
ip,
|
||||
user_operation,
|
||||
data,
|
||||
)
|
||||
.await
|
||||
}
|
||||
UserOperation::GetPost => {
|
||||
do_user_operation::<GetPost>(pool, rate_limiter, addr, msg.id, ip, user_operation, data)
|
||||
.await
|
||||
}
|
||||
UserOperation::GetPosts => {
|
||||
do_user_operation::<GetPosts>(pool, rate_limiter, addr, msg.id, ip, user_operation, data)
|
||||
.await
|
||||
}
|
||||
UserOperation::EditPost => {
|
||||
do_user_operation::<EditPost>(pool, rate_limiter, addr, msg.id, ip, user_operation, data)
|
||||
.await
|
||||
}
|
||||
UserOperation::CreatePostLike => {
|
||||
do_user_operation::<CreatePostLike>(
|
||||
pool,
|
||||
rate_limiter,
|
||||
addr,
|
||||
msg.id,
|
||||
ip,
|
||||
user_operation,
|
||||
data,
|
||||
)
|
||||
.await
|
||||
}
|
||||
UserOperation::SavePost => {
|
||||
do_user_operation::<SavePost>(pool, rate_limiter, addr, msg.id, ip, user_operation, data)
|
||||
.await
|
||||
}
|
||||
UserOperation::CreatePost => do_user_operation::<CreatePost>(args).await,
|
||||
UserOperation::GetPost => do_user_operation::<GetPost>(args).await,
|
||||
UserOperation::GetPosts => do_user_operation::<GetPosts>(args).await,
|
||||
UserOperation::EditPost => do_user_operation::<EditPost>(args).await,
|
||||
UserOperation::CreatePostLike => do_user_operation::<CreatePostLike>(args).await,
|
||||
UserOperation::SavePost => do_user_operation::<SavePost>(args).await,
|
||||
|
||||
// Comment ops
|
||||
UserOperation::CreateComment => {
|
||||
do_user_operation::<CreateComment>(
|
||||
pool,
|
||||
rate_limiter,
|
||||
addr,
|
||||
msg.id,
|
||||
ip,
|
||||
user_operation,
|
||||
data,
|
||||
)
|
||||
.await
|
||||
}
|
||||
UserOperation::EditComment => {
|
||||
do_user_operation::<EditComment>(
|
||||
pool,
|
||||
rate_limiter,
|
||||
addr,
|
||||
msg.id,
|
||||
ip,
|
||||
user_operation,
|
||||
data,
|
||||
)
|
||||
.await
|
||||
}
|
||||
UserOperation::SaveComment => {
|
||||
do_user_operation::<SaveComment>(
|
||||
pool,
|
||||
rate_limiter,
|
||||
addr,
|
||||
msg.id,
|
||||
ip,
|
||||
user_operation,
|
||||
data,
|
||||
)
|
||||
.await
|
||||
}
|
||||
UserOperation::GetComments => {
|
||||
do_user_operation::<GetComments>(
|
||||
pool,
|
||||
rate_limiter,
|
||||
addr,
|
||||
msg.id,
|
||||
ip,
|
||||
user_operation,
|
||||
data,
|
||||
)
|
||||
.await
|
||||
}
|
||||
UserOperation::CreateCommentLike => {
|
||||
do_user_operation::<CreateCommentLike>(
|
||||
pool,
|
||||
rate_limiter,
|
||||
addr,
|
||||
msg.id,
|
||||
ip,
|
||||
user_operation,
|
||||
data,
|
||||
)
|
||||
.await
|
||||
}
|
||||
UserOperation::CreateComment => do_user_operation::<CreateComment>(args).await,
|
||||
UserOperation::EditComment => do_user_operation::<EditComment>(args).await,
|
||||
UserOperation::SaveComment => do_user_operation::<SaveComment>(args).await,
|
||||
UserOperation::GetComments => do_user_operation::<GetComments>(args).await,
|
||||
UserOperation::CreateCommentLike => do_user_operation::<CreateCommentLike>(args).await,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn do_user_operation<'a, Data>(
|
||||
pool: Pool<ConnectionManager<PgConnection>>,
|
||||
struct Args<'a> {
|
||||
client: Client,
|
||||
pool: DbPool,
|
||||
rate_limiter: RateLimit,
|
||||
chatserver: Addr<ChatServer>,
|
||||
id: ConnectionId,
|
||||
ip: IPAddr,
|
||||
op: UserOperation,
|
||||
data: &str,
|
||||
) -> Result<String, Error>
|
||||
data: &'a str,
|
||||
}
|
||||
|
||||
async fn do_user_operation<'a, 'b, Data>(args: Args<'b>) -> Result<String, LemmyError>
|
||||
where
|
||||
for<'de> Data: Deserialize<'de> + 'a,
|
||||
Oper<Data>: 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<T> {
|
||||
data: T,
|
||||
}
|
||||
|
||||
fn to_json_string<Response>(op: &UserOperation, data: &Response) -> Result<String, Error>
|
||||
fn to_json_string<Response>(op: &UserOperation, data: &Response) -> Result<String, LemmyError>
|
||||
where
|
||||
Response: Serialize,
|
||||
{
|
||||
|
18
ui/src/api_tests/api.spec.ts
vendored
18
ui/src/api_tests/api.spec.ts
vendored
@ -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',
|
||||
|
4
ui/src/components/comment-form.tsx
vendored
4
ui/src/components/comment-form.tsx
vendored
@ -263,7 +263,9 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
|
||||
// If its a comment edit, only check that its from your user, and that its a
|
||||
// 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;
|
||||
|
49
ui/src/components/login.tsx
vendored
49
ui/src/components/login.tsx
vendored
@ -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<any, State> {
|
||||
@ -40,6 +45,11 @@ export class Login extends Component<any, State> {
|
||||
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<any, State> {
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-10 col-form-label" htmlFor="register-math">
|
||||
{i18n.t('what_is')}{' '}
|
||||
{`${this.state.mathQuestion.a} + ${this.state.mathQuestion.b}?`}
|
||||
</label>
|
||||
|
||||
<div class="col-sm-2">
|
||||
<input
|
||||
type="number"
|
||||
id="register-math"
|
||||
class="form-control"
|
||||
value={this.state.mathQuestion.answer}
|
||||
onInput={linkEvent(this, this.handleMathAnswerChange)}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{this.state.enable_nsfw && (
|
||||
<div class="form-group row">
|
||||
<div class="col-sm-10">
|
||||
@ -235,7 +262,11 @@ export class Login extends Component<any, State> {
|
||||
)}
|
||||
<div class="form-group row">
|
||||
<div class="col-sm-10">
|
||||
<button type="submit" class="btn btn-secondary">
|
||||
<button
|
||||
type="submit"
|
||||
class="btn btn-secondary"
|
||||
disabled={this.mathCheck}
|
||||
>
|
||||
{this.state.registerLoading ? (
|
||||
<svg class="icon icon-spinner spin">
|
||||
<use xlinkHref="#icon-spinner"></use>
|
||||
@ -272,7 +303,9 @@ export class Login extends Component<any, State> {
|
||||
i.state.registerLoading = true;
|
||||
i.setState(i.state);
|
||||
|
||||
WebSocketService.Instance.register(i.state.registerForm);
|
||||
if (!i.mathCheck) {
|
||||
WebSocketService.Instance.register(i.state.registerForm);
|
||||
}
|
||||
}
|
||||
|
||||
handleRegisterUsernameChange(i: Login, event: any) {
|
||||
@ -303,6 +336,11 @@ export class Login extends Component<any, State> {
|
||||
i.setState(i.state);
|
||||
}
|
||||
|
||||
handleMathAnswerChange(i: Login, event: any) {
|
||||
i.state.mathQuestion.answer = event.target.value;
|
||||
i.setState(i.state);
|
||||
}
|
||||
|
||||
handlePasswordReset(i: Login) {
|
||||
event.preventDefault();
|
||||
let resetForm: PasswordResetForm = {
|
||||
@ -311,6 +349,13 @@ export class Login extends Component<any, State> {
|
||||
WebSocketService.Instance.passwordReset(resetForm);
|
||||
}
|
||||
|
||||
get mathCheck(): boolean {
|
||||
return (
|
||||
this.state.mathQuestion.answer !=
|
||||
this.state.mathQuestion.a + this.state.mathQuestion.b
|
||||
);
|
||||
}
|
||||
|
||||
parseMessage(msg: WebSocketJsonResponse) {
|
||||
let res = wsJsonToRes(msg);
|
||||
if (msg.error) {
|
||||
|
16
ui/src/components/sponsors.tsx
vendored
16
ui/src/components/sponsors.tsx
vendored
@ -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<SilverUser> = [
|
||||
{
|
||||
name: 'Redjoker',
|
||||
@ -89,9 +91,13 @@ export class Sponsors extends Component<any, any> {
|
||||
{silver.map(s => (
|
||||
<div class="card col-12 col-md-2">
|
||||
<div>
|
||||
<a href={s.link} target="_blank" rel="noopener">
|
||||
💎 {s.name}
|
||||
</a>
|
||||
{s.link ? (
|
||||
<a href={s.link} target="_blank" rel="noopener">
|
||||
💎 {s.name}
|
||||
</a>
|
||||
) : (
|
||||
<div>💎 {s.name}</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
2
ui/src/i18next.ts
vendored
2
ui/src/i18next.ts
vendored
@ -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 {
|
||||
|
4
ui/src/utils.ts
vendored
4
ui/src/utils.ts
vendored
@ -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';
|
||||
}
|
||||
|
2
ui/src/version.ts
vendored
2
ui/src/version.ts
vendored
@ -1 +1 @@
|
||||
export const version: string = 'v0.7.5';
|
||||
export const version: string = 'v0.7.8';
|
||||
|
13
ui/translations/de.json
vendored
13
ui/translations/de.json
vendored
@ -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</1> Software, also ohne Werbung, Monetarisierung oder Venturekapital, Punkt. Deine Spenden gehen direkt an die Vollzeit Entwicklung des Projekts. Vielen Dank an die folgenden Personen:",
|
||||
"sponsor_message": "Lemmy ist freie <1>Open-Source</1> Software, ohne Werbung, Monetarisierung oder Venturekapital, Punkt. Deine Spenden gehen direkt an die Vollzeit Entwicklung des Projekts. Vielen Dank an die folgenden Personen:",
|
||||
"support_on_patreon": "Auf Patreon unterstützen",
|
||||
"support_on_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."
|
||||
}
|
||||
|
10
ui/translations/el.json
vendored
10
ui/translations/el.json
vendored
@ -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</1> για ασφαλή επικοινωνία.",
|
||||
"send_notifications_to_email": "Αποστολή ειδοποιήσεων στη διεύθυνση ηλεκτρονικού ταχυδρομείου",
|
||||
@ -200,7 +200,7 @@
|
||||
"monero": "Monero",
|
||||
"code": "Κώδικας",
|
||||
"by": "από",
|
||||
"to": "μέχρι",
|
||||
"to": "προς",
|
||||
"from": "από",
|
||||
"transfer_community": "μεταφορά κοινότητας",
|
||||
"transfer_site": "μεταφορά ιστότοπου",
|
||||
|
3
ui/translations/en.json
vendored
3
ui/translations/en.json
vendored
@ -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"
|
||||
}
|
||||
|
239
ui/translations/eo.json
vendored
239
ui/translations/eo.json
vendored
@ -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</1>",
|
||||
"trending_communities": "Furora <1>komunumoj</1>",
|
||||
"create_community": "Krei komunumon",
|
||||
"remove_community": "Forigi komunumon",
|
||||
"subscribed_to_communities": "Abonanta <1>komunumojn</1>",
|
||||
"trending_communities": "Furoraj <1>komunumoj</1>",
|
||||
"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}}</1>",
|
||||
"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</1>.",
|
||||
"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</1> programaro. Tio signifas ne reklami, pagigi, aŭ riska kapitalo, ĉiam. Viaj donacoj rekte subtenas plentempan evoluon de la projekto. Dankon al tiuj homoj:",
|
||||
"sponsor_message": "Lemmy estas senpaga, <1>liberkoda</1> programaro, sen reklamoj, pagigado, aŭ riska kapitalo, ĉiam ajn. Viaj donacoj rekte subtenas plentempan evoluigadon de la projekto. Dankon al tiuj homoj:",
|
||||
"support_on_patreon": "Subteni per Patreon",
|
||||
"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</1> / Reddit anstataŭo ke intenciĝas funkci en la <2>federacio-universo</2>.<3></3>ĝi estas mem-gastigebla, havas nuna-ĝisdatigajn komentarojn, kaj estas malgrandega (<4>~80kB</4>). Federacio en la ActivityPub-an reton estas planizita. <5></5>Estas <6>fruega beta versio</6>, kaj multaj trajtoj estas nune difektaj aŭ mankaj. <7></7>Sugestias novajn trajtojn aŭ raportas cimojn <8>ĉi tie.</8><9></9>Faris per <10>Rust</10>, <11>Actix</11>, <12>Inferno</12>, <13>Typescript</13>.",
|
||||
"not_logged_in": "Ne estas ensalutinta.",
|
||||
"powered_by": "Konstruita per",
|
||||
"landing": "Lemmy estas <1>amasigilo de ligiloj</1> / alternativo de Reddit, intencita funkcii en la <2>federuniverso</2>.<3></3>ĝi estas mem-gastigebla, havas tuj-ĝisdatigojn de komentaroj, kaj estas malgrandega (<4>~80kB</4>). Federado en la reto de ActivityPub estas planita. <5></5>Ĉi tio estas <6>tre frua beta-versio</6>, kaj multaj funkcioj estas nune difektaj aŭ mankaj. <7></7>Proponu novajn funkciojn aŭ raportu erarojn <8>ĉi tie.</8><9></9>Konstruita per <10>Rust</10>, <11>Actix</11>, <12>Inferno</12>, <13>Typescript</13>.",
|
||||
"not_logged_in": "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</1> por sekura mesaĝado.",
|
||||
"send_notifications_to_email": "Sendi sciigojn al retpoŝtadreso",
|
||||
"language": "Lingvo",
|
||||
"browser_default": "Laŭ foliumilo",
|
||||
"downvotes_disabled": "Kontraŭvoĉoj malŝaltiĝis",
|
||||
"enable_downvotes": "Ŝalti kontraŭvoĉojn",
|
||||
"open_registration": "Ebligi registradon",
|
||||
"registration_closed": "Registrado malebliĝis",
|
||||
"enable_nsfw": "Ŝalti konsternajn",
|
||||
"support_on_open_collective": "Subteni per OpenCollective",
|
||||
"theme": "Haŭto",
|
||||
"support_on_liberapay": "Subteni per Liberapay",
|
||||
"donate_to_lemmy": "Donaci al Lemmy",
|
||||
"silver_sponsors": "Arĝentaj subtenantoj estas tiuj, kiuj donacis $40 al Lemmy.",
|
||||
"are_you_sure": "ĉu vi certas?",
|
||||
"yes": "jes",
|
||||
"no": "ne",
|
||||
"logged_in": "Salutinta.",
|
||||
"site_saved": "Retejo konserviĝis.",
|
||||
"couldnt_get_comments": "Ne povis akiri la komentojn.",
|
||||
"email_already_exists": "Retpoŝtadreso jam ekzistas.",
|
||||
"couldnt_create_private_message": "Ne povis krei privatan mesaĝon.",
|
||||
"no_private_message_edit_allowed": "Ne rajtas redakti la privatan mesaĝon.",
|
||||
"couldnt_update_private_message": "Ne povis ĝisdatigi la privatan mesaĝon.",
|
||||
"time": "Tempo",
|
||||
"action": "Ago",
|
||||
"emoji_picker": "Elektilo de bildsignoj",
|
||||
"block_leaving": "Ĉu vi certe volas foriri?",
|
||||
"from": "de",
|
||||
"invalid_username": "Nevalida uzantonomo.",
|
||||
"upvote": "Porvoĉi",
|
||||
"number_of_upvotes": "{{count}} porvoĉo",
|
||||
"number_of_upvotes_plural": "{{count}} porvoĉoj",
|
||||
"downvote": "Kontraŭvoĉi",
|
||||
"number_of_downvotes": "{{count}} kontraŭvoĉo",
|
||||
"number_of_downvotes_plural": "{{count}} kontraŭvoĉoj"
|
||||
}
|
||||
|
6
ui/translations/it.json
vendored
6
ui/translations/it.json
vendored
@ -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.",
|
||||
|
108
ui/translations/sq.json
vendored
Normal file
108
ui/translations/sq.json
vendored
Normal file
@ -0,0 +1,108 @@
|
||||
{
|
||||
"remove_post": "Hiqe Postimin",
|
||||
"no_posts": "Nuk ka Postime.",
|
||||
"create_a_post": "Krijo një postim",
|
||||
"create_post": "Krijo Postimin",
|
||||
"posts": "Postime",
|
||||
"related_posts": "Këto postime mund të jenë të lidhura",
|
||||
"cross_posts": "Ky link është postuar edhe te:",
|
||||
"cross_post": "shumë-postim",
|
||||
"cross_posted_to": "shumë-postuar në: ",
|
||||
"comments": "Komentet",
|
||||
"remove_comment": "Fshije Komentin",
|
||||
"communities": "Komunitetet",
|
||||
"users": "Përdoruesit",
|
||||
"create_a_community": "Krijo një komunitet",
|
||||
"select_a_community": "Përzgjedh një komunitet",
|
||||
"create_community": "Krijo komunitetin",
|
||||
"remove_community": "Fshije Komunitetin",
|
||||
"subscribed_to_communities": "I abonuar në",
|
||||
"trending_communities": "Trendi",
|
||||
"list_of_communities": "Lista e komuniteteve",
|
||||
"community_reqs": "gërma të vogla, nënvizim, dhe pa hapësira.",
|
||||
"invalid_community_name": "Emër invalid.",
|
||||
"create_private_message": "Krijo Mesazh Privat",
|
||||
"send_secure_message": "Dërgo Mesazh të Sigurtë",
|
||||
"send_message": "Dërgo Mesazh",
|
||||
"message": "Mesazh",
|
||||
"edit": "redakto",
|
||||
"reply": "përgjigju",
|
||||
"more": "më shumë",
|
||||
"cancel": "Anulo",
|
||||
"preview": "Shiko paraprakisht",
|
||||
"upload_image": "ngarko imazhin",
|
||||
"upload_avatar": "Ngarko foton e profilit",
|
||||
"show_avatars": "Shfaq fotot e profilit",
|
||||
"show_context": "Shfaq kontekstin",
|
||||
"formatting_help": "ndihmë me formatimin",
|
||||
"sorting_help": "ndihmë me radhitjen",
|
||||
"view_source": "shiko origjinën",
|
||||
"unlock": "hape",
|
||||
"lock": "mbyll",
|
||||
"unsticky": "çngjit",
|
||||
"link": "link",
|
||||
"archive_link": "link i arkivuar",
|
||||
"mod": "moderator",
|
||||
"mods": "moderatorët",
|
||||
"settings": "Konfigurimet",
|
||||
"site_config": "Konfigurimet e faqes",
|
||||
"remove_as_mod": "Largoje si moderator",
|
||||
"appoint_as_mod": "emëro si moderator",
|
||||
"modlog": "Ditari i moderimit",
|
||||
"admin": "administrator",
|
||||
"admins": "administratorët",
|
||||
"appoint_as_admin": "emëro si administrator",
|
||||
"remove": "fshije",
|
||||
"removed": "është fshirë nga një moderator",
|
||||
"locked": "mbyllur",
|
||||
"stickied": "ngjitur",
|
||||
"reason": "Arsye",
|
||||
"mark_as_read": "shëno si të lexuar",
|
||||
"mark_as_unread": "shëno si të palexuar",
|
||||
"delete": "fshije",
|
||||
"delete_account": "Fshije Account-in",
|
||||
"click_to_delete_picture": "Shtyp për të fshirë imazhin.",
|
||||
"picture_deleted": "Imazhi është fshirë.",
|
||||
"restore": "rikthe",
|
||||
"ban": "",
|
||||
"ban_from_site": "",
|
||||
"save": "ruaj",
|
||||
"unsave": "anulo ruajtjen",
|
||||
"create": "krijo",
|
||||
"creator": "krijuesi",
|
||||
"username": "Emri virtual",
|
||||
"email_or_username": "Email-i ose Emri virtual",
|
||||
"number_of_users": "{{count}} Përdorues",
|
||||
"number_of_users_plural": "{{count}} Përdoruesa",
|
||||
"number_of_subscribers": "{{count}} i abonuar",
|
||||
"number_of_subscribers_plural": "{{count}} të abonuar",
|
||||
"number_of_points": "{{count}} Pikë",
|
||||
"number_of_points_plural": "{{count}} Pikë",
|
||||
"number_online": "{{count}} Përdorues Online",
|
||||
"number_online_plural": "{{count}} Përdoruesa Online",
|
||||
"name": "Emri",
|
||||
"title": "Titulli",
|
||||
"category": "Kategoria",
|
||||
"subscribers": "Të abonuarit",
|
||||
"both": "Të dy",
|
||||
"saved": "E ruajtur",
|
||||
"subscribe": "Abonohu",
|
||||
"subscribed": "Jeni abonuar",
|
||||
"next": "Tjetra",
|
||||
"post": "postim",
|
||||
"number_of_posts": "{{count}} Postim",
|
||||
"number_of_posts_plural": "{{count}} Postime",
|
||||
"number_of_comments": "{{count}} Koment",
|
||||
"number_of_comments_plural": "{{count}} Komente",
|
||||
"number_of_communities": "{{count}} Komunitet",
|
||||
"number_of_communities_plural": "{{count}} Komunitete",
|
||||
"avatar": "Fotoja e profilit",
|
||||
"sticky": "ngjite",
|
||||
"moderates": "Moderon",
|
||||
"admin_settings": "Konfigurimet administrative",
|
||||
"remove_as_admin": "largoje si administrator",
|
||||
"deleted": "është fshirë nga krijuesi",
|
||||
"delete_account_confirm": "Paralajmërim: kjo do të fshij të gjitha të dhënat e juaja përgjithmonë. Shtyp fjalëkalimin tënd për ta konfirmuar.",
|
||||
"unsubscribe": "Çabonohu",
|
||||
"prev": "E mëparshme"
|
||||
}
|
Loading…
Reference in New Issue
Block a user