Merge branch 'master' into federation

This commit is contained in:
Dessalines 2020-04-02 15:11:11 -04:00
commit 31f835db86
31 changed files with 762 additions and 243 deletions

1
.gitignore vendored
View file

@ -2,7 +2,6 @@
ansible/inventory
ansible/inventory_dev
ansible/passwords/
ansible/vars/
# docker build files
docker/lemmy_mine.hjson

1
.travis.yml vendored
View file

@ -12,7 +12,6 @@ before_cache:
- rm -rfv target/debug/build/lemmy_server-*
- rm -rfv target/debug/deps/lemmy_server-*
- rm -rfv target/debug/lemmy_server.d
- cargo clean
before_script:
- psql -c "create user lemmy with password 'password' superuser;" -U postgres
- psql -c 'create database lemmy with owner lemmy;' -U postgres

50
ansible/lemmy_dev.yml vendored
View file

@ -16,32 +16,6 @@
- setup: # gather facts
tasks:
# TODO: this task is running on all hosts at the same time so there is a race condition
- name: xxx
shell: |
mkdir -p "vars/{{ inventory_hostname }}/"
if [ ! -f "vars/{{ inventory_hostname }}/port_counter" ]; then
if [ -f "vars/max_port_counter" ]; then
MAX_PORT=$(cat vars/max_port_counter)
else
MAX_PORT=8000
fi
OUR_PORT=$(expr $MAX_PORT + 10)
echo $OUR_PORT > "vars/{{ inventory_hostname }}/port_counter"
echo $OUR_PORT > "vars/max_port_counter"
fi
cat "vars/{{ inventory_hostname }}/port_counter"
args:
executable: /bin/bash
delegate_to: localhost
register: lemmy_port
- set_fact: "lemmy_port={{ lemmy_port.stdout_lines[0] }}"
- set_fact: "pictshare_port={{ lemmy_port|int + 1 }}"
- set_fact: "iframely_port={{ lemmy_port|int + 2 }}"
- debug:
msg: "lemmy_port={{ lemmy_port }} pictshare_port={{pictshare_port}} iframely_port={{iframely_port}}"
- name: install dependencies
apt:
pkg: ['nginx', 'docker-compose', 'docker.io', 'certbot', 'python-certbot-nginx']
@ -51,29 +25,25 @@
args:
creates: '/etc/letsencrypt/live/{{domain}}/privkey.pem'
# TODO: need to use different path per domain
- name: create lemmy folder
file: path={{item.path}} state=directory
with_items:
- { path: '/lemmy/{{ domain }}/' }
- { path: '/lemmy/{{ domain }}/volumes/' }
- { path: '/var/cache/lemmy/{{ domain }}/' }
- { path: '/lemmy/' }
- { path: '/lemmy/volumes/' }
- block:
- name: add template files
template: src={{item.src}} dest={{item.dest}} mode={{item.mode}}
with_items:
- { src: 'templates/docker-compose.yml', dest: '/lemmy/{{domain}}/docker-compose.yml', mode: '0600' }
- { src: 'templates/nginx.conf', dest: '/etc/nginx/sites-enabled/lemmy-{{ domain }}.conf', mode: '0644' }
- { src: '../docker/iframely.config.local.js', dest: '/lemmy/{{ domain }}/iframely.config.local.js', mode: '0600' }
- { 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' }
- name: add config file (only during initial setup)
template: src='templates/config.hjson' dest='/lemmy/{{domain}}/lemmy.hjson' mode='0600' force='no' owner='1000' group='1000'
template: src='templates/config.hjson' dest='/lemmy/lemmy.hjson' mode='0600' force='no' owner='1000' group='1000'
vars:
# TODO: these paths are changed, need to move the files
# TODO: not sure what to call the local var folder, its not mentioned in the ansible docs
postgres_password: "{{ lookup('password', 'vars/{{ inventory_hostname }}/postgres_password chars=ascii_letters,digits') }}"
jwt_password: "{{ lookup('password', 'vars/{{ inventory_hostname }}/jwt_password chars=ascii_letters,digits') }}"
postgres_password: "{{ lookup('password', 'passwords/{{ inventory_hostname }}/postgres chars=ascii_letters,digits') }}"
jwt_password: "{{ lookup('password', 'passwords/{{ inventory_hostname }}/jwt chars=ascii_letters,digits') }}"
- name: build the dev docker image
local_action: shell cd .. && sudo docker build . -f docker/dev/Dockerfile -t lemmy:dev
@ -115,7 +85,7 @@
# be a problem for testing
- name: start docker-compose
docker_compose:
project_src: "/lemmy/{{ domain }}/"
project_src: /lemmy/
state: present
recreate: always
ignore_errors: yes
@ -126,6 +96,6 @@
- name: certbot renewal cronjob
cron:
special_time=daily
name=certbot-renew-lemmy-{{ domain }}
name=certbot-renew-lemmy
user=root
job="certbot certonly --nginx -d '{{ domain }}' --deploy-hook 'nginx -s reload'"

View file

@ -4,7 +4,7 @@ services:
lemmy:
image: {{ lemmy_docker_image }}
ports:
- "127.0.0.1:{{ lemmy_port }}:8536"
- "127.0.0.1:8536:8536"
restart: always
environment:
- RUST_LOG=error
@ -28,7 +28,7 @@ services:
pictshare:
image: shtripok/pictshare:latest
ports:
- "127.0.0.1:{{ pictshare_port }}:80"
- "127.0.0.1:8537:80"
volumes:
- ./volumes/pictshare:/usr/share/nginx/html/data
restart: always
@ -36,7 +36,7 @@ services:
iframely:
image: dogbin/iframely:latest
ports:
- "127.0.0.1:{{ iframely_port }}:80"
- "127.0.0.1:8061:80"
volumes:
- ./iframely.config.local.js:/iframely/config.local.js:ro
restart: always

View file

@ -1,4 +1,4 @@
proxy_cache_path /var/cache/lemmy/{{ domain }} levels=1:2 keys_zone=lemmy_frontend_cache_{{ domain }}:10m max_size=100m use_temp_path=off;
proxy_cache_path /var/cache/lemmy_frontend levels=1:2 keys_zone=lemmy_frontend_cache:10m max_size=100m use_temp_path=off;
server {
listen 80;
@ -52,7 +52,7 @@ server {
client_max_body_size 50M;
location / {
proxy_pass http://0.0.0.0:{{ lemmy_port }};
proxy_pass http://0.0.0.0:8536;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
@ -63,7 +63,7 @@ server {
proxy_set_header Connection "upgrade";
# Proxy Cache
proxy_cache lemmy_frontend_cache_{{ domain }};
proxy_cache lemmy_frontend_cache;
proxy_cache_use_stale error timeout http_500 http_502 http_503 http_504;
proxy_cache_revalidate on;
proxy_cache_lock on;
@ -71,7 +71,7 @@ server {
}
location /pictshare/ {
proxy_pass http://0.0.0.0:{{ pictshare_port }}/;
proxy_pass http://0.0.0.0:8537/;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
@ -82,7 +82,7 @@ server {
}
location /iframely/ {
proxy_pass http://0.0.0.0:{{ iframely_port }}/;
proxy_pass http://0.0.0.0:8061/;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
@ -98,6 +98,6 @@ map $remote_addr $remote_addr_anon {
::1 $remote_addr;
default 0.0.0.0;
}
log_format main_{{ domain }} '$remote_addr_anon - $remote_user [$time_local] "$request" '
log_format main '$remote_addr_anon - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" "$http_user_agent"';
access_log /var/log/nginx/access.log main_{{ domain }};
access_log /var/log/nginx/access.log main;

16
ansible/uninstall.yml vendored
View file

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

11
docker/lemmy.hjson vendored
View file

@ -41,7 +41,16 @@
# interval length for registration limit
register_per_second: 3600
}
# # email sending configuration
# # optional: parameters for automatic configuration of new instance (only used at first start)
# setup: {
# # username for the admin user
# admin_username: "lemmy"
# # password for the admin user
# admin_password: "lemmy"
# # name of the site (can be changed later)
# site_name: "Lemmy Test"
# }
# # optional: email sending configuration
# email: {
# # hostname of the smtp server
# smtp_server: ""

1
docs/src/SUMMARY.md vendored
View file

@ -13,6 +13,7 @@
- [Contributing](contributing.md)
- [Docker Development](contributing_docker_development.md)
- [Local Development](contributing_local_development.md)
- [Federation Development](contributing_federation_development.md)
- [Websocket/HTTP API](contributing_websocket_http_api.md)
- [ActivityPub API Outline](contributing_apub_api_outline.md)
- [Theming Guide](contributing_theming.md)

View file

@ -50,3 +50,4 @@
- [Federation.md](https://github.com/dariusk/gathio/blob/7fc93dbe9d4d99457a0e85c6c532112f415b7af2/FEDERATION.md)
- [Activitypub implementers guide](https://socialhub.activitypub.rocks/t/draft-guide-for-new-activitypub-implementers/479)
- [Data storage questions](https://socialhub.activitypub.rocks/t/data-storage-questions/579/3)
- [Activitypub as it has been understood](https://flak.tedunangst.com/post/ActivityPub-as-it-has-been-understood)

View file

@ -1,5 +1,7 @@
# Ansible Installation
This is the same as the [Docker installation](administration_install_docker.md), except that Ansible handles all of it automatically. It also does some extra things like setting up TLS and email for your Lemmy instance.
First, you need to [install Ansible on your local computer](https://docs.ansible.com/ansible/latest/installation_guide/intro_installation.html) (e.g. using `sudo apt install ansible`) or the equivalent for you platform.
Then run the following commands on your local computer:
@ -11,3 +13,10 @@ cp inventory.example inventory
nano inventory # enter your server, domain, contact email
ansible-playbook lemmy.yml --become
```
To update to a new version, just run the following in your local Lemmy repo:
```bash
git pull origin master
cd ansible
ansible-playbook lemmy.yml --become
```

View file

@ -1,29 +1,33 @@
# Docker Installation
Make sure you have both docker and docker-compose(>=`1.24.0`) installed:
Make sure you have both docker and docker-compose(>=`1.24.0`) installed. On Ubuntu, just run `apt install docker-compose docker.io`. Next,
```bash
mkdir lemmy/
cd lemmy/
# create a folder for the lemmy files. the location doesnt matter, you can put this anywhere you want
mkdir /lemmy
cd /lemmy
# download default config files
wget https://raw.githubusercontent.com/dessalines/lemmy/master/docker/prod/docker-compose.yml
wget https://raw.githubusercontent.com/dessalines/lemmy/master/docker/lemmy.hjson
wget https://raw.githubusercontent.com/dessalines/lemmy/master/docker/iframely.config.local.js
# Edit lemmy.hjson, and docker-compose.yml to do more configuration (like adding a custom password)
docker-compose up -d
```
and go to http://localhost:8536.
After this, have a look at the [config file](administration_configuration.md) named `lemmy.hjson`, and adjust it, in particular the hostname.
[A sample nginx config](/ansible/templates/nginx.conf) (Note: Avatar / Image uploading won't work without this), could be setup with:
To make Lemmy available outside the server, you need to setup a reverse proxy, like Nginx. [A sample nginx config](/ansible/templates/nginx.conf), could be setup with:
```bash
wget https://raw.githubusercontent.com/dessalines/lemmy/master/ansible/templates/nginx.conf
# Replace the {{ vars }}
sudo mv nginx.conf /etc/nginx/sites-enabled/lemmy.conf
```
You will also need to setup TLS, for example with [Let's Encrypt](https://letsencrypt.org/). After this you need to restart Nginx to reload the config.
## Updating
To update to the newest version, run:
To update to the newest version, you can manually change the version in `docker-compose.yml`. Alternatively, fetch the latest version from our git repo:
```bash
wget https://raw.githubusercontent.com/dessalines/lemmy/master/docker/prod/docker-compose.yml

View file

@ -0,0 +1,37 @@
# Federation Development
## Setup
If you don't have a local clone of the Lemmy repo yet, just run the following command:
```bash
git clone https://yerbamate.dev/nutomic/lemmy.git -b federation
```
If you already have the Lemmy repo cloned, you need to add a new remote:
```bash
git remote add federation https://yerbamate.dev/nutomic/lemmy.git
git checkout federation
git pull federation federation
```
## Running
You need to have the following packages installed, the Docker service needs to be running.
- docker
- docker-compose
- cargo
- yarn
Then run the following
```bash
cd dev/federation-test
./run-federation-test.bash
```
After the build is finished and the docker-compose setup is running, open [127.0.0.1:8540](http://127.0.0.1:8540) and
[127.0.0.1:8541](http://127.0.0.1:8541) in your browser to use the test instances. You can login as admin with
username `lemmy` and password `lemmy`, or create new accounts.
Please get in touch if you want to contribute to this, so we can coordinate things and avoid duplicate work.

253
server/Cargo.lock generated vendored
View file

@ -2,9 +2,9 @@
# It is not intended for manual editing.
[[package]]
name = "activitystreams"
version = "0.5.0-alpha.11"
version = "0.5.0-alpha.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0fb876395ae7a1dd1c7d7de38f2cdb583918db16b46ee5b75d2e9bf7af1ef9f"
checksum = "e7173513c9d586a1157f375835777e3b50498b6b7aab4411a7098b455ba995f0"
dependencies = [
"activitystreams-derive",
"chrono",
@ -17,9 +17,9 @@ dependencies = [
[[package]]
name = "activitystreams-derive"
version = "0.5.0-alpha.4"
version = "0.5.0-alpha.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2bc640808dceb2efac81e6bcb77a7f4e2e76af7fb60e88f966b48123b625d2f"
checksum = "c7ff4a2be3b67d763e78794f622ef2d53da077521229774837f61963c4067b36"
dependencies = [
"proc-macro2",
"quote",
@ -46,7 +46,7 @@ dependencies = [
"pin-project",
"smallvec",
"tokio",
"tokio-util",
"tokio-util 0.2.0",
"trust-dns-proto",
"trust-dns-resolver",
]
@ -63,7 +63,7 @@ dependencies = [
"futures-sink",
"log",
"tokio",
"tokio-util",
"tokio-util 0.2.0",
]
[[package]]
@ -378,6 +378,15 @@ dependencies = [
"memchr 2.3.3",
]
[[package]]
name = "ansi_term"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b"
dependencies = [
"winapi 0.3.8",
]
[[package]]
name = "arc-swap"
version = "0.4.5"
@ -401,9 +410,9 @@ checksum = "71938f30533e4d95a6d17aa530939da3842c2ab6f4f84b9dae68447e4129f74a"
[[package]]
name = "async-trait"
version = "0.1.26"
version = "0.1.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "21a03abb7c9b93ae229356151a083d26218c0358866a2a59d4280c856e9482e6"
checksum = "bab5c215748dc1ad11a145359b1067107ae0f8ca5e99844fa64067ed5bf198e3"
dependencies = [
"proc-macro2",
"quote",
@ -621,9 +630,9 @@ checksum = "130aac562c0dd69c56b3b1cc8ffd2e17be31d0b6c25b61c96b76231aa23e39e1"
[[package]]
name = "bytestring"
version = "0.1.4"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc267467f58ef6cc8874064c62a0423eb0d099362c8a23edd1c6d044f46eead4"
checksum = "fc7c05fa5172da78a62d9949d662d2ac89d4cc7355d7b49adee5163f1fb3f363"
dependencies = [
"bytes",
]
@ -652,6 +661,21 @@ dependencies = [
"time",
]
[[package]]
name = "clap"
version = "2.33.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5067f5bb2d80ef5d68b4c87db81601f0b75bca627bc2ef76b141d7b846a3c6d9"
dependencies = [
"ansi_term",
"atty",
"bitflags",
"strsim 0.8.0",
"textwrap",
"unicode-width",
"vec_map",
]
[[package]]
name = "cloudabi"
version = "0.0.3"
@ -661,6 +685,23 @@ dependencies = [
"bitflags",
]
[[package]]
name = "comrak"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e17058cc536cf290563e88787d7b9e6030ce4742943017cc2ffb71f88034021c"
dependencies = [
"clap",
"entities",
"lazy_static 1.4.0",
"pest",
"pest_derive",
"regex 1.3.6",
"twoway",
"typed-arena",
"unicode_categories",
]
[[package]]
name = "config"
version = "0.10.1"
@ -999,6 +1040,12 @@ dependencies = [
"cfg-if",
]
[[package]]
name = "entities"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5320ae4c3782150d900b79807611a59a99fc9a1d61d686faafc24b93fc8d7ca"
[[package]]
name = "enum-as-inner"
version = "0.3.2"
@ -1238,9 +1285,9 @@ dependencies = [
[[package]]
name = "h2"
version = "0.2.3"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7938e6aa2a31df4e21f224dc84704bd31c089a6d1355c535b03667371cccc843"
checksum = "377038bf3c89d18d6ca1431e7a5027194fbd724ca10592b9487ede5e8e144f42"
dependencies = [
"bytes",
"fnv",
@ -1252,7 +1299,7 @@ dependencies = [
"log",
"slab",
"tokio",
"tokio-util",
"tokio-util 0.3.1",
]
[[package]]
@ -1266,9 +1313,9 @@ dependencies = [
[[package]]
name = "hermit-abi"
version = "0.1.8"
version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1010591b26bbfe835e9faeabeb11866061cc7dcebffd56ad7d0942d0e61aefd8"
checksum = "725cf19794cf90aa94e65050cb4191ff5d8fa87a498383774c47b332e3af952e"
dependencies = [
"libc",
]
@ -1483,6 +1530,7 @@ dependencies = [
"actix-web-actors",
"bcrypt",
"chrono",
"comrak",
"config",
"diesel",
"diesel_migrations",
@ -1625,6 +1673,12 @@ dependencies = [
"linked-hash-map 0.5.2",
]
[[package]]
name = "maplit"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d"
[[package]]
name = "match_cfg"
version = "0.1.0"
@ -1931,6 +1985,49 @@ version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e"
[[package]]
name = "pest"
version = "2.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "10f4872ae94d7b90ae48754df22fd42ad52ce740b8f370b03da4835417403e53"
dependencies = [
"ucd-trie",
]
[[package]]
name = "pest_derive"
version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "833d1ae558dc601e9a60366421196a8d94bc0ac980476d0b67e1d0988d72b2d0"
dependencies = [
"pest",
"pest_generator",
]
[[package]]
name = "pest_generator"
version = "2.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "99b8db626e31e5b81787b9783425769681b347011cc59471e33ea46d2ea0cf55"
dependencies = [
"pest",
"pest_meta",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "pest_meta"
version = "2.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "54be6e404f5317079812fc8f9f5279de376d8856929e21c184ecf6bbd692a11d"
dependencies = [
"maplit",
"pest",
"sha-1",
]
[[package]]
name = "pin-project"
version = "0.4.8"
@ -1986,9 +2083,9 @@ dependencies = [
[[package]]
name = "proc-macro-hack"
version = "0.5.14"
version = "0.5.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fcfdefadc3d57ca21cf17990a28ef4c0f7c61383a28cb7604cf4a18e6ede1420"
checksum = "0d659fe7c6d27f25e9d80a1a094c223f5246f6a6596453e09d7229bf42750b63"
[[package]]
name = "proc-macro-nested"
@ -1998,9 +2095,9 @@ checksum = "8e946095f9d3ed29ec38de908c22f95d9ac008e424c7bcae54c75a79c527c694"
[[package]]
name = "proc-macro2"
version = "1.0.9"
version = "1.0.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c09721c6781493a2a492a96b5a5bf19b65917fe6728884e7c44dd0c60ca3435"
checksum = "df246d292ff63439fea9bc8c0a270bed0e390d5ebd4db4ba15aba81111b5abe3"
dependencies = [
"unicode-xid",
]
@ -2363,21 +2460,22 @@ checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
[[package]]
name = "security-framework"
version = "0.4.1"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97bbedbe81904398b6ebb054b3e912f99d55807125790f3198ac990d98def5b0"
checksum = "572dfa3a0785509e7a44b5b4bebcf94d41ba34e9ed9eb9df722545c3b3c4144a"
dependencies = [
"bitflags",
"core-foundation",
"core-foundation-sys",
"libc",
"security-framework-sys",
]
[[package]]
name = "security-framework-sys"
version = "0.4.1"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06fd2f23e31ef68dd2328cc383bd493142e46107a3a0e24f7d734e3f3b80fe4c"
checksum = "8ddb15a5fec93b7021b8a9e96009c5d8d51c15673569f7c0f6b7204e5b7b404f"
dependencies = [
"core-foundation-sys",
"libc",
@ -2495,6 +2593,18 @@ dependencies = [
"url",
]
[[package]]
name = "sha-1"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f7d94d0bede923b3cea61f3f1ff57ff8cdfd77b400fb8f9998949e0cf04163df"
dependencies = [
"block-buffer",
"digest",
"fake-simd",
"opaque-debug",
]
[[package]]
name = "sha1"
version = "0.6.0"
@ -2560,9 +2670,9 @@ checksum = "5c2fb2ec9bcd216a5b0d0ccf31ab17b5ed1d627960edff65bbe95d3ce221cefc"
[[package]]
name = "socket2"
version = "0.3.11"
version = "0.3.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8b74de517221a2cb01a53349cf54182acdc31a074727d3079068448c0676d85"
checksum = "03088793f677dce356f3ccc2edb1b314ad191ab702a5de3faf49304f7e104918"
dependencies = [
"cfg-if",
"libc",
@ -2588,6 +2698,12 @@ version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67f84c44fbb2f91db7fef94554e6b2ac05909c9c0b0bc23bb98d3a1aebfe7f7c"
[[package]]
name = "strsim"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a"
[[package]]
name = "strsim"
version = "0.9.3"
@ -2659,19 +2775,28 @@ dependencies = [
]
[[package]]
name = "thiserror"
version = "1.0.13"
name = "textwrap"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3711fd1c4e75b3eff12ba5c40dba762b6b65c5476e8174c1a664772060c49bf"
checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060"
dependencies = [
"unicode-width",
]
[[package]]
name = "thiserror"
version = "1.0.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0570dc61221295909abdb95c739f2e74325e14293b2026b0a7e195091ec54ae"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "1.0.13"
version = "1.0.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae2b85ba4c9aa32dd3343bd80eb8d22e9b54b7688c17ea3907f236885353b233"
checksum = "227362df41d566be41a28f64401e07a043157c21c14b9785a0d8e256f940a8fd"
dependencies = [
"proc-macro2",
"quote",
@ -2728,9 +2853,9 @@ dependencies = [
[[package]]
name = "tokio"
version = "0.2.13"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fa5e81d6bc4e67fe889d5783bd2a128ab2e0cfa487e0be16b6a8d177b101616"
checksum = "619cdb2245c40c42d563089b72e80c5df659513d667a017598439ef7a7b1ffe1"
dependencies = [
"bytes",
"fnv",
@ -2761,6 +2886,20 @@ dependencies = [
"tokio",
]
[[package]]
name = "tokio-util"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "be8242891f2b6cbef26a2d7e8605133c2c554cd35b3e4948ea892d6d68436499"
dependencies = [
"bytes",
"futures-core",
"futures-sink",
"log",
"pin-project-lite",
"tokio",
]
[[package]]
name = "toml"
version = "0.5.6"
@ -2809,12 +2948,40 @@ dependencies = [
"trust-dns-proto",
]
[[package]]
name = "twoway"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6b40075910de3a912adbd80b5d8bad6ad10a23eeb1f5bf9d4006839e899ba5bc"
dependencies = [
"memchr 2.3.3",
"unchecked-index",
]
[[package]]
name = "typed-arena"
version = "1.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a9b2228007eba4120145f785df0f6c92ea538f5a3635a612ecf4e334c8c1446d"
[[package]]
name = "typenum"
version = "1.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6d2783fe2d6b8c1101136184eb41be8b1ad379e4657050b8aaff0c79ee7575f9"
[[package]]
name = "ucd-trie"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56dee185309b50d1f11bfedef0fe6d036842e3fb77413abef29f8f8d1c5d4c1c"
[[package]]
name = "unchecked-index"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eeba86d422ce181a719445e51872fa30f1f7413b62becb52e95ec91aa262d85c"
[[package]]
name = "unicase"
version = "2.6.0"
@ -2848,12 +3015,24 @@ version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e83e153d1053cbb5a118eeff7fd5be06ed99153f00dbcd8ae310c5fb2b22edc0"
[[package]]
name = "unicode-width"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "caaa9d531767d1ff2150b9332433f32a24622147e5ebb1f26409d5da67afd479"
[[package]]
name = "unicode-xid"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "826e7639553986605ec5979c7dd957c7895e93eabed50ab2ffa7f6128a75097c"
[[package]]
name = "unicode_categories"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e"
[[package]]
name = "untrusted"
version = "0.7.0"
@ -2923,6 +3102,12 @@ version = "0.2.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3fc439f2794e98976c88a2a2dafce96b930fe8010b0a256b3c2199a773933168"
[[package]]
name = "vec_map"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05c78687fb1a80548ae3250346c3db86a80a7cdd77bda190189f2d0a0987c81a"
[[package]]
name = "version_check"
version = "0.1.5"
@ -3041,9 +3226,9 @@ checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
[[package]]
name = "winapi-util"
version = "0.1.3"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ccfbf554c6ad11084fb7517daca16cfdcaccbdadba4fc336f032a8b12c2ad80"
checksum = "fa515c5163a99cc82bab70fd3bfdd36d827be85de63737b40fcef2ce084a436e"
dependencies = [
"winapi 0.3.8",
]

3
server/Cargo.toml vendored
View file

@ -8,7 +8,7 @@ edition = "2018"
diesel = { version = "1.4.2", features = ["postgres","chrono", "r2d2", "64-column-tables"] }
diesel_migrations = "1.4.0"
dotenv = "0.15.0"
activitystreams = "0.5.0-alpha.10"
activitystreams = "0.5.0-alpha.16"
bcrypt = "0.6.2"
chrono = { version = "0.4.7", features = ["serde"] }
failure = "0.1.5"
@ -37,3 +37,4 @@ hjson = "0.8.2"
url = "2.1.1"
percent-encoding = "2.1.0"
isahc = "0.9"
comrak = "0.7"

View file

@ -1,4 +1,15 @@
{
# # optional: parameters for automatic configuration of new instance (only used at first start)
# setup: {
# # username for the admin user
# admin_username: ""
# # password for the admin user
# admin_password: ""
# # optional: email for the admin user (can be omitted and set later through the website)
# admin_email: ""
# # name of the site (can be changed later)
# site_name: ""
# }
# settings related to the postgresql database
database: {
# username to connect to postgres

View file

@ -1,5 +1,9 @@
use super::*;
use crate::api::user::Register;
use crate::api::{Oper, Perform};
use crate::settings::Settings;
use diesel::PgConnection;
use log::info;
use std::str::FromStr;
#[derive(Serialize, Deserialize)]
@ -53,12 +57,12 @@ pub struct GetModlogResponse {
#[derive(Serialize, Deserialize)]
pub struct CreateSite {
name: String,
description: Option<String>,
enable_downvotes: bool,
open_registration: bool,
enable_nsfw: bool,
auth: String,
pub name: String,
pub description: Option<String>,
pub enable_downvotes: bool,
pub open_registration: bool,
pub enable_nsfw: bool,
pub auth: String,
}
#[derive(Serialize, Deserialize)]
@ -277,10 +281,34 @@ impl Perform<GetSiteResponse> for Oper<GetSite> {
fn perform(&self, conn: &PgConnection) -> Result<GetSiteResponse, Error> {
let _data: &GetSite = &self.data;
// It can return a null site in order to redirect
let site_view = match Site::read(&conn, 1) {
Ok(_site) => Some(SiteView::read(&conn)?),
Err(_e) => None,
let site = Site::read(&conn, 1);
let site_view = if site.is_ok() {
Some(SiteView::read(&conn)?)
} else if let Some(setup) = Settings::get().setup.as_ref() {
let register = Register {
username: setup.admin_username.to_owned(),
email: setup.admin_email.to_owned(),
password: setup.admin_password.to_owned(),
password_verify: setup.admin_password.to_owned(),
admin: true,
show_nsfw: true,
};
let login_response = Oper::new(register).perform(&conn)?;
info!("Admin {} created", setup.admin_username);
let create_site = CreateSite {
name: setup.site_name.to_owned(),
description: None,
enable_downvotes: false,
open_registration: false,
enable_nsfw: false,
auth: login_response.jwt,
};
Oper::new(create_site).perform(&conn)?;
info!("Site {} created", setup.site_name);
Some(SiteView::read(&conn)?)
} else {
None
};
let mut admins = UserView::admins(&conn)?;

View file

@ -14,12 +14,12 @@ pub struct Login {
#[derive(Serialize, Deserialize)]
pub struct Register {
username: String,
email: Option<String>,
password: String,
password_verify: String,
admin: bool,
show_nsfw: bool,
pub username: String,
pub email: Option<String>,
pub password: String,
pub password_verify: String,
pub admin: bool,
pub show_nsfw: bool,
}
#[derive(Serialize, Deserialize)]
@ -42,7 +42,7 @@ pub struct SaveUserSettings {
#[derive(Serialize, Deserialize)]
pub struct LoginResponse {
jwt: String,
pub jwt: String,
}
#[derive(Serialize, Deserialize)]

View file

@ -107,7 +107,7 @@ pub async fn get_apub_community_outbox(
.set_id(base_url)?;
collection
.collection_props
.set_many_items_object_boxs(
.set_many_items_base_boxes(
community_posts
.iter()
.map(|c| c.as_page().unwrap())

View file

@ -9,8 +9,8 @@ use crate::settings::Settings;
use activitystreams::actor::{properties::ApActorProperties, Group};
use activitystreams::collection::{OrderedCollection, UnorderedCollection};
use activitystreams::ext::Ext;
use activitystreams::object::ObjectBox;
use activitystreams::object::Page;
use activitystreams::BaseBox;
use failure::Error;
use isahc::prelude::*;
use log::warn;
@ -75,11 +75,11 @@ pub fn get_remote_community_posts(identifier: &str) -> Result<GetPostsResponse,
fetch_remote_object::<Ext<Group, ApActorProperties>>(&get_remote_community_uri(identifier))?;
let outbox_uri = &community.extension.get_outbox().to_string();
let outbox = fetch_remote_object::<OrderedCollection>(outbox_uri)?;
let items = outbox.collection_props.get_many_items_object_boxs();
let items = outbox.collection_props.get_many_items_base_boxes();
let posts: Vec<PostView> = items
.unwrap()
.map(|obox: &ObjectBox| {
.map(|obox: &BaseBox| {
let page: Page = obox.clone().to_concrete::<Page>().unwrap();
PostView {
id: -1,

View file

@ -1,4 +1,3 @@
extern crate lazy_static;
use crate::settings::Settings;
use diesel::dsl::*;
use diesel::result::Error;

View file

@ -11,6 +11,7 @@ pub extern crate actix;
pub extern crate actix_web;
pub extern crate bcrypt;
pub extern crate chrono;
pub extern crate comrak;
pub extern crate dotenv;
pub extern crate jsonwebtoken;
pub extern crate lettre;
@ -18,6 +19,7 @@ pub extern crate lettre_email;
extern crate log;
pub extern crate rand;
pub extern crate regex;
pub extern crate rss;
pub extern crate serde;
pub extern crate serde_json;
pub extern crate sha2;
@ -220,6 +222,10 @@ fn fetch_iframely_and_pictshare_data(
)
}
pub fn markdown_to_html(text: &str) -> String {
comrak::markdown_to_html(text, &comrak::ComrakOptions::default())
}
#[cfg(test)]
mod tests {
use crate::{extract_usernames, is_email_regex, remove_slurs, slur_check, slurs_vec_to_str};

View file

@ -1,5 +1,3 @@
extern crate rss;
use super::*;
use crate::db::comment_view::{ReplyQueryBuilder, ReplyView};
use crate::db::community::Community;
@ -8,9 +6,9 @@ use crate::db::site_view::SiteView;
use crate::db::user::{Claims, User_};
use crate::db::user_mention_view::{UserMentionQueryBuilder, UserMentionView};
use crate::db::{ListingType, SortType};
use crate::Settings;
use crate::{markdown_to_html, Settings};
use actix_web::{web, HttpResponse, Result};
use chrono::{DateTime, Utc};
use chrono::{DateTime, NaiveDateTime, Utc};
use diesel::r2d2::{ConnectionManager, Pool};
use diesel::PgConnection;
use failure::Error;
@ -34,7 +32,6 @@ enum RequestType {
pub fn config(cfg: &mut web::ServiceConfig) {
cfg
.route("/feeds/{type}/{name}.xml", web::get().to(feeds::get_feed))
.route("/feeds/all.xml", web::get().to(feeds::get_all_feed))
.route("/feeds/all.xml", web::get().to(feeds::get_all_feed));
}
@ -44,9 +41,7 @@ async fn get_all_feed(
) -> Result<HttpResponse, actix_web::Error> {
let res = web::block(move || {
let conn = db.get()?;
let sort_type = get_sort_type(info)?;
get_feed_all_data(&conn, &sort_type)
get_feed_all_data(&conn, &get_sort_type(info)?)
})
.await
.map(|rss| {
@ -58,6 +53,29 @@ async fn get_all_feed(
Ok(res)
}
fn get_feed_all_data(conn: &PgConnection, sort_type: &SortType) -> Result<String, failure::Error> {
let site_view = SiteView::read(&conn)?;
let posts = PostQueryBuilder::create(&conn)
.listing_type(ListingType::All)
.sort(sort_type)
.list()?;
let items = create_post_items(posts);
let mut channel_builder = ChannelBuilder::default();
channel_builder
.title(&format!("{} - All", site_view.name))
.link(format!("https://{}", Settings::get().hostname))
.items(items);
if let Some(site_desc) = site_view.description {
channel_builder.description(&site_desc);
}
Ok(channel_builder.build().unwrap().to_string())
}
async fn get_feed(
path: web::Path<(String, String)>,
info: web::Query<Params>,
@ -86,6 +104,7 @@ async fn get_feed(
}
})
.await
.map(|builder| builder.build().unwrap().to_string())
.map(|rss| {
HttpResponse::Ok()
.content_type("application/rss+xml")
@ -103,34 +122,11 @@ fn get_sort_type(info: web::Query<Params>) -> Result<SortType, ParseError> {
SortType::from_str(&sort_query)
}
fn get_feed_all_data(conn: &PgConnection, sort_type: &SortType) -> Result<String, failure::Error> {
let site_view = SiteView::read(&conn)?;
let posts = PostQueryBuilder::create(&conn)
.listing_type(ListingType::All)
.sort(sort_type)
.list()?;
let items = create_post_items(posts);
let mut channel_builder = ChannelBuilder::default();
channel_builder
.title(&format!("{} - All", site_view.name))
.link(format!("https://{}", Settings::get().hostname))
.items(items);
if let Some(site_desc) = site_view.description {
channel_builder.description(&site_desc);
}
Ok(channel_builder.build().unwrap().to_string())
}
fn get_feed_user(
conn: &PgConnection,
sort_type: &SortType,
user_name: String,
) -> Result<String, Error> {
) -> Result<ChannelBuilder, Error> {
let site_view = SiteView::read(&conn)?;
let user = User_::find_by_username(&conn, &user_name)?;
let user_url = user.get_profile_url();
@ -149,14 +145,14 @@ fn get_feed_user(
.link(user_url)
.items(items);
Ok(channel_builder.build().unwrap().to_string())
Ok(channel_builder)
}
fn get_feed_community(
conn: &PgConnection,
sort_type: &SortType,
community_name: String,
) -> Result<String, Error> {
) -> Result<ChannelBuilder, Error> {
let site_view = SiteView::read(&conn)?;
let community = Community::read_from_name(&conn, community_name)?;
let community_url = community.get_url();
@ -179,10 +175,14 @@ fn get_feed_community(
channel_builder.description(&community_desc);
}
Ok(channel_builder.build().unwrap().to_string())
Ok(channel_builder)
}
fn get_feed_front(conn: &PgConnection, sort_type: &SortType, jwt: String) -> Result<String, Error> {
fn get_feed_front(
conn: &PgConnection,
sort_type: &SortType,
jwt: String,
) -> Result<ChannelBuilder, Error> {
let site_view = SiteView::read(&conn)?;
let user_id = Claims::decode(&jwt)?.claims.id;
@ -204,10 +204,10 @@ fn get_feed_front(conn: &PgConnection, sort_type: &SortType, jwt: String) -> Res
channel_builder.description(&site_desc);
}
Ok(channel_builder.build().unwrap().to_string())
Ok(channel_builder)
}
fn get_feed_inbox(conn: &PgConnection, jwt: String) -> Result<String, Error> {
fn get_feed_inbox(conn: &PgConnection, jwt: String) -> Result<ChannelBuilder, Error> {
let site_view = SiteView::read(&conn)?;
let user_id = Claims::decode(&jwt)?.claims.id;
@ -233,86 +233,61 @@ fn get_feed_inbox(conn: &PgConnection, jwt: String) -> Result<String, Error> {
channel_builder.description(&site_desc);
}
Ok(channel_builder.build().unwrap().to_string())
Ok(channel_builder)
}
fn create_reply_and_mention_items(
replies: Vec<ReplyView>,
mentions: Vec<UserMentionView>,
) -> Vec<Item> {
let mut items: Vec<Item> = Vec::new();
let mut reply_items: Vec<Item> = replies
.iter()
.map(|r| {
let reply_url = format!(
"https://{}/post/{}/comment/{}",
Settings::get().hostname,
r.post_id,
r.id
);
build_item(&r.creator_name, &r.published, &reply_url, &r.content)
})
.collect();
for r in replies {
let mut i = ItemBuilder::default();
let mut mention_items: Vec<Item> = mentions
.iter()
.map(|m| {
let mention_url = format!(
"https://{}/post/{}/comment/{}",
Settings::get().hostname,
m.post_id,
m.id
);
build_item(&m.creator_name, &m.published, &mention_url, &m.content)
})
.collect();
i.title(format!("Reply from {}", r.creator_name));
reply_items.append(&mut mention_items);
reply_items
}
let author_url = format!("https://{}/u/{}", Settings::get().hostname, r.creator_name);
i.author(format!(
"/u/{} <a href=\"{}\">(link)</a>",
r.creator_name, author_url
));
let dt = DateTime::<Utc>::from_utc(r.published, Utc);
i.pub_date(dt.to_rfc2822());
let reply_url = format!(
"https://{}/post/{}/comment/{}",
Settings::get().hostname,
r.post_id,
r.id
);
i.comments(reply_url.to_owned());
let guid = GuidBuilder::default()
.permalink(true)
.value(&reply_url)
.build();
i.guid(guid.unwrap());
i.link(reply_url);
// TODO find a markdown to html parser here, do images, etc
i.description(r.content);
items.push(i.build().unwrap());
}
for m in mentions {
let mut i = ItemBuilder::default();
i.title(format!("Mention from {}", m.creator_name));
let author_url = format!("https://{}/u/{}", Settings::get().hostname, m.creator_name);
i.author(format!(
"/u/{} <a href=\"{}\">(link)</a>",
m.creator_name, author_url
));
let dt = DateTime::<Utc>::from_utc(m.published, Utc);
i.pub_date(dt.to_rfc2822());
let mention_url = format!(
"https://{}/post/{}/comment/{}",
Settings::get().hostname,
m.post_id,
m.id
);
i.comments(mention_url.to_owned());
let guid = GuidBuilder::default()
.permalink(true)
.value(&mention_url)
.build();
i.guid(guid.unwrap());
i.link(mention_url);
// TODO find a markdown to html parser here, do images, etc
i.description(m.content);
items.push(i.build().unwrap());
}
items
fn build_item(creator_name: &str, published: &NaiveDateTime, url: &str, content: &str) -> Item {
let mut i = ItemBuilder::default();
i.title(format!("Reply from {}", creator_name));
let author_url = format!("https://{}/u/{}", Settings::get().hostname, creator_name);
i.author(format!(
"/u/{} <a href=\"{}\">(link)</a>",
creator_name, author_url
));
let dt = DateTime::<Utc>::from_utc(*published, Utc);
i.pub_date(dt.to_rfc2822());
i.comments(url.to_owned());
let guid = GuidBuilder::default().permalink(true).value(url).build();
i.guid(guid.unwrap());
i.link(url.to_owned());
// TODO add images
let html = markdown_to_html(&content.to_string());
i.description(html);
i.build().unwrap()
}
fn create_post_items(posts: Vec<PostView>) -> Vec<Item> {
@ -359,9 +334,8 @@ fn create_post_items(posts: Vec<PostView>) -> Vec<Item> {
i.link(url);
}
// TODO find a markdown to html parser here, do images, etc
let mut description = format!("
submitted by <a href=\"{}\">{}</a> to <a href=\"{}\">{}</a><br>{} points | <a href=\"{}\">{} comments</a>",
// TODO add images
let mut description = format!("submitted by <a href=\"{}\">{}</a> to <a href=\"{}\">{}</a><br>{} points | <a href=\"{}\">{} comments</a>",
author_url,
p.creator_name,
community_url,
@ -371,7 +345,8 @@ fn create_post_items(posts: Vec<PostView>) -> Vec<Item> {
p.number_of_comments);
if let Some(body) = p.body {
description.push_str(&format!("<br><br>{}", body));
let html = markdown_to_html(&body);
description.push_str(&html);
}
i.description(description);

View file

@ -1,4 +1,3 @@
extern crate lazy_static;
use crate::apub::get_apub_protocol_string;
use crate::db::site_view::SiteView;
use crate::version;

View file

@ -1,4 +1,3 @@
extern crate lazy_static;
use config::{Config, ConfigError, Environment, File};
use serde::Deserialize;
use std::env;
@ -9,6 +8,7 @@ static CONFIG_FILE: &str = "config/config.hjson";
#[derive(Debug, Deserialize)]
pub struct Settings {
pub setup: Option<Setup>,
pub database: Database,
pub hostname: String,
pub bind: IpAddr,
@ -20,6 +20,14 @@ pub struct Settings {
pub federation: Federation,
}
#[derive(Debug, Deserialize)]
pub struct Setup {
pub admin_username: String,
pub admin_password: String,
pub admin_email: Option<String>,
pub site_name: String,
}
#[derive(Debug, Deserialize)]
pub struct RateLimitConfig {
pub message: i32,

View file

@ -375,12 +375,13 @@ export class Navbar extends Component<any, NavbarState> {
let link = isCommentType(reply)
? `/post/${reply.post_id}/comment/${reply.id}`
: `/inbox`;
let body = md.render(reply.content);
let htmlBody = md.render(reply.content);
let body = reply.content; // Unfortunately the notifications API can't do html
messageToastify(
creator_name,
creator_avatar,
body,
htmlBody,
link,
this.context.router
);

View file

@ -250,14 +250,14 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
<div class="row">
<div className={`vote-bar col-1 pr-0 small text-center`}>
<button
className={`btn-animate btn btn-link btn-lg p-0 ${
className={`btn-animate btn btn-link p-0 ${
this.state.my_vote == 1 ? 'text-info' : 'text-muted'
}`}
onClick={linkEvent(this, this.handlePostLike)}
data-tippy-content={i18n.t('upvote')}
>
<svg class="icon upvote">
<use xlinkHref="#icon-arrow-up"></use>
<use xlinkHref="#icon-arrow-up1"></use>
</svg>
</button>
<div
@ -268,14 +268,14 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
</div>
{WebSocketService.Instance.site.enable_downvotes && (
<button
className={`btn-animate btn btn-link btn-lg p-0 ${
className={`btn-animate btn btn-link p-0 ${
this.state.my_vote == -1 ? 'text-danger' : 'text-muted'
}`}
onClick={linkEvent(this, this.handlePostDisLike)}
data-tippy-content={i18n.t('downvote')}
>
<svg class="icon downvote">
<use xlinkHref="#icon-arrow-down"></use>
<use xlinkHref="#icon-arrow-down1"></use>
</svg>
</button>
)}

View file

@ -97,6 +97,12 @@ export class Symbols extends Component<any, any> {
<symbol id="icon-arrow-up" viewBox="0 0 24 24">
<path d="M5.707 12.707l5.293-5.293v11.586c0 0.552 0.448 1 1 1s1-0.448 1-1v-11.586l5.293 5.293c0.391 0.391 1.024 0.391 1.414 0s0.391-1.024 0-1.414l-7-7c-0.092-0.092-0.202-0.166-0.324-0.217s-0.253-0.076-0.383-0.076c-0.256 0-0.512 0.098-0.707 0.293l-7 7c-0.391 0.391-0.391 1.024 0 1.414s1.024 0.391 1.414 0z"></path>
</symbol>
<symbol id="icon-arrow-up1" viewBox="0 0 26 28">
<path d="M25.172 15.172c0 0.531-0.219 1.031-0.578 1.406l-1.172 1.172c-0.375 0.375-0.891 0.594-1.422 0.594s-1.047-0.219-1.406-0.594l-4.594-4.578v11c0 1.125-0.938 1.828-2 1.828h-2c-1.062 0-2-0.703-2-1.828v-11l-4.594 4.578c-0.359 0.375-0.875 0.594-1.406 0.594s-1.047-0.219-1.406-0.594l-1.172-1.172c-0.375-0.375-0.594-0.875-0.594-1.406s0.219-1.047 0.594-1.422l10.172-10.172c0.359-0.375 0.875-0.578 1.406-0.578s1.047 0.203 1.422 0.578l10.172 10.172c0.359 0.375 0.578 0.891 0.578 1.422z"></path>
</symbol>
<symbol id="icon-arrow-down1" viewBox="0 0 26 28">
<path d="M25.172 13c0 0.531-0.219 1.047-0.578 1.406l-10.172 10.187c-0.375 0.359-0.891 0.578-1.422 0.578s-1.047-0.219-1.406-0.578l-10.172-10.187c-0.375-0.359-0.594-0.875-0.594-1.406s0.219-1.047 0.594-1.422l1.156-1.172c0.375-0.359 0.891-0.578 1.422-0.578s1.047 0.219 1.406 0.578l4.594 4.594v-11c0-1.094 0.906-2 2-2h2c1.094 0 2 0.906 2 2v11l4.594-4.594c0.359-0.359 0.875-0.578 1.406-0.578s1.047 0.219 1.422 0.578l1.172 1.172c0.359 0.375 0.578 0.891 0.578 1.422z"></path>
</symbol>
<symbol id="icon-mail" viewBox="0 0 24 24">
<path d="M3 7.921l8.427 5.899c0.34 0.235 0.795 0.246 1.147 0l8.426-5.899v10.079c0 0.272-0.11 0.521-0.295 0.705s-0.433 0.295-0.705 0.295h-16c-0.272 0-0.521-0.11-0.705-0.295s-0.295-0.433-0.295-0.705zM1 5.983c0 0.010 0 0.020 0 0.030v11.987c0 0.828 0.34 1.579 0.88 2.12s1.292 0.88 2.12 0.88h16c0.828 0 1.579-0.34 2.12-0.88s0.88-1.292 0.88-2.12v-11.988c0-0.010 0-0.020 0-0.030-0.005-0.821-0.343-1.565-0.88-2.102-0.541-0.54-1.292-0.88-2.12-0.88h-16c-0.828 0-1.579 0.34-2.12 0.88-0.537 0.537-0.875 1.281-0.88 2.103zM20.894 5.554l-8.894 6.225-8.894-6.225c0.048-0.096 0.112-0.183 0.188-0.259 0.185-0.185 0.434-0.295 0.706-0.295h16c0.272 0 0.521 0.11 0.705 0.295 0.076 0.076 0.14 0.164 0.188 0.259z"></path>
</symbol>

4
ui/src/utils.ts vendored
View file

@ -12,6 +12,7 @@ import 'moment/locale/ca';
import 'moment/locale/fa';
import 'moment/locale/pt-br';
import 'moment/locale/ja';
import 'moment/locale/ka';
import {
UserOperation,
@ -59,6 +60,7 @@ export const languages = [
{ code: 'eo', name: 'Esperanto' },
{ code: 'es', name: 'Español' },
{ code: 'de', name: 'Deutsch' },
{ code: 'ka', name: 'ქართული ენა' },
{ code: 'fa', name: 'فارسی' },
{ code: 'ja', name: '日本語' },
{ code: 'pt_BR', name: 'Português Brasileiro' },
@ -353,6 +355,8 @@ export function getMomentLanguage(): string {
lang = 'pt-br';
} else if (lang.startsWith('ja')) {
lang = 'ja';
} else if (lang.startsWith('ka')) {
lang = 'ka';
} else {
lang = 'en';
}

2
ui/translations/ka.json vendored Normal file
View file

@ -0,0 +1,2 @@
{
}

View file

@ -1,3 +1,257 @@
{
"remove_post": "Usuń Post",
"no_posts": "Brak Postów.",
"create_a_post": "Stwórz post",
"create_post": "Stwórz Post",
"number_of_posts_0": "Post",
"number_of_posts_1": "Posty/ów",
"number_of_posts_2": "Posty/ów",
"posts": "Posty",
"related_posts": "Te posty mogą być powiązane",
"cross_posts": "Ten link został też zapostowany na:",
"comments": "Komentarze",
"number_of_comments_0": "Komentarz",
"number_of_comments_1": "Komentarzy/e",
"number_of_comments_2": "Komentarzy/e",
"post": "post",
"cross_post": "post zewnętrzny",
"cross_posted_to": "post wrzucony na zewnątrz: ",
"remove_comment": "Usuń Komentarz",
"communities": "Społeczności",
"users": "Użytkownicy",
"create_a_community": "Stwórz społeczność",
"create_community": "Stwórz Społeczność",
"remove_community": "Usuń Społeczność",
"subscribed_to_communities": "Subskrybowane <1>społeczności</1>",
"trending_communities": "Popularne <1>społeczności</1>",
"list_of_communities": "Lista społeczności",
"community_reqs": "małe litery, podkreślniki, bez spacji.",
"create_private_message": "Stwórz Prywatną Wiadomość",
"send_secure_message": "Wyślij Bezpieczną Wiadomość",
"send_message": "Wyślij Wiadomość",
"message": "Wiadomość",
"edit": "edytuj",
"reply": "odpowiedz",
"more": "więcej",
"cancel": "Anuluj",
"preview": "Podgląd",
"upload_image": "prześlij obraz",
"avatar": "Awatar",
"upload_avatar": "Prześlij Awatar",
"show_avatars": "Pokaż Awatary",
"formatting_help": "poradnik formatowania",
"lock": "zablokuj",
"messages": "Wiadomości",
"yes": "tak",
"number_of_communities_0": "Społeczność",
"number_of_communities_1": "Społeczności",
"number_of_communities_2": "Społeczności",
"sorting_help": "poradnik sortowania",
"view_source": "pokaż źródło",
"unlock": "odblokuj",
"sticky": "przyklej",
"unsticky": "odklej",
"link": "link",
"archive_link": "link archiwalny",
"mod": "moderator",
"mods": "moderatorzy",
"moderates": "Moderuje",
"settings": "Ustawienia",
"remove_as_mod": "zabierz uprawnienia moderatora",
"appoint_as_mod": "przyznaj uprawnienia moderatora",
"modlog": "Log moderatorski",
"admin": "administrator",
"admins": "administratorzy",
"remove_as_admin": "wycofaj uprawnienia administratora",
"appoint_as_admin": "przyznaj uprawnienia administratora",
"remove": "usuń",
"removed": "usunięte",
"locked": "zablokowane",
"stickied": "przyklejone",
"reason": "Powód",
"mark_as_read": "zaznacz jako przeczytane",
"mark_as_unread": "zaznacz jako nieprzeczytane",
"delete": "usuń",
"deleted": "usunięte",
"delete_account": "Usuń Konto",
"delete_account_confirm": "Ostrzeżenie: twoje dane zostaną bezpowrotnie usunięte. Wpisz swoje hasło aby potwierdzić.",
"restore": "przywróć",
"ban": "zbanuj",
"ban_from_site": "zbanuj ze strony",
"unban": "odbanuj",
"unban_from_site": "odbanuj ze strony",
"banned": "zbanowano",
"save": "zapisz",
"unsave": "cofnij zapis",
"create": "stwórz",
"creator": "autor",
"username": "Nazwa użytkownika",
"email_or_username": "Email lub Nazwa Użytkownika",
"number_of_users_0": "Użytkownik",
"number_of_users_1": "Użytkownicy/ków",
"number_of_users_2": "Użytkownicy/ków",
"number_of_subscribers_0": "Subskrybent",
"number_of_subscribers_1": "Subskrybenci/tów",
"number_of_subscribers_2": "Subskrybenci/tów",
"number_of_points_0": "Punkt",
"number_of_points_1": "Punkty/ów",
"number_of_points_2": "Punkty/ów",
"number_online_0": "Użytkownik Online",
"number_online_1": "Użytkowników Online",
"number_online_2": "Użytkowników Online",
"name": "Nazwa",
"title": "Tytuł",
"category": "Kategoria",
"subscribers": "Subskrybenci",
"both": "Obydwa",
"saved": "Zapisane",
"unsubscribe": "Odsubskrybuj",
"subscribe": "Subskrybuj",
"subscribed": "Zasubskrybowane",
"prev": "Wstecz",
"next": "Dalej",
"sidebar": "Pasek boczny",
"sort_type": "Sortuj typ",
"hot": "Popularne",
"new": "Nowe",
"old": "Stare",
"top_day": "Popularne dzisiaj",
"week": "Tydzień",
"month": "Miesiąc",
"year": "Rok",
"all": "Wszystko",
"top": "Popularne",
"api": "API",
"docs": "Dokumentacja",
"inbox": "Skrzynka odbiorcza",
"inbox_for": "Skrzynka odbiorcza <1>{{user}}</1>",
"mark_all_as_read": "zaznacz wszystko jako przeczytane",
"type": "Rodzaj",
"unread": "Nieprzeczytane",
"replies": "Odpowiedzi",
"mentions": "Wzmianki",
"reply_sent": "Odpowiedź wysłana",
"message_sent": "Wiadomość wysłana",
"search": "Szukaj",
"overview": "Podgląd",
"view": "Widok",
"logout": "Wyloguj",
"login_sign_up": "Zaloguj / Zarejestruj",
"login": "Zaloguj",
"sign_up": "Zarejestruj",
"notifications_error": "Powiadomienia na pulpicie są niedostępne w Twojej przeglądarce. Spróbuj przeglądarkę Firefox lub Chrome.",
"unread_messages": "Nieprzeczytane Wiadomości",
"password": "Hasło",
"verify_password": "Zweryfikuj Hasło",
"old_password": "Stare Hasło",
"forgot_password": "nie pamiętam hasła",
"reset_password_mail_sent": "Wysłano email w celu zresetowania hasła.",
"password_change": "Zmiana Hasła",
"new_password": "Nowe Hasło",
"no_email_setup": "Email nie został poprawnie ustawiony na tym serwerze.",
"email": "Email",
"matrix_user_id": "Użytkownik Matrixa",
"private_message_disclaimer": "Ostrzeżenie: Prywatne wiadomości w Lemmym nie są bezpieczne. Jeśli chcesz wysyłać i odbierać bezpieczne wiadomości załóż konto na <1>Riot.im</1>.",
"send_notifications_to_email": "Wysyłaj powiadomienia na Email",
"optional": "Opcjonalne",
"expires": "Wygasa",
"language": "Język",
"browser_default": "Domyślna wartość przeglądarki",
"downvotes_disabled": "Wdółgłosy wyłączone",
"enable_downvotes": "Włącz Wdółgłosy",
"upvote": "Wgóręgłos",
"number_of_upvotes_0": "Wgóręgłos",
"number_of_upvotes_1": "Wgóręgłosy/ów",
"number_of_upvotes_2": "Wgóręgłosy/ów",
"downvote": "Wdółgłos",
"number_of_downvotes_0": "Wdółgłos",
"number_of_downvotes_1": "Wdółgłosy/ów",
"number_of_downvotes_2": "Wdółgłosy/ów",
"open_registration": "Rejestracja Otwarta",
"registration_closed": "Rejestracja Zamknięta",
"enable_nsfw": "Włącz NSFW",
"url": "URL",
"body": "Treść",
"copy_suggested_title": "skopiuj sugerowany tytuł: {{title}}",
"community": "Społeczność",
"expand_here": "Rozwiń tutaj",
"subscribe_to_communities": "Zasubskrybuj kilka <1>społeczności</1>.",
"chat": "Czat",
"recent_comments": "Najnowsze Komentarze",
"no_results": "Brak wyników.",
"setup": "Instalacja",
"lemmy_instance_setup": "Instalacja Instancji Lemmy",
"setup_admin": "Ustaw Administratora Strony",
"your_site": "twoja witryna",
"modified": "zmodyfikowane",
"nsfw": "NSFW",
"show_nsfw": "Pokaż treści NSFW (+18)",
"theme": "Motyw",
"sponsors": "Sponsorzy",
"sponsors_of_lemmy": "Sponsorzy projektu Lemmy",
"sponsor_message": "Lemmy jest wolnym, <1>otwartoźródłowym</1> oprogramowaniem, co oznacza zero reklam, opłat, czy innych form kapitalizacji, od zawsze na zawsze. Twoje darowizny idą bezpośrednio na rozwój projektu w pełno-etatowym wymiarze. Specjalne wyrazy podziękowania dla następujących osób:",
"support_on_patreon": "Wspieraj w serwisie Patreon",
"support_on_liberapay": "Wspieraj na Liberapay",
"donate_to_lemmy": "Przekaż datek na Lemmiego",
"donate": "Przekaż datek",
"general_sponsors": "Główni Sponsorzy to osoby które wsparły Lemmiego kwotą od $10 do $39.",
"crypto": "Kryptowaluta",
"bitcoin": "Bitcoin",
"ethereum": "Ethereum",
"monero": "Monero",
"code": "Kod",
"joined": "Dołączono",
"by": "przez",
"to": "do",
"from": "od",
"transfer_community": "transfer społeczności",
"transfer_site": "transfer witryny",
"are_you_sure": "na pewno?",
"no": "nie",
"powered_by": "Powered by",
"landing": "Lemmy jest <1>agregatorem linków</1> / alternatywą dla reddita. Jest przeznaczony do działania w ramach cyfrowej przestrzeni nazywanej <2>fediverse<2>. <3></3>Opiera się na samodzielnym hostingu, posiada aktualizowane na żywo wątki z komentarzami, i zajmuje bardzo mało miejsce (<4>~80kB</4>). Federacja w ramach sieci ActivityPub jest w planach. <5></5>Ta wersja jest <6>bardzo wczesną wersją beta</6>, co oznacza, że wiele funkcji nadal nie działa tak jak powinny. <7></7><8>Pod tym adresem</8> można sugerować nową funkcjonalność i zgłaszać błędy.<9></9>Stworzono z wykorzystaniem <10>Rust</10>, <11>Actix</11>, <12>Inferno</12>, <13>Typescript</13>.",
"not_logged_in": "Nie jesteś zalogowana/y.",
"logged_in": "Zalogowano.",
"community_ban": "Zostałaś/eś zbanowana/y z tej społeczności.",
"site_ban": "Zostałaś/eś zbanowana/y z tej witryny",
"couldnt_create_comment": "Nie udało się stworzyć komentarza.",
"couldnt_like_comment": "Polubienie komentarza nie powiodło się.",
"couldnt_update_comment": "Zaktualizowanie komentarza nie powiodło się.",
"couldnt_save_comment": "Zapisanie komentarza nie powiodło się.",
"couldnt_get_comments": "Pobranie komentarzy nie powiodło się.",
"no_comment_edit_allowed": "Nie masz uprawnień do edycji komentarza.",
"no_post_edit_allowed": "Nie masz uprawnień do edycji posta.",
"no_community_edit_allowed": "Nie masz uprawnień do edycji społeczności.",
"couldnt_find_community": "Nie udało się znaleźć społeczności.",
"couldnt_update_community": "Nie udało się zaktualizować Społeczności.",
"community_already_exists": "Społeczność już istnieje.",
"community_moderator_already_exists": "Moderator społeczności już istnieje.",
"community_follower_already_exists": "Osoba obserwująca społeczność już istnieje.",
"community_user_already_banned": "Użytkownik społeczności jest już zbanowany.",
"couldnt_create_post": "Nie udało się stworzyć posta.",
"post_title_too_long": "Tytuł posta zbyt długi.",
"couldnt_like_post": "Nie udało się polubić posta.",
"couldnt_find_post": "Nie udało się znaleźć posta.",
"couldnt_update_post": "Nie udało się zaktualizować postów",
"couldnt_get_posts": "Nie udało się pobrać postów",
"couldnt_save_post": "Nie udało się zapisać posta.",
"no_slurs": "Bez obelg.",
"not_an_admin": "Nie jest administratorem.",
"site_already_exists": "Witryna już istnieje.",
"couldnt_update_site": "Nie udało się zaktualizować witryny.",
"couldnt_find_that_username_or_email": "Nie udało się znaleźć takiej nazwy użytkownika lub adresu email.",
"password_incorrect": "Hasło niepoprawne.",
"passwords_dont_match": "Hasła nie pasują do siebie.",
"admin_already_created": "Wybacz, funkcja administratora jest już przypisana.",
"user_already_exists": "Użytkownik już istnieje.",
"email_already_exists": "Email już istnieje.",
"couldnt_update_user": "Nie udało się zaktualizować użytkownika.",
"system_err_login": "Błąd systemu. Spróbuj wylogować się i następnie zalogować ponownie.",
"couldnt_create_private_message": "Nie udało się stworzyć prywatnej wiadomości.",
"no_private_message_edit_allowed": "Brak uprawnień do edycji prywatnej wiadomości.",
"couldnt_update_private_message": "Nie udało się zaktualizować prywatnej wiadomości.",
"time": "Czas",
"action": "Akcja",
"block_leaving": "Czy na pewno chcesz wyjść?",
"show_context": "Pokaż kontekst"
}

View file

@ -243,5 +243,6 @@
"number_of_upvotes": "{{count}} voto positivo",
"number_of_upvotes_plural": "{{count}} votos positivos",
"number_of_downvotes": "{{count}} voto negativo",
"number_of_downvotes_plural": "{{count}} votos negativos"
"number_of_downvotes_plural": "{{count}} votos negativos",
"show_context": "Mostrar contexto"
}