Merge branch 'master' into federation_merge_from_master_2

This commit is contained in:
Dessalines 2020-06-23 21:11:38 -04:00
commit dc94e58cbf
61 changed files with 1177 additions and 1080 deletions

2
CODE_OF_CONDUCT.md vendored
View file

@ -30,6 +30,6 @@ In the Lemmy community we strive to go the extra step to look out for each other
And if someone takes issue with something you said or did, resist the urge to be defensive. Just stop doing what it was they complained about and apologize. Even if you feel you were misinterpreted or unfairly accused, chances are good there was something you couldve communicated better — remember that its your responsibility to make others comfortable. Everyone wants to get along and we are all here first and foremost because we want to talk about cool technology. You will find that people will be eager to assume good intent and forgive as long as you earn their trust. And if someone takes issue with something you said or did, resist the urge to be defensive. Just stop doing what it was they complained about and apologize. Even if you feel you were misinterpreted or unfairly accused, chances are good there was something you couldve communicated better — remember that its your responsibility to make others comfortable. Everyone wants to get along and we are all here first and foremost because we want to talk about cool technology. You will find that people will be eager to assume good intent and forgive as long as you earn their trust.
The enforcement policies listed above apply to all official Lemmy venues; including git repositories under [github.com/LemmyNet/lemmy](https://github.com/LemmyNet/lemmy) and [yerbamate.dev/dessalines/lemmy](https://yerbamate.dev/dessalines/lemmy), the [Matrix channel](https://matrix.to/#/!BZVTUuEiNmRcbFeLeI:matrix.org?via=matrix.org&via=privacytools.io&via=permaweb.io); and all instances under lemmy.ml. For other projects adopting the Rust Code of Conduct, please contact the maintainers of those projects for enforcement. If you wish to use this code of conduct for your own project, consider explicitly mentioning your moderation policy or making a copy with your own moderation policy so as to avoid confusion. The enforcement policies listed above apply to all official Lemmy venues; including git repositories under [github.com/LemmyNet/lemmy](https://github.com/LemmyNet/lemmy) and [yerbamate.dev/LemmyNet/lemmy](https://yerbamate.dev/LemmyNet/lemmy), the [Matrix channel](https://matrix.to/#/!BZVTUuEiNmRcbFeLeI:matrix.org?via=matrix.org&via=privacytools.io&via=permaweb.io); and all instances under lemmy.ml. For other projects adopting the Rust Code of Conduct, please contact the maintainers of those projects for enforcement. If you wish to use this code of conduct for your own project, consider explicitly mentioning your moderation policy or making a copy with your own moderation policy so as to avoid confusion.
Adapted from the [Rust Code of Conduct](https://www.rust-lang.org/policies/code-of-conduct), which is based on the [Node.js Policy on Trolling](http://blog.izs.me/post/30036893703/policy-on-trolling) as well as the [Contributor Covenant v1.3.0](https://www.contributor-covenant.org/version/1/3/0/). Adapted from the [Rust Code of Conduct](https://www.rust-lang.org/policies/code-of-conduct), which is based on the [Node.js Policy on Trolling](http://blog.izs.me/post/30036893703/policy-on-trolling) as well as the [Contributor Covenant v1.3.0](https://www.contributor-covenant.org/version/1/3/0/).

63
RELEASES.md vendored
View file

@ -1,3 +1,66 @@
# Lemmy v0.7.0 Release (2020-06-23)
This release replaces [pictshare](https://github.com/HaschekSolutions/pictshare)
with [pict-rs](https://git.asonix.dog/asonix/pict-rs), which improves performance
and security.
Overall, since our last major release in January (v0.6.0), we have closed over
[100 issues!](https://github.com/LemmyNet/lemmy/milestone/16?closed=1)
- Site-wide list of recent comments
- Reconnecting websockets
- Many more themes, including a default light one.
- Expandable embeds for post links (and thumbnails), from
[iframely](https://github.com/itteco/iframely)
- Better icons
- Emoji autocomplete to post and message bodies, and an Emoji Picker
- Post body now searchable
- Community title and description is now searchable
- Simplified cross-posts
- Better documentation
- LOTS more languages
- Lots of bugs squashed
- And more ...
## Upgrading
Before starting the upgrade, make sure that you have a working backup of your
database and image files. See our
[documentation](https://dev.lemmy.ml/docs/administration_backup_and_restore.html)
for backup instructions.
**With Ansible:**
```
# deploy with ansible from your local lemmy git repo
git pull
cd ansible
ansible-playbook lemmy.yml
# connect via ssh to run the migration script
ssh your-server
cd /lemmy/
wget https://raw.githubusercontent.com/LemmyNet/lemmy/master/docker/prod/migrate-pictshare-to-pictrs.bash
chmod +x migrate-pictshare-to-pictrs.bash
sudo ./migrate-pictshare-to-pictrs.bash
```
**With manual Docker installation:**
```
# run these commands on your server
cd /lemmy
wget https://raw.githubusercontent.com/LemmyNet/lemmy/master/ansible/templates/nginx.conf
# Replace the {{ vars }}
sudo mv nginx.conf /etc/nginx/sites-enabled/lemmy.conf
sudo nginx -s reload
wget https://raw.githubusercontent.com/LemmyNet/lemmy/master/docker/prod/docker-compose.yml
wget https://raw.githubusercontent.com/LemmyNet/lemmy/master/docker/prod/migrate-pictshare-to-pictrs.bash
chmod +x migrate-pictshare-to-pictrs.bash
sudo bash migrate-pictshare-to-pictrs.bash
```
**Note:** After upgrading, all users need to reload the page, then logout and
login again, so that images are loaded correctly.
# Lemmy v0.6.0 Release (2020-01-16) # Lemmy v0.6.0 Release (2020-01-16)
`v0.6.0` is here, and we've closed [41 issues!](https://github.com/LemmyNet/lemmy/milestone/15?closed=1) `v0.6.0` is here, and we've closed [41 issues!](https://github.com/LemmyNet/lemmy/milestone/15?closed=1)

2
ansible/VERSION vendored
View file

@ -1 +1 @@
v0.6.71 v0.7.0

1
ansible/ansible.cfg vendored
View file

@ -1,5 +1,6 @@
[defaults] [defaults]
inventory=inventory inventory=inventory
interpreter_python=/usr/bin/python3
[ssh_connection] [ssh_connection]
pipelining = True pipelining = True

8
ansible/lemmy.yml vendored
View file

@ -24,10 +24,11 @@
creates: '/etc/letsencrypt/live/{{domain}}/privkey.pem' creates: '/etc/letsencrypt/live/{{domain}}/privkey.pem'
- name: create lemmy folder - name: create lemmy folder
file: path={{item.path}} state=directory file: path={{item.path}} {{item.owner}} state=directory
with_items: with_items:
- { path: '/lemmy/' } - { path: '/lemmy/', owner: 'root' }
- { path: '/lemmy/volumes/' } - { path: '/lemmy/volumes/', owner: 'root' }
- { path: '/lemmy/volumes/pictrs/', owner: '991' }
- block: - block:
- name: add template files - name: add template files
@ -59,6 +60,7 @@
project_src: /lemmy/ project_src: /lemmy/
state: present state: present
pull: yes pull: yes
remove_orphans: yes
- name: reload nginx with new config - name: reload nginx with new config
shell: nginx -s reload shell: nginx -s reload

View file

@ -26,10 +26,11 @@
creates: '/etc/letsencrypt/live/{{domain}}/privkey.pem' creates: '/etc/letsencrypt/live/{{domain}}/privkey.pem'
- name: create lemmy folder - name: create lemmy folder
file: path={{item.path}} state=directory file: path={{item.path}} owner={{item.owner}} state=directory
with_items: with_items:
- { path: '/lemmy/' } - { path: '/lemmy/', owner: 'root' }
- { path: '/lemmy/volumes/' } - { path: '/lemmy/volumes/', owner: 'root' }
- { path: '/lemmy/volumes/pictrs/', owner: '991' }
- block: - block:
- name: add template files - name: add template files
@ -88,6 +89,7 @@
project_src: /lemmy/ project_src: /lemmy/
state: present state: present
recreate: always recreate: always
remove_orphans: yes
ignore_errors: yes ignore_errors: yes
- name: reload nginx with new config - name: reload nginx with new config

View file

@ -12,7 +12,7 @@ services:
- ./lemmy.hjson:/config/config.hjson:ro - ./lemmy.hjson:/config/config.hjson:ro
depends_on: depends_on:
- postgres - postgres
- pictshare - pictrs
- iframely - iframely
postgres: postgres:
@ -25,12 +25,13 @@ services:
- ./volumes/postgres:/var/lib/postgresql/data - ./volumes/postgres:/var/lib/postgresql/data
restart: always restart: always
pictshare: pictrs:
image: hascheksolutions/pictshare:latest image: asonix/pictrs:amd64-v0.1.0-r9
user: 991:991
ports: ports:
- "127.0.0.1:8537:80" - "127.0.0.1:8537:8080"
volumes: volumes:
- ./volumes/pictshare:/usr/share/nginx/html/data - ./volumes/pictrs:/mnt
restart: always restart: always
iframely: iframely:

View file

@ -48,8 +48,8 @@ server {
add_header X-Frame-Options "DENY"; add_header X-Frame-Options "DENY";
add_header X-XSS-Protection "1; mode=block"; add_header X-XSS-Protection "1; mode=block";
# Upload limit for pictshare # Upload limit for pictrs
client_max_body_size 50M; client_max_body_size 20M;
location / { location / {
proxy_pass http://0.0.0.0:8536; proxy_pass http://0.0.0.0:8536;
@ -70,15 +70,21 @@ server {
proxy_cache_min_uses 5; proxy_cache_min_uses 5;
} }
location /pictshare/ { # Redirect pictshare images to pictrs
proxy_pass http://0.0.0.0:8537/; location ~ /pictshare/(.*)$ {
return 301 /pictrs/image/$1;
}
# pict-rs images
location /pictrs {
location /pictrs/image {
proxy_pass http://0.0.0.0:8537/image;
proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Host $host; proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
if ($request_uri ~ \.(?:ico|gif|jpe?g|png|webp|bmp|mp4)$) {
add_header Cache-Control "public, max-age=31536000, immutable";
} }
# Block the import
return 403;
} }
location /iframely/ { location /iframely/ {

15
docker/dev/Dockerfile vendored
View file

@ -21,17 +21,13 @@ COPY server/Cargo.toml server/Cargo.lock ./
RUN sudo chown -R rust:rust . RUN sudo chown -R rust:rust .
RUN mkdir -p ./src/bin \ RUN mkdir -p ./src/bin \
&& echo 'fn main() { println!("Dummy") }' > ./src/bin/main.rs && echo 'fn main() { println!("Dummy") }' > ./src/bin/main.rs
RUN cargo build --release RUN cargo build
RUN rm -f ./target/x86_64-unknown-linux-musl/release/deps/lemmy_server* RUN rm -f ./target/x86_64-unknown-linux-musl/release/deps/lemmy_server*
COPY server/src ./src/ COPY server/src ./src/
COPY server/migrations ./migrations/ COPY server/migrations ./migrations/
# Build for release # Build for debug
RUN cargo build --frozen --release RUN cargo build
# Get diesel-cli on there just in case
# RUN cargo install diesel_cli --no-default-features --features postgres
FROM ekidd/rust-musl-builder:1.42.0-openssl11 as docs FROM ekidd/rust-musl-builder:1.42.0-openssl11 as docs
WORKDIR /app WORKDIR /app
@ -39,15 +35,14 @@ COPY docs ./docs
RUN sudo chown -R rust:rust . RUN sudo chown -R rust:rust .
RUN mdbook build docs/ RUN mdbook build docs/
FROM alpine:3.12
FROM alpine:3.10
# Install libpq for postgres # Install libpq for postgres
RUN apk add libpq RUN apk add libpq
# Copy resources # Copy resources
COPY server/config/defaults.hjson /config/defaults.hjson COPY server/config/defaults.hjson /config/defaults.hjson
COPY --from=rust /app/server/target/x86_64-unknown-linux-musl/release/lemmy_server /app/lemmy COPY --from=rust /app/server/target/x86_64-unknown-linux-musl/debug/lemmy_server /app/lemmy
COPY --from=docs /app/docs/book/ /app/dist/documentation/ COPY --from=docs /app/docs/book/ /app/dist/documentation/
COPY --from=node /app/ui/dist /app/dist COPY --from=node /app/ui/dist /app/dist

View file

@ -1,79 +0,0 @@
FROM node:10-jessie as node
WORKDIR /app/ui
# Cache deps
COPY ui/package.json ui/yarn.lock ./
RUN yarn install --pure-lockfile
# Build
COPY ui /app/ui
RUN yarn build
# contains qemu-*-static for cross-compilation
FROM multiarch/qemu-user-static as qemu
FROM arm64v8/rust:1.40-buster as rust
COPY --from=qemu /usr/bin/qemu-aarch64-static /usr/bin
#COPY --from=qemu /usr/bin/qemu-arm-static /usr/bin
# Install musl
#RUN apt-get update && apt-get install -y mc
#RUN apt-get install -y musl-tools mc
#libpq-dev mc
#RUN rustup target add ${TARGET}
# Cache deps
WORKDIR /app
RUN USER=root cargo new server
WORKDIR /app/server
COPY server/Cargo.toml server/Cargo.lock ./
RUN mkdir -p ./src/bin \
&& echo 'fn main() { println!("Dummy") }' > ./src/bin/main.rs
RUN cargo build --release
# RUN cargo build
COPY server/src ./src/
COPY server/migrations ./migrations/
RUN rm -f ./target/release/deps/lemmy_server* ; rm -f ./target/debug/deps/lemmy_server*
# build for release
RUN cargo build --frozen --release
# RUN cargo build --frozen
# Get diesel-cli on there just in case
# RUN cargo install diesel_cli --no-default-features --features postgres
# RUN cp /app/server/target/debug/lemmy_server /app/server/ready
RUN cp /app/server/target/release/lemmy_server /app/server/ready
#FROM alpine:3.10
# debian because build with dynamic linking with debian:buster
FROM arm64v8/debian:buster-slim as lemmy
#COPY --from=qemu /usr/bin/qemu-arm-static /usr/bin
COPY --from=qemu /usr/bin/qemu-aarch64-static /usr/bin
# Install libpq for postgres
#RUN apk add libpq
RUN apt-get update && apt-get install -y libpq5
RUN addgroup --gid 1000 lemmy
# for alpine
#RUN adduser -D -s /bin/sh -u 1000 -G lemmy lemmy
# for debian
RUN adduser --disabled-password --shell /bin/sh --uid 1000 --ingroup lemmy lemmy
# Copy resources
COPY server/config/defaults.hjson /config/defaults.hjson
COPY --from=rust /app/server/ready /app/lemmy
COPY --from=node /app/ui/dist /app/dist
RUN chown lemmy:lemmy /app/lemmy
USER lemmy
EXPOSE 8536
CMD ["/app/lemmy"]

View file

@ -1,79 +0,0 @@
FROM node:10-jessie as node
WORKDIR /app/ui
# Cache deps
COPY ui/package.json ui/yarn.lock ./
RUN yarn install --pure-lockfile
# Build
COPY ui /app/ui
RUN yarn build
# contains qemu-*-static for cross-compilation
FROM multiarch/qemu-user-static as qemu
FROM arm32v7/rust:1.37-buster as rust
#COPY --from=qemu /usr/bin/qemu-aarch64-static /usr/bin
COPY --from=qemu /usr/bin/qemu-arm-static /usr/bin
# Install musl
#RUN apt-get update && apt-get install -y mc
#RUN apt-get install -y musl-tools mc
#libpq-dev mc
#RUN rustup target add ${TARGET}
# Cache deps
WORKDIR /app
RUN USER=root cargo new server
WORKDIR /app/server
COPY server/Cargo.toml server/Cargo.lock ./
RUN mkdir -p ./src/bin \
&& echo 'fn main() { println!("Dummy") }' > ./src/bin/main.rs
#RUN cargo build --release
# RUN cargo build
RUN RUSTFLAGS='-Ccodegen-units=1' cargo build
COPY server/src ./src/
COPY server/migrations ./migrations/
RUN rm -f ./target/release/deps/lemmy_server* ; rm -f ./target/debug/deps/lemmy_server*
# build for release
#RUN cargo build --frozen --release
RUN cargo build --frozen
# Get diesel-cli on there just in case
# RUN cargo install diesel_cli --no-default-features --features postgres
RUN cp /app/server/target/debug/lemmy_server /app/server/ready
#RUN cp /app/server/target/release/lemmy_server /app/server/ready
#FROM alpine:3.10
# debian because build with dynamic linking with debian:buster
FROM arm32v7/debian:buster-slim as lemmy
COPY --from=qemu /usr/bin/qemu-arm-static /usr/bin
# Install libpq for postgres
#RUN apk add libpq
RUN apt-get update && apt-get install -y libpq5
RUN addgroup --gid 1000 lemmy
# for alpine
#RUN adduser -D -s /bin/sh -u 1000 -G lemmy lemmy
# for debian
RUN adduser --disabled-password --shell /bin/sh --uid 1000 --ingroup lemmy lemmy
# Copy resources
COPY server/config/defaults.hjson /config/defaults.hjson
COPY --from=rust /app/server/ready /app/lemmy
COPY --from=node /app/ui/dist /app/dist
RUN chown lemmy:lemmy /app/lemmy
USER lemmy
EXPOSE 8536
CMD ["/app/lemmy"]

View file

@ -1,88 +0,0 @@
# can be build on x64, arm32, arm64 platforms
# to build on target platform run
# docker build -f Dockerfile.libc -t dessalines/lemmy:version ../..
#
# to use docker buildx run
# docker buildx build --platform linux/amd64,linux/arm64 -f Dockerfile.libc -t YOURNAME/lemmy --push ../..
FROM node:12-buster as node
# use this if use docker buildx
#FROM --platform=$BUILDPLATFORM node:12-buster as node
WORKDIR /app/ui
# Cache deps
COPY ui/package.json ui/yarn.lock ./
RUN yarn install --pure-lockfile --network-timeout 100000
# Build
COPY ui /app/ui
RUN yarn build
FROM rust:1.42 as rust
# Cache deps
WORKDIR /app
RUN USER=root cargo new server
WORKDIR /app/server
COPY server/Cargo.toml server/Cargo.lock ./
RUN mkdir -p ./src/bin \
&& echo 'fn main() { println!("Dummy") }' > ./src/bin/main.rs
RUN cargo build --release
#RUN cargo build && \
# rm -f ./target/release/deps/lemmy_server* ; rm -f ./target/debug/deps/lemmy_server*
COPY server/src ./src/
COPY server/migrations ./migrations/
# build for release
# workaround for https://github.com/rust-lang/rust/issues/62896
#RUN RUSTFLAGS='-Ccodegen-units=1' cargo build --release
RUN cargo build --release --frozen
#RUN cargo build --frozen
# Get diesel-cli on there just in case
# RUN cargo install diesel_cli --no-default-features --features postgres
# make result place always the same for lemmy container
RUN cp /app/server/target/release/lemmy_server /app/server/ready
#RUN cp /app/server/target/debug/lemmy_server /app/server/ready
FROM rust:1.42 as docs
WORKDIR /app
# Build docs
COPY docs ./docs
RUN cargo install mdbook
RUN mdbook build docs/
#FROM alpine:3.10
# debian because build with dynamic linking with debian:buster
FROM debian:buster as lemmy
# Install libpq for postgres
#RUN apk add libpq
RUN apt-get update && apt-get install -y libpq5
RUN addgroup --gid 1000 lemmy
# for alpine
#RUN adduser -D -s /bin/sh -u 1000 -G lemmy lemmy
# for debian
RUN adduser --disabled-password --shell /bin/sh --uid 1000 --ingroup lemmy lemmy
# Copy resources
COPY server/config/defaults.hjson /config/defaults.hjson
COPY --from=node /app/ui/dist /app/dist
COPY --from=docs /app/docs/book/ /app/dist/documentation/
COPY --from=rust /app/server/ready /app/lemmy
RUN chown lemmy:lemmy /app/lemmy
USER lemmy
EXPOSE 8536
CMD ["/app/lemmy"]

View file

@ -1,15 +1,6 @@
version: '3.3' version: '3.3'
services: services:
postgres:
image: postgres:12-alpine
environment:
- POSTGRES_USER=lemmy
- POSTGRES_PASSWORD=password
- POSTGRES_DB=lemmy
volumes:
- ./volumes/postgres:/var/lib/postgresql/data
restart: always
lemmy: lemmy:
build: build:
@ -23,16 +14,27 @@ services:
volumes: volumes:
- ../lemmy.hjson:/config/config.hjson - ../lemmy.hjson:/config/config.hjson
depends_on: depends_on:
- pictrs
- postgres - postgres
- pictshare
- iframely - iframely
pictshare: postgres:
image: hascheksolutions/pictshare:latest image: postgres:12-alpine
ports: environment:
- "127.0.0.1:8537:80" - POSTGRES_USER=lemmy
- POSTGRES_PASSWORD=password
- POSTGRES_DB=lemmy
volumes: volumes:
- ./volumes/pictshare:/usr/share/nginx/html/data - ./volumes/postgres:/var/lib/postgresql/data
restart: always
pictrs:
image: asonix/pictrs:v0.1.13-r0
ports:
- "127.0.0.1:8537:8080"
user: 991:991
volumes:
- ./volumes/pictrs:/mnt
restart: always restart: always
iframely: iframely:

View file

@ -1,2 +1,6 @@
#!/bin/sh #!/bin/sh
set -e
export COMPOSE_DOCKER_CLI_BUILD=1
export DOCKER_BUILDKIT=1
docker-compose up -d --no-deps --build docker-compose up -d --no-deps --build

View file

@ -4,7 +4,9 @@ set -e
BRANCH=$1 BRANCH=$1
git checkout $BRANCH git checkout $BRANCH
cd ../../
export COMPOSE_DOCKER_CLI_BUILD=1
export DOCKER_BUILDKIT=1
# Rebuilding dev docker # Rebuilding dev docker
sudo docker build . -f "docker/dev/Dockerfile" -t "dessalines/lemmy:$BRANCH" sudo docker build . -f "docker/dev/Dockerfile" -t "dessalines/lemmy:$BRANCH"

View file

@ -12,11 +12,11 @@ services:
- ../federation/nginx.conf:/etc/nginx/nginx.conf - ../federation/nginx.conf:/etc/nginx/nginx.conf
depends_on: depends_on:
- lemmy_alpha - lemmy_alpha
- pictshare_alpha - pictrs_alpha
- lemmy_beta - lemmy_beta
- pictshare_beta - pictrs_beta
- lemmy_gamma - lemmy_gamma
- pictshare_gamma - pictrs_gamma
- iframely - iframely
restart: "always" restart: "always"
@ -48,10 +48,11 @@ services:
volumes: volumes:
- ./volumes/postgres_alpha:/var/lib/postgresql/data - ./volumes/postgres_alpha:/var/lib/postgresql/data
restart: always restart: always
pictshare_alpha: pictrs_alpha:
image: shtripok/pictshare:latest image: asonix/pictrs:v0.1.13-r0
user: 991:991
volumes: volumes:
- ./volumes/pictshare_alpha:/usr/share/nginx/html/data - ./volumes/pictrs_alpha:/mnt
restart: always restart: always
lemmy_beta: lemmy_beta:
@ -82,10 +83,11 @@ services:
volumes: volumes:
- ./volumes/postgres_beta:/var/lib/postgresql/data - ./volumes/postgres_beta:/var/lib/postgresql/data
restart: always restart: always
pictshare_beta: pictrs_beta:
image: shtripok/pictshare:latest image: asonix/pictrs:v0.1.13-r0
user: 991:991
volumes: volumes:
- ./volumes/pictshare_beta:/usr/share/nginx/html/data - ./volumes/pictrs_beta:/mnt
restart: always restart: always
lemmy_gamma: lemmy_gamma:
@ -116,10 +118,11 @@ services:
volumes: volumes:
- ./volumes/postgres_gamma:/var/lib/postgresql/data - ./volumes/postgres_gamma:/var/lib/postgresql/data
restart: always restart: always
pictshare_gamma: pictrs_gamma:
image: shtripok/pictshare:latest image: asonix/pictrs:v0.1.13-r0
user: 991:991
volumes: volumes:
- ./volumes/pictshare_gamma:/usr/share/nginx/html/data - ./volumes/pictrs_gamma:/mnt
restart: always restart: always
iframely: iframely:

View file

@ -23,12 +23,17 @@ http {
proxy_set_header Connection "upgrade"; proxy_set_header Connection "upgrade";
} }
location /pictshare/ { # pict-rs images
proxy_pass http://pictshare_alpha:80/; location /pictrs {
location /pictrs/image {
proxy_pass http://pictrs_alpha:8080/image;
proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Host $host; proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
} }
# Block the import
return 403;
}
location /iframely/ { location /iframely/ {
proxy_pass http://iframely:80/; proxy_pass http://iframely:80/;
@ -58,12 +63,17 @@ http {
proxy_set_header Connection "upgrade"; proxy_set_header Connection "upgrade";
} }
location /pictshare/ { # pict-rs images
proxy_pass http://pictshare_beta:80/; location /pictrs {
location /pictrs/image {
proxy_pass http://pictrs_beta:8080/image;
proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Host $host; proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
} }
# Block the import
return 403;
}
location /iframely/ { location /iframely/ {
proxy_pass http://iframely:80/; proxy_pass http://iframely:80/;
@ -93,12 +103,17 @@ http {
proxy_set_header Connection "upgrade"; proxy_set_header Connection "upgrade";
} }
location /pictshare/ { # pict-rs images
proxy_pass http://pictshare_gamma:80/; location /pictrs {
location /pictrs/image {
proxy_pass http://pictrs_gamma:8080/image;
proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Host $host; proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
} }
# Block the import
return 403;
}
location /iframely/ { location /iframely/ {
proxy_pass http://iframely:80/; proxy_pass http://iframely:80/;

View file

@ -20,4 +20,9 @@ popd || exit
sudo docker build ../../ --file Dockerfile -t lemmy-federation:latest sudo docker build ../../ --file Dockerfile -t 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 docker-compose up sudo docker-compose up

64
docker/prod/Dockerfile vendored Normal file
View file

@ -0,0 +1,64 @@
ARG RUST_BUILDER_IMAGE=shtripok/rust-musl-builder:arm
FROM $RUST_BUILDER_IMAGE as rust
#ARG RUSTRELEASEDIR="debug"
ARG RUSTRELEASEDIR="release"
# Cache deps
WORKDIR /app
RUN sudo chown -R rust:rust .
RUN USER=root cargo new server
WORKDIR /app/server
COPY --chown=rust:rust server/Cargo.toml server/Cargo.lock ./
#RUN sudo chown -R rust:rust .
RUN mkdir -p ./src/bin \
&& echo 'fn main() { println!("Dummy") }' > ./src/bin/main.rs
RUN cargo build --release
RUN rm -f ./target/$CARGO_BUILD_TARGET/$RUSTRELEASEDIR/deps/lemmy_server*
COPY --chown=rust:rust server/src ./src/
COPY --chown=rust:rust server/migrations ./migrations/
# build for release
# workaround for https://github.com/rust-lang/rust/issues/62896
RUN cargo build --frozen --release
# reduce binary size
RUN strip ./target/$CARGO_BUILD_TARGET/$RUSTRELEASEDIR/lemmy_server
RUN cp ./target/$CARGO_BUILD_TARGET/$RUSTRELEASEDIR/lemmy_server /app/server/
FROM $RUST_BUILDER_IMAGE as docs
WORKDIR /app
COPY --chown=rust:rust docs ./docs
RUN mdbook build docs/
FROM node:12-buster as node
WORKDIR /app/ui
# Cache deps
COPY ui/package.json ui/yarn.lock ./
RUN yarn install --pure-lockfile --network-timeout 600000
# Build
COPY ui /app/ui
RUN yarn build
FROM alpine:3.12 as lemmy
# Install libpq for postgres
RUN apk add libpq
RUN addgroup -g 1000 lemmy
RUN adduser -D -s /bin/sh -u 1000 -G lemmy lemmy
# Copy resources
COPY --chown=lemmy:lemmy server/config/defaults.hjson /config/defaults.hjson
COPY --chown=lemmy:lemmy --from=rust /app/server/lemmy_server /app/lemmy
COPY --chown=lemmy:lemmy --from=docs /app/docs/book/ /app/dist/documentation/
COPY --chown=lemmy:lemmy --from=node /app/ui/dist /app/dist
RUN chown lemmy:lemmy /app/lemmy
USER lemmy
EXPOSE 8536
CMD ["/app/lemmy"]

View file

@ -1,4 +1,5 @@
#!/bin/sh #!/bin/sh
set -e
git checkout master git checkout master
# Import translations # Import translations
@ -20,7 +21,7 @@ git add "server/src/version.rs"
echo $new_tag > "ansible/VERSION" echo $new_tag > "ansible/VERSION"
git add "ansible/VERSION" git add "ansible/VERSION"
cd docker/dev || exit cd docker/prod || exit
# Changing the docker-compose prod # Changing the docker-compose prod
sed -i "s/dessalines\/lemmy:.*/dessalines\/lemmy:$new_tag/" ../prod/docker-compose.yml sed -i "s/dessalines\/lemmy:.*/dessalines\/lemmy:$new_tag/" ../prod/docker-compose.yml
@ -32,41 +33,24 @@ git add ../../ansible/templates/docker-compose.yml
git commit -m"Version $new_tag" git commit -m"Version $new_tag"
git tag $new_tag git tag $new_tag
export COMPOSE_DOCKER_CLI_BUILD=1
export DOCKER_BUILDKIT=1
# Rebuilding docker # Rebuilding docker
docker-compose build
docker tag dev_lemmy:latest dessalines/lemmy:x64-$new_tag
docker push dessalines/lemmy:x64-$new_tag
# Build for Raspberry Pi / other archs
# Arm currently not working
# docker build -t lemmy:armv7hf -f Dockerfile.armv7hf ../../
# docker tag lemmy:armv7hf dessalines/lemmy:armv7hf-$new_tag
# docker push dessalines/lemmy:armv7hf-$new_tag
# aarch64
# Only do this on major releases (IE the third semver is 0)
if [ $third_semver -eq 0 ]; then if [ $third_semver -eq 0 ]; then
# Registering qemu binaries # TODO get linux/arm/v7 build working
docker run --rm --privileged multiarch/qemu-user-static:register --reset # Build for Raspberry Pi / other archs too
docker buildx build --platform linux/amd64,linux/arm64 ../../ \
docker build -t lemmy:aarch64 -f Dockerfile.aarch64 ../../ --file Dockerfile \
docker tag lemmy:aarch64 dessalines/lemmy:arm64-$new_tag --tag dessalines/lemmy:$new_tag \
docker push dessalines/lemmy:arm64-$new_tag --push
fi
# Creating the manifest for the multi-arch build
if [ $third_semver -eq 0 ]; then
docker manifest create dessalines/lemmy:$new_tag \
dessalines/lemmy:x64-$new_tag \
dessalines/lemmy:arm64-$new_tag
else else
docker manifest create dessalines/lemmy:$new_tag \ docker buildx build --platform linux/amd64 ../../ \
dessalines/lemmy:x64-$new_tag --file Dockerfile \
--tag dessalines/lemmy:$new_tag \
--push
fi fi
docker manifest push dessalines/lemmy:$new_tag
# Push # Push
git push origin $new_tag git push origin $new_tag
git push git push

View file

@ -12,7 +12,7 @@ services:
restart: always restart: always
lemmy: lemmy:
image: dessalines/lemmy:v0.6.71 image: dessalines/lemmy:v0.7.0
ports: ports:
- "127.0.0.1:8536:8536" - "127.0.0.1:8536:8536"
restart: always restart: always
@ -22,17 +22,17 @@ services:
- ./lemmy.hjson:/config/config.hjson - ./lemmy.hjson:/config/config.hjson
depends_on: depends_on:
- postgres - postgres
- pictshare - pictrs
- iframely - iframely
pictshare: pictrs:
image: hascheksolutions/pictshare:latest image: asonix/pictrs:v0.1.13-r0
ports: ports:
- "127.0.0.1:8537:80" - "127.0.0.1:8537:8080"
user: 991:991
volumes: volumes:
- ./volumes/pictshare:/usr/share/nginx/html/data - ./volumes/pictrs:/mnt
restart: always restart: always
mem_limit: 100m
iframely: iframely:
image: dogbin/iframely:latest image: dogbin/iframely:latest

View file

@ -0,0 +1,60 @@
#!/bin/bash
set -e
if [[ $(id -u) != 0 ]]; then
echo "This migration needs to be run as root"
exit
fi
if [[ ! -f docker-compose.yml ]]; then
echo "No docker-compose.yml found in current directory. Is this the right folder?"
exit
fi
# Fixing pictrs permissions
mkdir -p volumes/pictrs
sudo chown -R 991:991 volumes/pictrs
echo "Restarting docker-compose, making sure that pictrs is started and pictshare is removed"
docker-compose up -d --remove-orphans
if [[ -z $(docker-compose ps | grep pictrs) ]]; then
echo "Pict-rs is not running, make sure you update Lemmy first"
exit
fi
# echo "Stopping Lemmy so that users dont upload new images during the migration"
# docker-compose stop lemmy
pushd volumes/pictshare/
echo "Importing pictshare images to pict-rs..."
IMAGE_NAMES=*
for image in $IMAGE_NAMES; do
IMAGE_PATH="$(pwd)/$image/$image"
if [[ ! -f $IMAGE_PATH ]]; then
continue
fi
echo -e "\nImporting $IMAGE_PATH"
ret=0
curl --silent --fail -F "images[]=@$IMAGE_PATH" http://127.0.0.1:8537/import || ret=$?
if [[ $ret != 0 ]]; then
echo "Error for $IMAGE_PATH : $ret"
fi
done
echo "Fixing permissions on pictshare folder"
find . -type d -exec chmod 755 {} \;
find . -type f -exec chmod 644 {} \;
popd
echo "Rewrite image links in Lemmy database"
docker-compose exec -u postgres postgres psql -U lemmy -c "UPDATE user_ SET avatar = REPLACE(avatar, 'pictshare', 'pictrs/image') WHERE avatar is not null;"
docker-compose exec -u postgres postgres psql -U lemmy -c "UPDATE post SET url = REPLACE(url, 'pictshare', 'pictrs/image') WHERE url is not null;"
echo "Moving pictshare data folder to pictshare_backup"
mv volumes/pictshare volumes/pictshare_backup
echo "Migration done, starting Lemmy again"
echo "If everything went well, you can delete ./volumes/pictshare_backup/"
docker-compose start lemmy

View file

@ -6,20 +6,25 @@ Make sure you have both docker and docker-compose(>=`1.24.0`) installed. On Ubun
# create a folder for the lemmy files. the location doesnt matter, you can put this anywhere you want # create a folder for the lemmy files. the location doesnt matter, you can put this anywhere you want
mkdir /lemmy mkdir /lemmy
cd /lemmy cd /lemmy
# download default config files # download default config files
wget https://raw.githubusercontent.com/dessalines/lemmy/master/docker/prod/docker-compose.yml wget https://raw.githubusercontent.com/LemmyNet/lemmy/master/docker/prod/docker-compose.yml
wget https://raw.githubusercontent.com/dessalines/lemmy/master/docker/lemmy.hjson wget https://raw.githubusercontent.com/LemmyNet/lemmy/master/docker/lemmy.hjson
wget https://raw.githubusercontent.com/dessalines/lemmy/master/docker/iframely.config.local.js wget https://raw.githubusercontent.com/LemmyNet/lemmy/master/docker/iframely.config.local.js
# Set correct permissions for pictrs folder
mkdir -p volumes/pictrs
sudo chown -R 991:991 volumes/pictrs
``` ```
After this, have a look at the [config file](administration_configuration.md) named `lemmy.hjson`, and adjust it, in particular the hostname, and possibly the db password. Then run: After this, have a look at the [config file](administration_configuration.md) named `lemmy.hjson`, and adjust it, in particular the hostname, and possibly the db password. Then run:
`docker-compose up -d` `docker-compose up -d`
To make Lemmy available outside the server, you need to setup a reverse proxy, like Nginx. [A sample nginx config](https://raw.githubusercontent.com/dessalines/lemmy/master/ansible/templates/nginx.conf), could be setup with: To make Lemmy available outside the server, you need to setup a reverse proxy, like Nginx. [A sample nginx config](https://raw.githubusercontent.com/LemmyNet/lemmy/master/ansible/templates/nginx.conf), could be setup with:
```bash ```bash
wget https://raw.githubusercontent.com/dessalines/lemmy/master/ansible/templates/nginx.conf wget https://raw.githubusercontent.com/LemmyNet/lemmy/master/ansible/templates/nginx.conf
# Replace the {{ vars }} # Replace the {{ vars }}
sudo mv nginx.conf /etc/nginx/sites-enabled/lemmy.conf sudo mv nginx.conf /etc/nginx/sites-enabled/lemmy.conf
``` ```
@ -31,6 +36,6 @@ You will also need to setup TLS, for example with [Let's Encrypt](https://letsen
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: 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 ```bash
wget https://raw.githubusercontent.com/dessalines/lemmy/master/docker/prod/docker-compose.yml wget https://raw.githubusercontent.com/LemmyNet/lemmy/master/docker/prod/docker-compose.yml
docker-compose up -d docker-compose up -d
``` ```

View file

@ -5,7 +5,7 @@ Information about contributing to Lemmy, whether it is translating, testing, des
## Issue tracking / Repositories ## Issue tracking / Repositories
- [GitHub (for issues)](https://github.com/LemmyNet/lemmy) - [GitHub (for issues)](https://github.com/LemmyNet/lemmy)
- [Gitea](https://yerbamate.dev/dessalines/lemmy) - [Gitea](https://yerbamate.dev/LemmyNet/lemmy)
- [GitLab](https://gitlab.com/dessalines/lemmy) - [GitLab](https://gitlab.com/dessalines/lemmy)
## Translating ## Translating

View file

@ -3,11 +3,22 @@
## Running ## Running
```bash ```bash
sudo apt install git docker-compose
git clone https://github.com/LemmyNet/lemmy git clone https://github.com/LemmyNet/lemmy
cd lemmy/docker/dev cd lemmy/docker/dev
./docker_update.sh # This builds and runs it, updating for your changes sudo docker-compose up --no-deps --build
``` ```
and go to http://localhost:8536. and go to http://localhost:8536.
Note that compile times when changing `Cargo.toml` are relatively long with Docker, because builds can't be incrementally cached. If this is a problem for you, you should use [Local Development](contributing_local_development.md). To speed up the Docker compile, add the following to `/etc/docker/daemon.json` and restart Docker.
```
{
"features": {
"buildkit": true
}
}
```
If the build is still too slow, you will have to use a
[local build](contributing_local_development.md) instead.

View file

@ -1,31 +1,67 @@
#### Requirements ### Ubuntu
- [Rust](https://www.rust-lang.org/)
- [Yarn](https://yarnpkg.com/en/)
- [Postgres](https://www.postgresql.org/)
#### Set up Postgres DB #### Build requirements:
```
sudo apt install git cargo libssl-dev pkg-config libpq-dev yarn curl gnupg2 git
# install yarn
curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add -
echo "deb https://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list
sudo apt update && sudo apt install yarn
```
```bash #### Get the source code
```
git clone https://github.com/LemmyNet/lemmy.git
# or alternatively from gitea
# git clone https://yerbamate.dev/LemmyNet/lemmy.git
```
All the following commands need to be run either in `lemmy/server` or `lemmy/ui`, as indicated
by the `cd` command.
#### Build the backend (Rust)
```
cd server cd server
./db-init.sh cargo build
# for development, use `cargo check` instead)
``` ```
Or run the commands manually: #### Build the frontend (Typescript)
```
cd ui
yarn
yarn build
```
```bash #### Setup postgresql
psql -c "create user lemmy with password 'password' superuser;" -U postgres ```
psql -c 'create database lemmy with owner lemmy;' -U postgres sudo apt install postgresql
sudo systemctl start postgresql
# initialize postgres database
sudo -u postgres psql -c "create user lemmy with password 'password' superuser;" -U postgres
sudo -u postgres psql -c 'create database lemmy with owner lemmy;' -U postgres
export LEMMY_DATABASE_URL=postgres://lemmy:password@localhost:5432/lemmy export LEMMY_DATABASE_URL=postgres://lemmy:password@localhost:5432/lemmy
# or execute server/db-init.sh
``` ```
#### Running #### Run a local development instance
```bash
git clone https://github.com/LemmyNet/lemmy
cd lemmy
./install.sh
# For live coding, where both the front and back end, automagically reload on any save, do:
# cd ui && yarn start
# cd server && cargo watch -x run
``` ```
# run each of these in a seperate terminal
cd server && cargo run
ui & yarn start
```
Then open [localhost:4444](http://localhost:4444) in your browser. It will auto-refresh if you edit
any frontend files. For backend coding, you will have to rerun `cargo run`. You can use
`cargo check` as a faster way to find compilation errors.
To speed up incremental builds, you can add the following to `~/.cargo/config`:
```
[target.x86_64-unknown-linux-gnu]
rustflags = ["-Clink-arg=-fuse-ld=lld"]
```
Note that this setup doesn't include image uploads or link previews (provided by pict-rs and
iframely respectively). If you want to test those, you should use the
[Docker development](contributing_docker_development.md).

737
server/Cargo.lock generated vendored

File diff suppressed because it is too large Load diff

10
server/Cargo.toml vendored
View file

@ -4,6 +4,9 @@ version = "0.0.1"
authors = ["Dessalines <tyhou13@gmx.com>"] authors = ["Dessalines <tyhou13@gmx.com>"]
edition = "2018" edition = "2018"
[profile.release]
lto = true
[dependencies] [dependencies]
diesel = { version = "1.4.4", features = ["postgres","chrono","r2d2","64-column-tables","serde_json"] } diesel = { version = "1.4.4", features = ["postgres","chrono","r2d2","64-column-tables","serde_json"] }
diesel_migrations = "1.4.0" diesel_migrations = "1.4.0"
@ -13,7 +16,7 @@ activitystreams-new = { git = "https://git.asonix.dog/asonix/activitystreams-ske
activitystreams-ext = { git = "https://git.asonix.dog/asonix/activitystreams-ext" } activitystreams-ext = { git = "https://git.asonix.dog/asonix/activitystreams-ext" }
bcrypt = "0.8.0" bcrypt = "0.8.0"
chrono = { version = "0.4.7", features = ["serde"] } chrono = { version = "0.4.7", features = ["serde"] }
serde_json = { version = "1.0.48", features = ["preserve_order"]} serde_json = { version = "1.0.52", features = ["preserve_order"]}
failure = "0.1.8" failure = "0.1.8"
serde = { version = "1.0.105", features = ["derive"] } serde = { version = "1.0.105", features = ["derive"] }
actix = "0.9.0" actix = "0.9.0"
@ -34,11 +37,10 @@ lettre_email = "0.9.4"
sha2 = "0.8.1" sha2 = "0.8.1"
rss = "1.9.0" rss = "1.9.0"
htmlescape = "0.3.1" htmlescape = "0.3.1"
config = "0.10.1"
hjson = "0.8.2"
url = { version = "2.1.1", features = ["serde"] } url = { version = "2.1.1", features = ["serde"] }
config = {version = "0.10.1", default-features = false, features = ["hjson"] }
percent-encoding = "2.1.0" percent-encoding = "2.1.0"
isahc = "0.9.2" attohttpc = { version = "0.14.0", default-features = false, features = ["tls-rustls"] }
comrak = "0.7" comrak = "0.7"
openssl = "0.10" openssl = "0.10"
http = "0.2.1" http = "0.2.1"

View file

@ -1,3 +1,4 @@
use super::*;
use crate::{ use crate::{
api::{APIError, Oper, Perform}, api::{APIError, Oper, Perform},
apub::{ apub::{
@ -6,19 +7,8 @@ use crate::{
ActorType, ActorType,
EndpointType, EndpointType,
}, },
db::{ db::{Bannable, Crud, Followable, Joinable, SortType},
community::*, is_valid_community_name,
community_view::*,
moderator::*,
site::*,
user::*,
user_view::*,
Bannable,
Crud,
Followable,
Joinable,
SortType,
},
naive_from_unix, naive_from_unix,
naive_now, naive_now,
slur_check, slur_check,
@ -259,6 +249,10 @@ impl Perform for Oper<CreateCommunity> {
} }
} }
if !is_valid_community_name(&data.name) {
return Err(APIError::err("invalid_community_name").into());
}
let user_id = claims.id; let user_id = claims.id;
let conn = pool.get()?; let conn = pool.get()?;
@ -353,6 +347,10 @@ impl Perform for Oper<EditCommunity> {
Err(_e) => return Err(APIError::err("not_logged_in").into()), Err(_e) => return Err(APIError::err("not_logged_in").into()),
}; };
if !is_valid_community_name(&data.name) {
return Err(APIError::err("invalid_community_name").into());
}
let user_id = claims.id; let user_id = claims.id;
let conn = pool.get()?; let conn = pool.get()?;

View file

@ -1,4 +1,7 @@
use crate::websocket::WebsocketInfo; use crate::{
db::{community::*, community_view::*, moderator::*, site::*, user::*, user_view::*},
websocket::WebsocketInfo,
};
use diesel::{ use diesel::{
r2d2::{ConnectionManager, Pool}, r2d2::{ConnectionManager, Pool},
PgConnection, PgConnection,

View file

@ -17,7 +17,7 @@ use crate::{
Saveable, Saveable,
SortType, SortType,
}, },
fetch_iframely_and_pictshare_data, fetch_iframely_and_pictrs_data,
naive_now, naive_now,
slur_check, slur_check,
slurs_vec_to_str, slurs_vec_to_str,
@ -152,9 +152,9 @@ impl Perform for Oper<CreatePost> {
return Err(APIError::err("site_ban").into()); return Err(APIError::err("site_ban").into());
} }
// Fetch Iframely and Pictshare cached image // Fetch Iframely and pictrs cached image
let (iframely_title, iframely_description, iframely_html, pictshare_thumbnail) = let (iframely_title, iframely_description, iframely_html, pictrs_thumbnail) =
fetch_iframely_and_pictshare_data(data.url.to_owned()); fetch_iframely_and_pictrs_data(data.url.to_owned());
let post_form = PostForm { let post_form = PostForm {
name: data.name.to_owned(), name: data.name.to_owned(),
@ -171,7 +171,7 @@ impl Perform for Oper<CreatePost> {
embed_title: iframely_title, embed_title: iframely_title,
embed_description: iframely_description, embed_description: iframely_description,
embed_html: iframely_html, embed_html: iframely_html,
thumbnail_url: pictshare_thumbnail, thumbnail_url: pictrs_thumbnail,
ap_id: "changeme".into(), ap_id: "changeme".into(),
local: true, local: true,
published: None, published: None,
@ -507,9 +507,9 @@ impl Perform for Oper<EditPost> {
return Err(APIError::err("site_ban").into()); return Err(APIError::err("site_ban").into());
} }
// Fetch Iframely and Pictshare cached image // Fetch Iframely and Pictrs cached image
let (iframely_title, iframely_description, iframely_html, pictshare_thumbnail) = let (iframely_title, iframely_description, iframely_html, pictrs_thumbnail) =
fetch_iframely_and_pictshare_data(data.url.to_owned()); fetch_iframely_and_pictrs_data(data.url.to_owned());
let read_post = Post::read(&conn, data.edit_id)?; let read_post = Post::read(&conn, data.edit_id)?;
@ -528,7 +528,7 @@ impl Perform for Oper<EditPost> {
embed_title: iframely_title, embed_title: iframely_title,
embed_description: iframely_description, embed_description: iframely_description,
embed_html: iframely_html, embed_html: iframely_html,
thumbnail_url: pictshare_thumbnail, thumbnail_url: pictrs_thumbnail,
ap_id: read_post.ap_id, ap_id: read_post.ap_id,
local: read_post.local, local: read_post.local,
published: None, published: None,

View file

@ -5,7 +5,6 @@ use crate::{
use activitystreams::{context, object::properties::ObjectProperties, public, Activity, Base}; use activitystreams::{context, object::properties::ObjectProperties, public, Activity, Base};
use diesel::PgConnection; use diesel::PgConnection;
use failure::{Error, _core::fmt::Debug}; use failure::{Error, _core::fmt::Debug};
use isahc::prelude::*;
use log::debug; use log::debug;
use serde::Serialize; use serde::Serialize;
use url::Url; use url::Url;
@ -57,16 +56,18 @@ where
for t in to { for t in to {
let to_url = Url::parse(&t)?; let to_url = Url::parse(&t)?;
if !is_apub_id_valid(&to_url) { if !is_apub_id_valid(&to_url) {
debug!("Not sending activity to {} (invalid or blocklisted)", t); debug!("Not sending activity to {} (invalid or blacklisted)", t);
continue; continue;
} }
let request = Request::post(t).header("Host", to_url.domain().unwrap()); let mut request = attohttpc::post(t).header("Host", to_url.domain().unwrap());
let signature = sign(&request, actor)?; let signature = sign(&mut request, actor)?;
let res = request let res = request
.header("Signature", signature) .header("Signature", signature)
.header("Content-Type", "application/json") .header("Content-Type", "application/json")
.body(json.to_owned())? .text(json.to_owned())
.send()?; .send()?
.text()?;
debug!("Result for activity send: {:?}", res); debug!("Result for activity send: {:?}", res);
} }
Ok(()) Ok(())

View file

@ -1,8 +1,8 @@
use crate::apub::ActorType; use crate::apub::ActorType;
use activitystreams::ext::Extension; use activitystreams::ext::Extension;
use actix_web::HttpRequest; use actix_web::HttpRequest;
use attohttpc::RequestBuilder;
use failure::Error; use failure::Error;
use http::request::Builder;
use http_signature_normalization::Config; use http_signature_normalization::Config;
use log::debug; use log::debug;
use openssl::{ use openssl::{
@ -35,28 +35,29 @@ pub fn generate_actor_keypair() -> Result<Keypair, Error> {
}) })
} }
// TODO is it possible to create this signature, with just the url and actor?
/// Signs request headers with the given keypair. /// Signs request headers with the given keypair.
pub fn sign(request: &Builder, actor: &dyn ActorType) -> Result<String, Error> { pub fn sign(request: &mut RequestBuilder, actor: &dyn ActorType) -> Result<String, Error> {
let signing_key_id = format!("{}#main-key", actor.actor_id()); let signing_key_id = format!("{}#main-key", actor.actor_id());
let headers = request let headers = request
.headers_ref() .inspect()
.unwrap() .headers()
.iter() .iter()
.map(|h| -> Result<(String, String), Error> { .map(|h| -> Result<(String, String), Error> {
Ok((h.0.as_str().to_owned(), h.1.to_str()?.to_owned())) Ok((h.0.as_str().to_owned(), h.1.to_str()?.to_owned()))
}) })
.collect::<Result<BTreeMap<String, String>, Error>>()?; .collect::<Result<BTreeMap<String, String>, Error>>()?;
let mut path_and_query = request.inspect().url().path().to_owned();
if let Some(query) = request.inspect().url().query() {
path_and_query.push_str(query);
}
let signature_header_value = HTTP_SIG_CONFIG let signature_header_value = HTTP_SIG_CONFIG
.begin_sign( .begin_sign(
request.method_ref().unwrap().as_str(), request.inspect().method().as_str(),
request &path_and_query,
.uri_ref()
.unwrap()
.path_and_query()
.unwrap()
.as_str(),
headers, headers,
)? )?
.sign(signing_key_id, |signing_string| { .sign(signing_key_id, |signing_string| {

View file

@ -2,7 +2,6 @@ use activitystreams::object::Note;
use actix_web::Result; use actix_web::Result;
use diesel::{result::Error::NotFound, PgConnection}; use diesel::{result::Error::NotFound, PgConnection};
use failure::{Error, _core::fmt::Debug}; use failure::{Error, _core::fmt::Debug};
use isahc::prelude::*;
use log::debug; use log::debug;
use serde::Deserialize; use serde::Deserialize;
use std::time::Duration; use std::time::Duration;
@ -64,11 +63,11 @@ where
} }
// TODO: this function should return a future // TODO: this function should return a future
let timeout = Duration::from_secs(60); let timeout = Duration::from_secs(60);
let text = Request::get(url.as_str()) let text: String = attohttpc::get(url.as_str())
.header("Accept", APUB_JSON_CONTENT_TYPE) .header("Accept", APUB_JSON_CONTENT_TYPE)
.connect_timeout(timeout) .connect_timeout(timeout)
.timeout(timeout) .timeout(timeout)
.body(())? // .body(())
.send()? .send()?
.text()?; .text()?;
let res: Response = serde_json::from_str(&text)?; let res: Response = serde_json::from_str(&text)?;

View file

@ -32,7 +32,6 @@ use actix_web::{body::Body, HttpResponse, Result};
use chrono::NaiveDateTime; use chrono::NaiveDateTime;
use diesel::PgConnection; use diesel::PgConnection;
use failure::Error; use failure::Error;
use isahc::prelude::*;
use log::debug; use log::debug;
use serde::Serialize; use serde::Serialize;
use url::Url; use url::Url;
@ -253,7 +252,7 @@ pub fn fetch_webfinger_url(mention: &MentionData) -> Result<String, Error> {
mention.domain mention.domain
); );
debug!("Fetching webfinger url: {}", &fetch_url); debug!("Fetching webfinger url: {}", &fetch_url);
let text = isahc::get(&fetch_url)?.text()?; let text: String = attohttpc::get(&fetch_url).send()?.text()?;
let res: WebFingerResponse = serde_json::from_str(&text)?; let res: WebFingerResponse = serde_json::from_str(&text)?;
let link = res let link = res
.links .links

View file

@ -39,7 +39,6 @@ pub mod websocket;
use crate::settings::Settings; use crate::settings::Settings;
use actix_web::dev::ConnectionInfo; use actix_web::dev::ConnectionInfo;
use chrono::{DateTime, FixedOffset, Local, NaiveDateTime, Utc}; use chrono::{DateTime, FixedOffset, Local, NaiveDateTime, Utc};
use isahc::prelude::*;
use itertools::Itertools; use itertools::Itertools;
use lettre::{ use lettre::{
smtp::{ smtp::{
@ -86,7 +85,8 @@ pub fn is_email_regex(test: &str) -> bool {
} }
pub fn is_image_content_type(test: &str) -> Result<(), failure::Error> { pub fn is_image_content_type(test: &str) -> Result<(), failure::Error> {
if isahc::get(test)? if attohttpc::get(test)
.send()?
.headers() .headers()
.get("Content-Type") .get("Content-Type")
.ok_or_else(|| format_err!("No Content-Type header"))? .ok_or_else(|| format_err!("No Content-Type header"))?
@ -180,30 +180,40 @@ pub struct IframelyResponse {
pub fn fetch_iframely(url: &str) -> Result<IframelyResponse, failure::Error> { pub fn fetch_iframely(url: &str) -> Result<IframelyResponse, failure::Error> {
let fetch_url = format!("http://iframely/oembed?url={}", url); let fetch_url = format!("http://iframely/oembed?url={}", url);
let text = isahc::get(&fetch_url)?.text()?; let text: String = attohttpc::get(&fetch_url).send()?.text()?;
let res: IframelyResponse = serde_json::from_str(&text)?; let res: IframelyResponse = serde_json::from_str(&text)?;
Ok(res) Ok(res)
} }
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug, Clone)]
pub struct PictshareResponse { pub struct PictrsResponse {
status: String, files: Vec<PictrsFile>,
url: String, msg: String,
} }
pub fn fetch_pictshare(image_url: &str) -> Result<PictshareResponse, failure::Error> { #[derive(Deserialize, Debug, Clone)]
pub struct PictrsFile {
file: String,
delete_token: String,
}
pub fn fetch_pictrs(image_url: &str) -> Result<PictrsResponse, failure::Error> {
is_image_content_type(image_url)?; is_image_content_type(image_url)?;
let fetch_url = format!( let fetch_url = format!(
"http://pictshare/api/geturl.php?url={}", "http://pictrs:8080/image/download?url={}",
utf8_percent_encode(image_url, NON_ALPHANUMERIC) utf8_percent_encode(image_url, NON_ALPHANUMERIC) // TODO this might not be needed
); );
let text = isahc::get(&fetch_url)?.text()?; let text = attohttpc::get(&fetch_url).send()?.text()?;
let res: PictshareResponse = serde_json::from_str(&text)?; let res: PictrsResponse = serde_json::from_str(&text)?;
if res.msg == "ok" {
Ok(res) Ok(res)
} else {
Err(format_err!("{}", &res.msg))
}
} }
fn fetch_iframely_and_pictshare_data( fn fetch_iframely_and_pictrs_data(
url: Option<String>, url: Option<String>,
) -> ( ) -> (
Option<String>, Option<String>,
@ -223,20 +233,20 @@ fn fetch_iframely_and_pictshare_data(
} }
}; };
// Fetch pictshare thumbnail // Fetch pictrs thumbnail
let pictshare_thumbnail = match iframely_thumbnail_url { let pictrs_thumbnail = match iframely_thumbnail_url {
Some(iframely_thumbnail_url) => match fetch_pictshare(&iframely_thumbnail_url) { Some(iframely_thumbnail_url) => match fetch_pictrs(&iframely_thumbnail_url) {
Ok(res) => Some(res.url), Ok(res) => Some(res.files[0].file.to_owned()),
Err(e) => { Err(e) => {
error!("pictshare err: {}", e); error!("pictrs err: {}", e);
None None
} }
}, },
// Try to generate a small thumbnail if iframely is not supported // Try to generate a small thumbnail if iframely is not supported
None => match fetch_pictshare(&url) { None => match fetch_pictrs(&url) {
Ok(res) => Some(res.url), Ok(res) => Some(res.files[0].file.to_owned()),
Err(e) => { Err(e) => {
error!("pictshare err: {}", e); error!("pictrs err: {}", e);
None None
} }
}, },
@ -246,7 +256,7 @@ fn fetch_iframely_and_pictshare_data(
iframely_title, iframely_title,
iframely_description, iframely_description,
iframely_html, iframely_html,
pictshare_thumbnail, pictrs_thumbnail,
) )
} }
None => (None, None, None, None), None => (None, None, None, None),
@ -298,11 +308,16 @@ pub fn is_valid_username(name: &str) -> bool {
VALID_USERNAME_REGEX.is_match(name) VALID_USERNAME_REGEX.is_match(name)
} }
pub fn is_valid_community_name(name: &str) -> bool {
VALID_COMMUNITY_NAME_REGEX.is_match(name)
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use crate::{ use crate::{
is_email_regex, is_email_regex,
is_image_content_type, is_image_content_type,
is_valid_community_name,
is_valid_username, is_valid_username,
remove_slurs, remove_slurs,
scrape_text_for_mentions, scrape_text_for_mentions,
@ -344,6 +359,15 @@ mod tests {
assert!(!is_valid_username("")); assert!(!is_valid_username(""));
} }
#[test]
fn test_valid_community_name() {
assert!(is_valid_community_name("example"));
assert!(is_valid_community_name("example_community"));
assert!(!is_valid_community_name("Example"));
assert!(!is_valid_community_name("Ex"));
assert!(!is_valid_community_name(""));
}
#[test] #[test]
fn test_slur_filter() { fn test_slur_filter() {
let test = let test =
@ -402,4 +426,5 @@ lazy_static! {
// static ref WEBFINGER_USER_REGEX: Regex = Regex::new(r"@(?P<name>[\w.]+)@(?P<domain>[a-zA-Z0-9._-]+\.[a-zA-Z0-9_-]+)").unwrap(); // static ref WEBFINGER_USER_REGEX: Regex = Regex::new(r"@(?P<name>[\w.]+)@(?P<domain>[a-zA-Z0-9._-]+\.[a-zA-Z0-9_-]+)").unwrap();
static ref WEBFINGER_USER_REGEX: Regex = Regex::new(r"@(?P<name>[\w.]+)@(?P<domain>[a-zA-Z0-9._:-]+)").unwrap(); static ref WEBFINGER_USER_REGEX: Regex = Regex::new(r"@(?P<name>[\w.]+)@(?P<domain>[a-zA-Z0-9._:-]+)").unwrap();
static ref VALID_USERNAME_REGEX: Regex = Regex::new(r"^[a-zA-Z0-9_]{3,20}$").unwrap(); static ref VALID_USERNAME_REGEX: Regex = Regex::new(r"^[a-zA-Z0-9_]{3,20}$").unwrap();
static ref VALID_COMMUNITY_NAME_REGEX: Regex = Regex::new(r"^[a-z0-9_]{3,20}$").unwrap();
} }

View file

@ -1 +1 @@
pub const VERSION: &str = "v0.6.71"; pub const VERSION: &str = "v0.7.0";

View file

@ -0,0 +1,30 @@
$white: #ffffff;
$orange: #faa077;
$cyan: #02bdc2;
$green: #d4e9d7;
$secondary: $green;
$body-color: $gray-700;
$link-color: theme-color("danger");;
$primary: $orange;
$red: #d8486a;
$border-radius: 1.5rem;
$border-radius-lg: 1.5rem;
$border-radius-sm: 1rem;
$font-family-sans-serif: Guardian-EgypTT,serif,-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
$headings-color: $gray-700;
$input-btn-focus-color: rgba($component-active-bg, .75);
$form-feedback-valid-color: theme-color("info");
$navbar-light-color: $gray-600;
$black: #222222;
$navbar-dark-toggler-border-color: rgba($black, .1);
$navbar-light-active-color: $gray-900;
$card-color: $gray-700;
$card-cap-color: $gray-700;
$info: darken($green, 25%);;
$body-bg: #f2f0f0;
$success: darken($green, 25%);;
$danger: darken($primary, 25%);
$navbar-light-hover-color: $gray-900;
$card-bg: $gray-100;
$border-color: $gray-700;

1
ui/assets/css/themes/litely.min.css vendored Normal file

File diff suppressed because one or more lines are too long

View file

@ -18,6 +18,7 @@ import {
setupTribute, setupTribute,
wsJsonToRes, wsJsonToRes,
emojiPicker, emojiPicker,
pictrsDeleteToast,
} from '../utils'; } from '../utils';
import { WebSocketService, UserService } from '../services'; import { WebSocketService, UserService } from '../services';
import autosize from 'autosize'; import autosize from 'autosize';
@ -60,7 +61,7 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
buttonTitle: !this.props.node buttonTitle: !this.props.node
? capitalizeFirstLetter(i18n.t('post')) ? capitalizeFirstLetter(i18n.t('post'))
: this.props.edit : this.props.edit
? capitalizeFirstLetter(i18n.t('edit')) ? capitalizeFirstLetter(i18n.t('save'))
: capitalizeFirstLetter(i18n.t('reply')), : capitalizeFirstLetter(i18n.t('reply')),
previewMode: false, previewMode: false,
loading: false, loading: false,
@ -137,7 +138,7 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
/> />
{this.state.previewMode && ( {this.state.previewMode && (
<div <div
className="md-div" className="card card-body md-div"
dangerouslySetInnerHTML={mdToHtml( dangerouslySetInnerHTML={mdToHtml(
this.state.commentForm.content this.state.commentForm.content
)} )}
@ -150,7 +151,7 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
<button <button
type="submit" type="submit"
class="btn btn-sm btn-secondary mr-2" class="btn btn-sm btn-secondary mr-2"
disabled={this.props.disabled} disabled={this.props.disabled || this.state.loading}
> >
{this.state.loading ? ( {this.state.loading ? (
<svg class="icon icon-spinner spin"> <svg class="icon icon-spinner spin">
@ -244,7 +245,20 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
}); });
} }
handleFinished() { handleFinished(data: CommentResponse) {
let isReply =
this.props.node !== undefined && data.comment.parent_id !== null;
let xor =
+!(data.comment.parent_id !== null) ^ +(this.props.node !== undefined);
if (
(data.comment.creator_id == UserService.Instance.user.id &&
// If its a reply, make sure parent child match
isReply &&
data.comment.parent_id == this.props.node.comment.id) ||
// Otherwise, check the XOR of the two
(!isReply && xor)
) {
this.state.previewMode = false; this.state.previewMode = false;
this.state.loading = false; this.state.loading = false;
this.state.commentForm.content = ''; this.state.commentForm.content = '';
@ -254,9 +268,10 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
if (this.props.node) { if (this.props.node) {
this.props.onReplyCancel(); this.props.onReplyCancel();
} }
autosize.update(document.querySelector('textarea')); autosize.update(form);
this.setState(this.state); this.setState(this.state);
} }
}
handleCommentSubmit(i: CommentForm, event: any) { handleCommentSubmit(i: CommentForm, event: any) {
event.preventDefault(); event.preventDefault();
@ -305,9 +320,9 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
file = event; file = event;
} }
const imageUploadUrl = `/pictshare/api/upload.php`; const imageUploadUrl = `/pictrs/image`;
const formData = new FormData(); const formData = new FormData();
formData.append('file', file); formData.append('images[]', file);
i.state.imageLoading = true; i.state.imageLoading = true;
i.setState(i.state); i.setState(i.state);
@ -318,9 +333,14 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
}) })
.then(res => res.json()) .then(res => res.json())
.then(res => { .then(res => {
let url = `${window.location.origin}/pictshare/${res.url}`; console.log('pictrs upload:');
let imageMarkdown = console.log(res);
res.filetype == 'mp4' ? `[vid](${url}/raw)` : `![](${url})`; if (res.msg == 'ok') {
let hash = res.files[0].file;
let url = `${window.location.origin}/pictrs/image/${hash}`;
let deleteToken = res.files[0].delete_token;
let deleteUrl = `${window.location.origin}/pictrs/image/delete/${deleteToken}/${hash}`;
let imageMarkdown = `![](${url})`;
let content = i.state.commentForm.content; let content = i.state.commentForm.content;
content = content ? `${content}\n${imageMarkdown}` : imageMarkdown; content = content ? `${content}\n${imageMarkdown}` : imageMarkdown;
i.state.commentForm.content = content; i.state.commentForm.content = content;
@ -328,6 +348,16 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
i.setState(i.state); i.setState(i.state);
let textarea: any = document.getElementById(i.id); let textarea: any = document.getElementById(i.id);
autosize.update(textarea); autosize.update(textarea);
pictrsDeleteToast(
i18n.t('click_to_delete_picture'),
i18n.t('picture_deleted'),
deleteUrl
);
} else {
i.state.imageLoading = false;
i.setState(i.state);
toast(JSON.stringify(res), 'danger');
}
}) })
.catch(error => { .catch(error => {
i.state.imageLoading = false; i.state.imageLoading = false;
@ -343,14 +373,10 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
if (UserService.Instance.user) { if (UserService.Instance.user) {
if (res.op == UserOperation.CreateComment) { if (res.op == UserOperation.CreateComment) {
let data = res.data as CommentResponse; let data = res.data as CommentResponse;
if (data.comment.creator_id == UserService.Instance.user.id) { this.handleFinished(data);
this.handleFinished();
}
} else if (res.op == UserOperation.EditComment) { } else if (res.op == UserOperation.EditComment) {
let data = res.data as CommentResponse; let data = res.data as CommentResponse;
if (data.comment.creator_id == UserService.Instance.user.id) { this.handleFinished(data);
this.handleFinished();
}
} }
} }
} }

View file

@ -132,7 +132,7 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
> >
<div <div
id={`comment-${node.comment.id}`} id={`comment-${node.comment.id}`}
className={`details comment-node border-top border-light ${ className={`details comment-node border-top border-light py-2 ${
this.isCommentNew ? 'mark' : '' this.isCommentNew ? 'mark' : ''
}`} }`}
style={ style={
@ -148,7 +148,7 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
'ml-2' 'ml-2'
}`} }`}
> >
<div class="d-flex flex-wrap align-items-center mb-1 mt-1 text-muted small"> <div class="d-flex flex-wrap align-items-center text-muted small">
<span class="mr-2"> <span class="mr-2">
<UserListing <UserListing
user={{ user={{
@ -297,25 +297,6 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
)} )}
</button> </button>
)} )}
<button
class="btn btn-link btn-animate text-muted"
onClick={linkEvent(this, this.handleSaveCommentClick)}
data-tippy-content={
node.comment.saved ? i18n.t('unsave') : i18n.t('save')
}
>
{this.state.saveLoading ? (
this.loadingIcon
) : (
<svg
class={`icon icon-inline ${
node.comment.saved && 'text-warning'
}`}
>
<use xlinkHref="#icon-star"></use>
</svg>
)}
</button>
<button <button
class="btn btn-link btn-animate text-muted" class="btn btn-link btn-animate text-muted"
onClick={linkEvent(this, this.handleReplyClick)} onClick={linkEvent(this, this.handleReplyClick)}
@ -351,6 +332,30 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
</button> </button>
)} )}
{!this.props.showContext && this.linkBtn} {!this.props.showContext && this.linkBtn}
<button
class="btn btn-link btn-animate text-muted"
onClick={linkEvent(
this,
this.handleSaveCommentClick
)}
data-tippy-content={
node.comment.saved
? i18n.t('unsave')
: i18n.t('save')
}
>
{this.state.saveLoading ? (
this.loadingIcon
) : (
<svg
class={`icon icon-inline ${
node.comment.saved && 'text-warning'
}`}
>
<use xlinkHref="#icon-star"></use>
</svg>
)}
</button>
<button <button
className="btn btn-link btn-animate text-muted" className="btn btn-link btn-animate text-muted"
onClick={linkEvent(this, this.handleViewSource)} onClick={linkEvent(this, this.handleViewSource)}

View file

@ -207,7 +207,11 @@ export class CommunityForm extends Component<
)} )}
<div class="form-group row"> <div class="form-group row">
<div class="col-12"> <div class="col-12">
<button type="submit" class="btn btn-secondary mr-2"> <button
type="submit"
class="btn btn-secondary mr-2"
disabled={this.state.loading}
>
{this.state.loading ? ( {this.state.loading ? (
<svg class="icon icon-spinner spin"> <svg class="icon icon-spinner spin">
<use xlinkHref="#icon-spinner"></use> <use xlinkHref="#icon-spinner"></use>

View file

@ -123,7 +123,10 @@ export class Inbox extends Component<any, InboxState> {
this.state.unreadOrAll == UnreadOrAll.Unread && ( this.state.unreadOrAll == UnreadOrAll.Unread && (
<ul class="list-inline mb-1 text-muted small font-weight-bold"> <ul class="list-inline mb-1 text-muted small font-weight-bold">
<li className="list-inline-item"> <li className="list-inline-item">
<span class="pointer" onClick={this.markAllAsRead}> <span
class="pointer"
onClick={linkEvent(this, this.markAllAsRead)}
>
{i18n.t('mark_all_as_read')} {i18n.t('mark_all_as_read')}
</span> </span>
</li> </li>
@ -392,8 +395,14 @@ export class Inbox extends Component<any, InboxState> {
this.refetch(); this.refetch();
} }
markAllAsRead() { markAllAsRead(i: Inbox) {
WebSocketService.Instance.markAllAsRead(); WebSocketService.Instance.markAllAsRead();
i.state.replies = [];
i.state.mentions = [];
i.state.messages = [];
i.sendUnreadCount();
window.scrollTo(0, 0);
i.setState(i.state);
} }
parseMessage(msg: WebSocketJsonResponse) { parseMessage(msg: WebSocketJsonResponse) {
@ -447,12 +456,7 @@ export class Inbox extends Component<any, InboxState> {
this.setState(this.state); this.setState(this.state);
setupTippy(); setupTippy();
} else if (res.op == UserOperation.MarkAllAsRead) { } else if (res.op == UserOperation.MarkAllAsRead) {
this.state.replies = []; // Moved to be instant
this.state.mentions = [];
this.state.messages = [];
this.sendUnreadCount();
window.scrollTo(0, 0);
this.setState(this.state);
} else if (res.op == UserOperation.EditComment) { } else if (res.op == UserOperation.EditComment) {
let data = res.data as CommentResponse; let data = res.data as CommentResponse;
editCommentRes(data, this.state.replies); editCommentRes(data, this.state.replies);

View file

@ -111,6 +111,7 @@ export class Login extends Component<any, State> {
required required
/> />
<button <button
type="button"
disabled={!validEmail(this.state.loginForm.username_or_email)} disabled={!validEmail(this.state.loginForm.username_or_email)}
onClick={linkEvent(this, this.handlePasswordReset)} onClick={linkEvent(this, this.handlePasswordReset)}
className="btn p-0 btn-link d-inline-block float-right text-muted small font-weight-bold" className="btn p-0 btn-link d-inline-block float-right text-muted small font-weight-bold"

View file

@ -22,7 +22,7 @@ import {
} from '../interfaces'; } from '../interfaces';
import { import {
wsJsonToRes, wsJsonToRes,
pictshareAvatarThumbnail, pictrsAvatarThumbnail,
showAvatars, showAvatars,
fetchLimit, fetchLimit,
isCommentType, isCommentType,
@ -218,7 +218,7 @@ export class Navbar extends Component<any, NavbarState> {
<span> <span>
{UserService.Instance.user.avatar && showAvatars() && ( {UserService.Instance.user.avatar && showAvatars() && (
<img <img
src={pictshareAvatarThumbnail( src={pictrsAvatarThumbnail(
UserService.Instance.user.avatar UserService.Instance.user.avatar
)} )}
height="32" height="32"

View file

@ -36,6 +36,7 @@ import {
setupTippy, setupTippy,
emojiPicker, emojiPicker,
hostname, hostname,
pictrsDeleteToast,
} from '../utils'; } from '../utils';
import autosize from 'autosize'; import autosize from 'autosize';
import Tribute from 'tributejs/src/Tribute.js'; import Tribute from 'tributejs/src/Tribute.js';
@ -284,7 +285,7 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
/> />
{this.state.previewMode && ( {this.state.previewMode && (
<div <div
className="md-div" className="card card-body md-div"
dangerouslySetInnerHTML={mdToHtml(this.state.postForm.body)} dangerouslySetInnerHTML={mdToHtml(this.state.postForm.body)}
/> />
)} )}
@ -364,7 +365,9 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
<div class="form-group row"> <div class="form-group row">
<div class="col-sm-10"> <div class="col-sm-10">
<button <button
disabled={!this.state.postForm.community_id} disabled={
!this.state.postForm.community_id || this.state.loading
}
type="submit" type="submit"
class="btn btn-secondary mr-2" class="btn btn-secondary mr-2"
> >
@ -410,6 +413,12 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
handlePostSubmit(i: PostForm, event: any) { handlePostSubmit(i: PostForm, event: any) {
event.preventDefault(); event.preventDefault();
// Coerce empty url string to undefined
if (i.state.postForm.url && i.state.postForm.url === '') {
i.state.postForm.url = undefined;
}
if (i.props.post) { if (i.props.post) {
WebSocketService.Instance.editPost(i.state.postForm); WebSocketService.Instance.editPost(i.state.postForm);
} else { } else {
@ -523,9 +532,9 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
file = event; file = event;
} }
const imageUploadUrl = `/pictshare/api/upload.php`; const imageUploadUrl = `/pictrs/image`;
const formData = new FormData(); const formData = new FormData();
formData.append('file', file); formData.append('images[]', file);
i.state.imageLoading = true; i.state.imageLoading = true;
i.setState(i.state); i.setState(i.state);
@ -536,13 +545,26 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
}) })
.then(res => res.json()) .then(res => res.json())
.then(res => { .then(res => {
let url = `${window.location.origin}/pictshare/${encodeURI(res.url)}`; console.log('pictrs upload:');
if (res.filetype == 'mp4') { console.log(res);
url += '/raw'; if (res.msg == 'ok') {
} let hash = res.files[0].file;
let url = `${window.location.origin}/pictrs/image/${hash}`;
let deleteToken = res.files[0].delete_token;
let deleteUrl = `${window.location.origin}/pictrs/image/delete/${deleteToken}/${hash}`;
i.state.postForm.url = url; i.state.postForm.url = url;
i.state.imageLoading = false; i.state.imageLoading = false;
i.setState(i.state); i.setState(i.state);
pictrsDeleteToast(
i18n.t('click_to_delete_picture'),
i18n.t('picture_deleted'),
deleteUrl
);
} else {
i.state.imageLoading = false;
i.setState(i.state);
toast(JSON.stringify(res), 'danger');
}
}) })
.catch(error => { .catch(error => {
i.state.imageLoading = false; i.state.imageLoading = false;

View file

@ -29,7 +29,7 @@ import {
isImage, isImage,
isVideo, isVideo,
getUnixTime, getUnixTime,
pictshareImage, pictrsImage,
setupTippy, setupTippy,
hostname, hostname,
previewLines, previewLines,
@ -163,15 +163,15 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
getImage(thumbnail: boolean = false) { getImage(thumbnail: boolean = false) {
let post = this.props.post; let post = this.props.post;
if (isImage(post.url)) { if (isImage(post.url)) {
if (post.url.includes('pictshare')) { if (post.url.includes('pictrs')) {
return pictshareImage(post.url, thumbnail); return pictrsImage(post.url, thumbnail);
} else if (post.thumbnail_url) { } else if (post.thumbnail_url) {
return pictshareImage(post.thumbnail_url, thumbnail); return pictrsImage(post.thumbnail_url, thumbnail);
} else { } else {
return post.url; return post.url;
} }
} else if (post.thumbnail_url) { } else if (post.thumbnail_url) {
return pictshareImage(post.thumbnail_url, thumbnail); return pictrsImage(post.thumbnail_url, thumbnail);
} }
} }

View file

@ -157,7 +157,7 @@ export class PrivateMessageForm extends Component<
/> />
{this.state.previewMode && ( {this.state.previewMode && (
<div <div
className="md-div" className="card card-body md-div"
dangerouslySetInnerHTML={mdToHtml( dangerouslySetInnerHTML={mdToHtml(
this.state.privateMessageForm.content this.state.privateMessageForm.content
)} )}
@ -186,7 +186,11 @@ export class PrivateMessageForm extends Component<
)} )}
<div class="form-group row"> <div class="form-group row">
<div class="offset-sm-2 col-sm-10"> <div class="offset-sm-2 col-sm-10">
<button type="submit" class="btn btn-secondary mr-2"> <button
type="submit"
class="btn btn-secondary mr-2"
disabled={this.state.loading}
>
{this.state.loading ? ( {this.state.loading ? (
<svg class="icon icon-spinner spin"> <svg class="icon icon-spinner spin">
<use xlinkHref="#icon-spinner"></use> <use xlinkHref="#icon-spinner"></use>

View file

@ -5,12 +5,7 @@ import {
EditPrivateMessageForm, EditPrivateMessageForm,
} from '../interfaces'; } from '../interfaces';
import { WebSocketService, UserService } from '../services'; import { WebSocketService, UserService } from '../services';
import { import { mdToHtml, pictrsAvatarThumbnail, showAvatars, toast } from '../utils';
mdToHtml,
pictshareAvatarThumbnail,
showAvatars,
toast,
} from '../utils';
import { MomentTime } from './moment-time'; import { MomentTime } from './moment-time';
import { PrivateMessageForm } from './private-message-form'; import { PrivateMessageForm } from './private-message-form';
import { i18n } from '../i18next'; import { i18n } from '../i18next';
@ -78,7 +73,7 @@ export class PrivateMessage extends Component<
<img <img
height="32" height="32"
width="32" width="32"
src={pictshareAvatarThumbnail( src={pictrsAvatarThumbnail(
this.mine this.mine
? message.recipient_avatar ? message.recipient_avatar
: message.creator_avatar : message.creator_avatar

View file

@ -78,7 +78,7 @@ export class SiteForm extends Component<SiteFormProps, SiteFormState> {
<form onSubmit={linkEvent(this, this.handleCreateSiteSubmit)}> <form onSubmit={linkEvent(this, this.handleCreateSiteSubmit)}>
<h5>{`${ <h5>{`${
this.props.site this.props.site
? capitalizeFirstLetter(i18n.t('edit')) ? capitalizeFirstLetter(i18n.t('save'))
: capitalizeFirstLetter(i18n.t('name')) : capitalizeFirstLetter(i18n.t('name'))
} ${i18n.t('your_site')}`}</h5> } ${i18n.t('your_site')}`}</h5>
<div class="form-group row"> <div class="form-group row">
@ -175,7 +175,11 @@ export class SiteForm extends Component<SiteFormProps, SiteFormState> {
</div> </div>
<div class="form-group row"> <div class="form-group row">
<div class="col-12"> <div class="col-12">
<button type="submit" class="btn btn-secondary mr-2"> <button
type="submit"
class="btn btn-secondary mr-2"
disabled={this.state.loading}
>
{this.state.loading ? ( {this.state.loading ? (
<svg class="icon icon-spinner spin"> <svg class="icon icon-spinner spin">
<use xlinkHref="#icon-spinner"></use> <use xlinkHref="#icon-spinner"></use>

View file

@ -1,7 +1,7 @@
import { Component } from 'inferno'; import { Component } from 'inferno';
import { Link } from 'inferno-router'; import { Link } from 'inferno-router';
import { UserView } from '../interfaces'; import { UserView } from '../interfaces';
import { pictshareAvatarThumbnail, showAvatars, hostname } from '../utils'; import { pictrsAvatarThumbnail, showAvatars, hostname } from '../utils';
interface UserOther { interface UserOther {
name: string; name: string;
@ -40,7 +40,7 @@ export class UserListing extends Component<UserListingProps, any> {
<img <img
height="32" height="32"
width="32" width="32"
src={pictshareAvatarThumbnail(user.avatar)} src={pictrsAvatarThumbnail(user.avatar)}
class="rounded-circle mr-2" class="rounded-circle mr-2"
/> />
)} )}

View file

@ -927,7 +927,7 @@ export class User extends Component<any, UserState> {
handleUserSettingsThemeChange(i: User, event: any) { handleUserSettingsThemeChange(i: User, event: any) {
i.state.userSettingsForm.theme = event.target.value; i.state.userSettingsForm.theme = event.target.value;
setTheme(event.target.value); setTheme(event.target.value, true);
i.setState(i.state); i.setState(i.state);
} }
@ -993,9 +993,9 @@ export class User extends Component<any, UserState> {
handleImageUpload(i: User, event: any) { handleImageUpload(i: User, event: any) {
event.preventDefault(); event.preventDefault();
let file = event.target.files[0]; let file = event.target.files[0];
const imageUploadUrl = `/pictshare/api/upload.php`; const imageUploadUrl = `/pictrs/image`;
const formData = new FormData(); const formData = new FormData();
formData.append('file', file); formData.append('images[]', file);
i.state.avatarLoading = true; i.state.avatarLoading = true;
i.setState(i.state); i.setState(i.state);
@ -1006,14 +1006,19 @@ export class User extends Component<any, UserState> {
}) })
.then(res => res.json()) .then(res => res.json())
.then(res => { .then(res => {
let url = `${window.location.origin}/pictshare/${res.url}`; console.log('pictrs upload:');
if (res.filetype == 'mp4') { console.log(res);
url += '/raw'; if (res.msg == 'ok') {
} let hash = res.files[0].file;
let url = `${window.location.origin}/pictrs/image/${hash}`;
i.state.userSettingsForm.avatar = url; i.state.userSettingsForm.avatar = url;
console.log(url);
i.state.avatarLoading = false; i.state.avatarLoading = false;
i.setState(i.state); i.setState(i.state);
} else {
i.state.avatarLoading = false;
i.setState(i.state);
toast(JSON.stringify(res), 'danger');
}
}) })
.catch(error => { .catch(error => {
i.state.avatarLoading = false; i.state.avatarLoading = false;

3
ui/src/index.html vendored
View file

@ -15,7 +15,8 @@
<link rel="stylesheet" type="text/css" href="/static/assets/css/toastify.css" /> <link rel="stylesheet" type="text/css" href="/static/assets/css/toastify.css" />
<link rel="stylesheet" type="text/css" href="/static/assets/css/selectr.min.css" /> <link rel="stylesheet" type="text/css" href="/static/assets/css/selectr.min.css" />
<link rel="stylesheet" type="text/css" href="/static/assets/css/tippy.css" /> <link rel="stylesheet" type="text/css" href="/static/assets/css/tippy.css" />
<link rel="stylesheet" type="text/css" href="/static/assets/css/themes/darkly.min.css" id="darkly" /> <link rel="stylesheet" type="text/css" href="/static/assets/css/themes/litely.min.css" id="default-light" media="(prefers-color-scheme: light)" />
<link rel="stylesheet" type="text/css" href="/static/assets/css/themes/darkly.min.css" id="default-dark" media="(prefers-color-scheme: no-preference), (prefers-color-scheme: dark)" />
<link rel="stylesheet" type="text/css" href="/static/assets/css/main.css" /> <link rel="stylesheet" type="text/css" href="/static/assets/css/main.css" />
<!-- Scripts --> <!-- Scripts -->

View file

@ -41,9 +41,7 @@ export class UserService {
private setUser(jwt: string) { private setUser(jwt: string) {
this.user = jwt_decode(jwt); this.user = jwt_decode(jwt);
if (this.user.theme != 'darkly') { setTheme(this.user.theme, true);
setTheme(this.user.theme);
}
this.sub.next({ user: this.user }); this.sub.next({ user: this.user });
console.log(this.user); console.log(this.user);
} }

92
ui/src/utils.ts vendored
View file

@ -103,6 +103,7 @@ export const themes = [
'vaporwave', 'vaporwave',
'vaporwave-dark', 'vaporwave-dark',
'i386', 'i386',
'litely',
]; ];
export const emojiPicker = new EmojiButton({ export const emojiPicker = new EmojiButton({
@ -113,11 +114,26 @@ export const emojiPicker = new EmojiButton({
// TODO i18n // TODO i18n
}); });
export function randomStr() { const DEFAULT_ALPHABET =
return Math.random() 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
.toString(36)
.replace(/[^a-z]+/g, '') function getRandomCharFromAlphabet(alphabet: string): string {
.substr(2, 10); return alphabet.charAt(Math.floor(Math.random() * alphabet.length));
}
export function randomStr(
idDesiredLength: number = 20,
alphabet = DEFAULT_ALPHABET
): string {
/**
* Create n-long array and map it to random chars from given alphabet.
* Then join individual chars as string
*/
return Array.from({ length: idDesiredLength })
.map(() => {
return getRandomCharFromAlphabet(alphabet);
})
.join('');
} }
export function wsJsonToRes(msg: WebSocketJsonResponse): WebSocketResponse { export function wsJsonToRes(msg: WebSocketJsonResponse): WebSocketResponse {
@ -404,7 +420,7 @@ export function getMomentLanguage(): string {
return lang; return lang;
} }
export function setTheme(theme: string = 'darkly') { export function setTheme(theme: string = 'darkly', loggedIn: boolean = false) {
// unload all the other themes // unload all the other themes
for (var i = 0; i < themes.length; i++) { for (var i = 0; i < themes.length; i++) {
let styleSheet = document.getElementById(themes[i]); let styleSheet = document.getElementById(themes[i]);
@ -413,11 +429,24 @@ export function setTheme(theme: string = 'darkly') {
} }
} }
// if the user is not logged in, we load the default themes and let the browser decide
if (!loggedIn) {
document.getElementById('default-light').removeAttribute('disabled');
document.getElementById('default-dark').removeAttribute('disabled');
} else {
document
.getElementById('default-light')
.setAttribute('disabled', 'disabled');
document
.getElementById('default-dark')
.setAttribute('disabled', 'disabled');
// Load the theme dynamically // Load the theme dynamically
let cssLoc = `/static/assets/css/themes/${theme}.min.css`; let cssLoc = `/static/assets/css/themes/${theme}.min.css`;
loadCss(theme, cssLoc); loadCss(theme, cssLoc);
document.getElementById(theme).removeAttribute('disabled'); document.getElementById(theme).removeAttribute('disabled');
} }
}
export function loadCss(id: string, loc: string) { export function loadCss(id: string, loc: string) {
if (!document.getElementById(id)) { if (!document.getElementById(id)) {
@ -440,10 +469,12 @@ export function objectFlip(obj: any) {
return ret; return ret;
} }
export function pictshareAvatarThumbnail(src: string): string { export function pictrsAvatarThumbnail(src: string): string {
// sample url: http://localhost:8535/pictshare/gs7xuu.jpg // sample url: http://localhost:8535/pictrs/image/thumbnail256/gs7xuu.jpg
let split = src.split('pictshare'); let split = src.split('/pictrs/image');
let out = `${split[0]}pictshare/${canUseWebP() ? 'webp/' : ''}96${split[1]}`; let out = `${split[0]}/pictrs/image/${
canUseWebP() ? 'webp/' : ''
}thumbnail96${split[1]}`;
return out; return out;
} }
@ -455,21 +486,18 @@ export function showAvatars(): boolean {
} }
// Converts to image thumbnail // Converts to image thumbnail
export function pictshareImage( export function pictrsImage(hash: string, thumbnail: boolean = false): string {
hash: string, let root = `/pictrs/image`;
thumbnail: boolean = false
): string {
let root = `/pictshare`;
// Necessary for other servers / domains // Necessary for other servers / domains
if (hash.includes('pictshare')) { if (hash.includes('pictrs')) {
let split = hash.split('/pictshare/'); let split = hash.split('/pictrs/image/');
root = `${split[0]}/pictshare`; root = `${split[0]}/pictrs/image`;
hash = split[1]; hash = split[1];
} }
let out = `${root}/${canUseWebP() ? 'webp/' : ''}${ let out = `${root}/${canUseWebP() ? 'webp/' : ''}${
thumbnail ? '192/' : '' thumbnail ? 'thumbnail256/' : ''
}${hash}`; }${hash}`;
return out; return out;
} }
@ -488,6 +516,29 @@ export function toast(text: string, background: string = 'success') {
}).showToast(); }).showToast();
} }
export function pictrsDeleteToast(
clickToDeleteText: string,
deletePictureText: string,
deleteUrl: string
) {
let backgroundColor = `var(--light)`;
let toast = Toastify({
text: clickToDeleteText,
backgroundColor: backgroundColor,
gravity: 'top',
position: 'right',
duration: 0,
onClick: () => {
if (toast) {
window.location.replace(deleteUrl);
alert(deletePictureText);
toast.hideToast();
}
},
close: true,
}).showToast();
}
export function messageToastify( export function messageToastify(
creator: string, creator: string,
avatar: string, avatar: string,
@ -501,7 +552,7 @@ export function messageToastify(
text: `${body}<br />${creator}`, text: `${body}<br />${creator}`,
avatar: avatar, avatar: avatar,
backgroundColor: backgroundColor, backgroundColor: backgroundColor,
className: 'text-body', className: 'text-dark',
close: true, close: true,
gravity: 'top', gravity: 'top',
position: 'right', position: 'right',
@ -910,7 +961,6 @@ function canUseWebP() {
return false; return false;
// var elem = document.createElement('canvas'); // var elem = document.createElement('canvas');
// if (!!(elem.getContext && elem.getContext('2d'))) { // if (!!(elem.getContext && elem.getContext('2d'))) {
// var testString = !(window.mozInnerScreenX == null) ? 'png' : 'webp'; // var testString = !(window.mozInnerScreenX == null) ? 'png' : 'webp';
// // was able or not to get WebP representation // // was able or not to get WebP representation

2
ui/src/version.ts vendored
View file

@ -1 +1 @@
export const version: string = 'v0.6.71'; export const version: string = 'v0.7.0';

View file

@ -27,6 +27,7 @@
"number_of_communities": "{{count}} Community", "number_of_communities": "{{count}} Community",
"number_of_communities_plural": "{{count}} Communities", "number_of_communities_plural": "{{count}} Communities",
"community_reqs": "lowercase, underscores, and no spaces.", "community_reqs": "lowercase, underscores, and no spaces.",
"invalid_community_name": "Invalid name.",
"create_private_message": "Create Private Message", "create_private_message": "Create Private Message",
"send_secure_message": "Send Secure Message", "send_secure_message": "Send Secure Message",
"send_message": "Send Message", "send_message": "Send Message",
@ -75,6 +76,8 @@
"delete_account": "Delete Account", "delete_account": "Delete Account",
"delete_account_confirm": "delete_account_confirm":
"Warning: this will permanently delete all your data. Enter your password to confirm.", "Warning: this will permanently delete all your data. Enter your password to confirm.",
"click_to_delete_picture": "Click to delete picture.",
"picture_deleted": "Picture deleted.",
"restore": "restore", "restore": "restore",
"ban": "ban", "ban": "ban",
"ban_from_site": "ban from site", "ban_from_site": "ban from site",

View file

@ -5,7 +5,7 @@
"create_a_post": "Crear una publicación", "create_a_post": "Crear una publicación",
"create_post": "Crear Publicación", "create_post": "Crear Publicación",
"number_of_posts": "{{count}} Publicación", "number_of_posts": "{{count}} Publicación",
"number_of_posts_plural": "{{count}} Publicaciónes", "number_of_posts_plural": "{{count}} Publicaciones",
"posts": "Publicaciones", "posts": "Publicaciones",
"related_posts": "Estas publicaciones podrían estar relacionadas", "related_posts": "Estas publicaciones podrían estar relacionadas",
"cross_posts": "Este link también ha sido publicado en:", "cross_posts": "Este link también ha sido publicado en:",
@ -57,16 +57,16 @@
"remove_as_admin": "eliminar como administrador", "remove_as_admin": "eliminar como administrador",
"appoint_as_admin": "designar como administrador", "appoint_as_admin": "designar como administrador",
"remove": "eliminar", "remove": "eliminar",
"removed": "eliminado", "removed": "eliminado por moderador",
"locked": "bloqueado", "locked": "bloqueado",
"stickied": "fijado", "stickied": "fijado",
"reason": "Razón", "reason": "Razón",
"mark_as_read": "marcar como leído", "mark_as_read": "marcar como leído",
"mark_as_unread": "marcar como no leído", "mark_as_unread": "marcar como no leído",
"delete": "eliminar", "delete": "eliminar",
"deleted": "eliminado", "deleted": "eliminado por creador",
"delete_account": "Eliminar Cuenta", "delete_account": "Eliminar Cuenta",
"delete_account_confirm": "Aviso: esta acción eliminará permanentemente tu información. Introduce tu contraseña para continuar", "delete_account_confirm": "Advertencia: esta acción eliminará permanentemente toda tu información. Introduce tu contraseña para confirmar.",
"restore": "restaurar", "restore": "restaurar",
"ban": "expulsar", "ban": "expulsar",
"ban_from_site": "expulsar del sitio", "ban_from_site": "expulsar del sitio",
@ -169,7 +169,7 @@
"theme": "Tema", "theme": "Tema",
"sponsors": "Patrocinadores", "sponsors": "Patrocinadores",
"sponsors_of_lemmy": "Patrocinadores de Lemmy", "sponsors_of_lemmy": "Patrocinadores de Lemmy",
"sponsor_message": "Lemmy es software libre y de <1>código abierto</1>, lo que significa que no tendrá publicidades, monetización, ni capitales emprendedores, nunca. Tus donaciones apoyan directamente el desarrollo a tiempo completo del proyecto. Muchas gracias a las siguientes personas:", "sponsor_message": "Lemmy es software libre y de <1>código abierto</1>, lo que significa que nunca tendrá publicidad, monetización, ni capitales emprendedores. Tus donaciones apoyan directamente el desarrollo a tiempo completo del proyecto. Muchas gracias a las siguientes personas:",
"support_on_patreon": "Apoyo en Patreon", "support_on_patreon": "Apoyo en Patreon",
"support_on_liberapay": "Apoyo en Liberapay", "support_on_liberapay": "Apoyo en Liberapay",
"donate_to_lemmy": "Donar a Lemmy", "donate_to_lemmy": "Donar a Lemmy",
@ -250,6 +250,8 @@
"banned_users": "Usuarios Baneados", "banned_users": "Usuarios Baneados",
"support_on_open_collective": "Dona en OpenCollective", "support_on_open_collective": "Dona en OpenCollective",
"site_saved": "Sitio Guardado.", "site_saved": "Sitio Guardado.",
"emoji_picker": "Emoji Picker", "emoji_picker": "Lista de emojis",
"admin_settings": "Panel de Administración" "admin_settings": "Panel de Administración",
"select_a_community": "Selecciona una comunidad",
"invalid_username": "Nombre de usuario inválido."
} }

View file

@ -28,7 +28,7 @@
"community_reqs": "en minuscule, sans espace et avec tiret du bas.", "community_reqs": "en minuscule, sans espace et avec tiret du bas.",
"create_private_message": "Créer un message privé", "create_private_message": "Créer un message privé",
"send_secure_message": "Envoyer le message sécurisé", "send_secure_message": "Envoyer le message sécurisé",
"send_message": "Enovyer le message", "send_message": "Envoyer le message",
"message": "Message", "message": "Message",
"edit": "éditer", "edit": "éditer",
"reply": "répondre", "reply": "répondre",
@ -36,7 +36,7 @@
"preview": "prévisualiser", "preview": "prévisualiser",
"upload_image": "envoyer une image", "upload_image": "envoyer une image",
"avatar": "Avatar", "avatar": "Avatar",
"upload_avatar": "Télécharger une avatar", "upload_avatar": "Télécharger un avatar",
"show_avatars": "Afficher les avatars", "show_avatars": "Afficher les avatars",
"formatting_help": "aide au formattage", "formatting_help": "aide au formattage",
"view_source": "voir la source", "view_source": "voir la source",
@ -58,14 +58,14 @@
"remove_as_admin": "Supprimer comme admin", "remove_as_admin": "Supprimer comme admin",
"appoint_as_admin": "Nommer comme admin", "appoint_as_admin": "Nommer comme admin",
"remove": "retirer", "remove": "retirer",
"removed": "retiré", "removed": "retiré par le modérateur",
"locked": "bloqué", "locked": "bloqué",
"stickied": "épinglé", "stickied": "épinglé",
"reason": "Raison", "reason": "Raison",
"mark_as_read": "marquer comme lu", "mark_as_read": "marquer comme lu",
"mark_as_unread": "marquer comme non-lu", "mark_as_unread": "marquer comme non-lu",
"delete": "supprimer", "delete": "supprimer",
"deleted": "supprimé", "deleted": "supprimé par le créateur",
"delete_account": "Supprimer le compte", "delete_account": "Supprimer le compte",
"delete_account_confirm": "Avertissement : cette action supprimera toutes vos données de façons permanente ! Saisissez votre mot de passe pour confirmer.", "delete_account_confirm": "Avertissement : cette action supprimera toutes vos données de façons permanente ! Saisissez votre mot de passe pour confirmer.",
"restore": "restaurer", "restore": "restaurer",
@ -171,7 +171,7 @@
"theme": "Thème", "theme": "Thème",
"sponsors": "Sponsors", "sponsors": "Sponsors",
"sponsors_of_lemmy": "Sponsors de Lemmy", "sponsors_of_lemmy": "Sponsors de Lemmy",
"sponsor_message": "Lemmy est un logiciel libre et <1>open-source</1>, cest à dire, il fonctionne sans publicité et sans monétisation aucune. Vos dons soutiennent directement le développement du projet à temps plein. Merci à toutes ces personnes :", "sponsor_message": "Lemmy est un logiciel libre et <1>open-source</1>, sans jamais aucune publicité, ni monétisation ou capital-risque. Vos dons soutiennent directement le développement du projet à temps plein. Merci à toutes ces personnes :",
"support_on_patreon": "Soutenir sur Patreon", "support_on_patreon": "Soutenir sur Patreon",
"support_on_liberapay": "Soutenir sur Liberapay", "support_on_liberapay": "Soutenir sur Liberapay",
"donate_to_lemmy": "Faire un don à Lemmy", "donate_to_lemmy": "Faire un don à Lemmy",
@ -251,5 +251,7 @@
"number_of_downvotes_plural": "{{count}} votes contre", "number_of_downvotes_plural": "{{count}} votes contre",
"downvote": "Voter contre", "downvote": "Voter contre",
"emoji_picker": "Sélecteur démojis", "emoji_picker": "Sélecteur démojis",
"silver_sponsors": "Les sponsors argent sont ceux et celles qui ont fait une donation de 40$ à Lemmy." "silver_sponsors": "Les sponsors argent sont ceux et celles qui ont fait une donation de 40$ à Lemmy.",
"select_a_community": "Sélectionner une communauté",
"invalid_username": "Nom d'utilisateur invalide."
} }

View file

@ -1 +1,107 @@
{} {
"post": "Elküld",
"remove_post": "Bejegyzés eltávolítása",
"no_posts": "Nincs bejegyzés.",
"create_post": "Bejegyzés létrehozása",
"create_a_post": "Bejegyzés létrehozása",
"number_of_posts": "{{count}} bejegyzés",
"number_of_posts_plural": "{{count}} bejegyzés",
"posts": "Bejegyzések",
"related_posts": "Ezek a bejegyzések kapcsolódhatnak",
"cross_posts": "Ez a hivatkozás itt is be lett küldve:",
"cross_post": "keresztbejegyzés",
"comments": "Hozzászólások",
"remove_comment": "Hozzászólások eltávolítása",
"cross_posted_to": "beküldve ide is: ",
"number_of_comments": "{{count}} hozzászólás",
"number_of_comments_plural": "{{count}} hozzászólás",
"communities": "Közösségek",
"users": "Felhasználók",
"create_a_community": "Közösség létrehozása",
"select_a_community": "Közösség kiválasztása",
"create_community": "Közösség létrehozása",
"remove_community": "Közösség eltávolítása",
"trending_communities": "Népszerű <1>közösségek</1>",
"list_of_communities": "Közösségek listája",
"community_reqs": "Kisbetű és alsóvonás megengedett, szóköz nem.",
"create_private_message": "Privát üzenet létrehozása",
"send_secure_message": "Biztonságos üzenet küldése",
"send_message": "Üzenet küldése",
"message": "Üzenet",
"edit": "szerkesztés",
"reply": "válasz",
"more": "több",
"cancel": "Mégse",
"preview": "Előnézet",
"upload_image": "kép feltöltése",
"avatar": "Avatár",
"upload_avatar": "Avatár feltöltése",
"show_avatars": "Avatárok mutatása",
"show_context": "Összefüggés mutatása",
"sorting_help": "rendezési segítség",
"view_source": "forrás megtekintése",
"unlock": "zárolás feloldása",
"lock": "zárolás",
"sticky": "rögzítés",
"unsticky": "rögzítés feloldása",
"link": "hivatkozás",
"mod": "moderátor",
"mods": "moderátorok",
"moderates": "Moderált közösségek",
"settings": "Beállítások",
"admin_settings": "Adminisztrációs beállítások",
"remove_as_mod": "moderátori jog eltávolítása",
"appoint_as_mod": "kinevezés moderátornak",
"modlog": "Moderációs napló",
"admin": "admin",
"admins": "adminok",
"remove_as_admin": "adminjog eltávolítása",
"appoint_as_admin": "kinevezés adminnak",
"remove": "eltávolítás",
"locked": "zárolva",
"stickied": "rögzítve",
"reason": "Indok",
"mark_as_read": "megjelölés olvasottnak",
"mark_as_unread": "megjelölés olvasatlannak",
"delete": "törlés",
"deleted": "eltávolítva a szerző által",
"delete_account": "FIók törlése",
"restore": "visszaállítás",
"ban": "kitiltás",
"ban_from_site": "kitiltás az oldalról",
"unban": "kitiltás visszavonása",
"unban_from_site": "az oldalról történő kitiltás visszavonása",
"banned": "kitiltva",
"banned_users": "Kitiltott felhasználók",
"save": "mentés",
"unsave": "mentés visszavonása",
"create": "létrehozás",
"creator": "szerző",
"username": "Felhasználónév",
"number_of_points": "{{count}} pont",
"number_of_points_plural": "{{count}} pont",
"number_of_subscribers": "{{count}} feliratkozó",
"number_of_subscribers_plural": "{{count}} feliratkozó",
"name": "Név",
"title": "Cím",
"category": "Kategória",
"both": "Mindkettő",
"saved": "Mentve",
"unsubscribe": "Leiratkozás",
"subscribe": "Feliratkozás",
"subscribed": "Feliratkozva",
"subscribed_to_communities": "Követett <1>közösségek</1>",
"number_of_communities": "{{count}} közösség",
"number_of_communities_plural": "{{count}} közösség",
"formatting_help": "formázási segítség",
"archive_link": "hivatkozás archiválása",
"site_config": "Oldalbeállítások",
"removed": "eltávolítva egy mod által",
"delete_account_confirm": "Figyelmeztetés: ez véglegesen törölni fogja az összes adatodat. A megerősítéshez írd be a jelszavad!",
"email_or_username": "Email vagy felhasználónév",
"number_of_users": "{{count}} felhasználó",
"number_of_users_plural": "{{count}} felhasználó",
"number_online": "{{count}} online felhasználó",
"number_online_plural": "{{count}} online felhasználó",
"subscribers": "Feliratkozók"
}

View file

@ -35,13 +35,13 @@
"remove_as_admin": "移除管理权限", "remove_as_admin": "移除管理权限",
"appoint_as_admin": "添加管理权限", "appoint_as_admin": "添加管理权限",
"remove": "移除", "remove": "移除",
"removed": "已移除", "removed": "已被管理员移除",
"locked": "已加锁", "locked": "已加锁",
"reason": "原因", "reason": "原因",
"mark_as_read": "标记未读", "mark_as_read": "标记未读",
"mark_as_unread": "标记已读", "mark_as_unread": "标记已读",
"delete": "删除", "delete": "删除",
"deleted": "已删除", "deleted": "作者已删除",
"restore": "恢复", "restore": "恢复",
"ban": "禁止", "ban": "禁止",
"ban_from_site": "禁止此站点", "ban_from_site": "禁止此站点",
@ -235,5 +235,11 @@
"time": "时间", "time": "时间",
"action": "行动", "action": "行动",
"block_leaving": "确定要离开吗?", "block_leaving": "确定要离开吗?",
"show_context": "显示上下文" "show_context": "显示上下文",
"admin_settings": "管理员设置",
"site_config": "网站配置",
"banned_users": "被禁止用户",
"site_saved": "网站已保存",
"emoji_picker": "选择表情",
"invalid_username": "用户名无效"
} }