]> Untitled Git - lemmy.git/commitdiff
Merge branch 'kartikynwa-webmanifest' into test
authorDessalines <tyhou13@gmx.com>
Sat, 29 Aug 2020 21:00:56 +0000 (17:00 -0400)
committerDessalines <tyhou13@gmx.com>
Sat, 29 Aug 2020 21:00:56 +0000 (17:00 -0400)
195 files changed:
.gitignore
.travis.yml
README.md
RELEASES.md
ansible/VERSION
ansible/templates/nginx.conf
docker/dev/Dockerfile
docker/dev/docker-compose.yml
docker/federation-test/run-tests.sh
docker/federation-test/servers.sh
docker/federation-test/tests.sh
docker/federation/docker-compose.yml
docker/federation/nginx.conf
docker/federation/run-federation-test.bash
docker/lemmy.hjson
docker/prod/Dockerfile
docker/prod/deploy.sh
docker/prod/docker-compose.yml
docker/travis/docker-compose.yml [new file with mode: 0644]
docker/travis/docker_push.sh [new file with mode: 0644]
docker/travis/run-tests.sh [new file with mode: 0755]
docs/src/about.md
docs/src/about_guide.md
docs/src/about_ranking.md
docs/src/administration_backup_and_restore.md
docs/src/administration_configuration.md
docs/src/administration_install_ansible.md
docs/src/administration_install_docker.md
docs/src/contributing_federation_development.md
docs/src/contributing_local_development.md
docs/src/contributing_websocket_http_api.md
server/Cargo.lock
server/Cargo.toml
server/config/defaults.hjson
server/lemmy_db/Cargo.toml
server/lemmy_db/src/activity.rs
server/lemmy_db/src/category.rs
server/lemmy_db/src/comment.rs
server/lemmy_db/src/comment_view.rs
server/lemmy_db/src/community.rs
server/lemmy_db/src/community_view.rs
server/lemmy_db/src/lib.rs
server/lemmy_db/src/moderator.rs
server/lemmy_db/src/password_reset_request.rs
server/lemmy_db/src/post.rs
server/lemmy_db/src/post_view.rs
server/lemmy_db/src/private_message.rs
server/lemmy_db/src/private_message_view.rs
server/lemmy_db/src/schema.rs
server/lemmy_db/src/site.rs
server/lemmy_db/src/site_view.rs
server/lemmy_db/src/user.rs
server/lemmy_db/src/user_mention.rs
server/lemmy_db/src/user_mention_view.rs
server/lemmy_db/src/user_view.rs
server/lemmy_utils/Cargo.toml
server/lemmy_utils/src/lib.rs
server/lemmy_utils/src/settings.rs
server/migrations/2020-07-18-234519_add_unique_community_user_actor_ids/down.sql [new file with mode: 0644]
server/migrations/2020-07-18-234519_add_unique_community_user_actor_ids/up.sql [new file with mode: 0644]
server/migrations/2020-08-03-000110_add_preferred_usernames_banners_and_icons/down.sql [new file with mode: 0644]
server/migrations/2020-08-03-000110_add_preferred_usernames_banners_and_icons/up.sql [new file with mode: 0644]
server/migrations/2020-08-06-205355_update_community_post_count/down.sql [new file with mode: 0644]
server/migrations/2020-08-06-205355_update_community_post_count/up.sql [new file with mode: 0644]
server/src/api/claims.rs
server/src/api/comment.rs
server/src/api/community.rs
server/src/api/mod.rs
server/src/api/post.rs
server/src/api/site.rs
server/src/api/user.rs
server/src/apub/activities.rs
server/src/apub/comment.rs
server/src/apub/community.rs
server/src/apub/community_inbox.rs [deleted file]
server/src/apub/extensions/group_extensions.rs
server/src/apub/extensions/page_extension.rs
server/src/apub/extensions/signatures.rs
server/src/apub/fetcher.rs
server/src/apub/inbox/activities/announce.rs [new file with mode: 0644]
server/src/apub/inbox/activities/create.rs [new file with mode: 0644]
server/src/apub/inbox/activities/delete.rs [new file with mode: 0644]
server/src/apub/inbox/activities/dislike.rs [new file with mode: 0644]
server/src/apub/inbox/activities/like.rs [new file with mode: 0644]
server/src/apub/inbox/activities/mod.rs [new file with mode: 0644]
server/src/apub/inbox/activities/remove.rs [new file with mode: 0644]
server/src/apub/inbox/activities/undo.rs [new file with mode: 0644]
server/src/apub/inbox/activities/update.rs [new file with mode: 0644]
server/src/apub/inbox/community_inbox.rs [new file with mode: 0644]
server/src/apub/inbox/mod.rs [new file with mode: 0644]
server/src/apub/inbox/shared_inbox.rs [new file with mode: 0644]
server/src/apub/inbox/user_inbox.rs [new file with mode: 0644]
server/src/apub/mod.rs
server/src/apub/post.rs
server/src/apub/private_message.rs
server/src/apub/shared_inbox.rs [deleted file]
server/src/apub/user.rs
server/src/apub/user_inbox.rs [deleted file]
server/src/code_migrations.rs
server/src/lib.rs
server/src/main.rs
server/src/rate_limit/mod.rs
server/src/rate_limit/rate_limiter.rs
server/src/request.rs
server/src/routes/api.rs
server/src/routes/federation.rs
server/src/routes/feeds.rs
server/src/routes/images.rs [new file with mode: 0644]
server/src/routes/index.rs
server/src/routes/mod.rs
server/src/routes/nodeinfo.rs
server/src/routes/webfinger.rs
server/src/routes/websocket.rs
server/src/version.rs
server/src/websocket/mod.rs
server/src/websocket/server.rs
ui/assets/css/main.css
ui/assets/css/themes/_variables.darkly.scss [new file with mode: 0644]
ui/assets/css/themes/_variables.litely.scss
ui/assets/css/themes/darkly.min.css
ui/assets/css/themes/litely.min.css
ui/package.json
ui/src/api_tests/api.spec.ts [deleted file]
ui/src/api_tests/comment.spec.ts [new file with mode: 0644]
ui/src/api_tests/community.spec.ts [new file with mode: 0644]
ui/src/api_tests/follow.spec.ts [new file with mode: 0644]
ui/src/api_tests/post.spec.ts [new file with mode: 0644]
ui/src/api_tests/private_message.spec.ts [new file with mode: 0644]
ui/src/api_tests/shared.ts [new file with mode: 0644]
ui/src/api_tests/user.spec.ts [new file with mode: 0644]
ui/src/components/admin-settings.tsx
ui/src/components/banner-icon-header.tsx [new file with mode: 0644]
ui/src/components/comment-form.tsx
ui/src/components/comment-node.tsx
ui/src/components/comment-nodes.tsx
ui/src/components/communities.tsx
ui/src/components/community-form.tsx
ui/src/components/community-link.tsx
ui/src/components/community.tsx
ui/src/components/create-community.tsx
ui/src/components/create-post.tsx
ui/src/components/create-private-message.tsx
ui/src/components/data-type-select.tsx
ui/src/components/footer.tsx
ui/src/components/iframely-card.tsx
ui/src/components/image-upload-form.tsx [new file with mode: 0644]
ui/src/components/inbox.tsx
ui/src/components/instances.tsx [new file with mode: 0644]
ui/src/components/listing-type-select.tsx
ui/src/components/login.tsx
ui/src/components/main.tsx
ui/src/components/markdown-textarea.tsx [new file with mode: 0644]
ui/src/components/modlog.tsx
ui/src/components/navbar.tsx
ui/src/components/password_change.tsx
ui/src/components/post-form.tsx
ui/src/components/post-listing.tsx
ui/src/components/post-listings.tsx
ui/src/components/post.tsx
ui/src/components/private-message-form.tsx
ui/src/components/private-message.tsx
ui/src/components/search.tsx
ui/src/components/setup.tsx
ui/src/components/sidebar.tsx
ui/src/components/site-form.tsx
ui/src/components/sort-select.tsx
ui/src/components/sponsors.tsx
ui/src/components/symbols.tsx
ui/src/components/user-details.tsx
ui/src/components/user-listing.tsx
ui/src/components/user.tsx
ui/src/i18next.ts
ui/src/index.tsx
ui/src/interfaces.ts
ui/src/service-worker.ts [new file with mode: 0644]
ui/src/services/UserService.ts
ui/src/services/WebSocketService.ts
ui/src/utils.ts
ui/src/version.ts [deleted file]
ui/translations/de.json
ui/translations/el.json
ui/translations/en.json
ui/translations/eo.json
ui/translations/es.json
ui/translations/eu.json
ui/translations/fi.json
ui/translations/fr.json
ui/translations/ga.json
ui/translations/it.json
ui/translations/ko.json [new file with mode: 0644]
ui/translations/pl.json
ui/translations/pt_BR.json
ui/translations/ru.json
ui/translations/sv.json
ui/yarn.lock

index 236a729eb05f97bf7ade4a0f8a904c491905039f..c3a8bd70ed5174a0fb40a66fa8d45cfb2751acb0 100644 (file)
@@ -16,3 +16,6 @@ ui/src/translations
 
 # ide config
 .idea/
+.vscode/
+
+target
index 9541afaae1e57f50bf98ad5d46caaa4f906d2463..be0b401831d5d342f5c300f2171b43ea3b24fd4e 100644 (file)
@@ -1,35 +1,28 @@
-language: rust
-rust:
-  - stable
-matrix:
-  allow_failures:
-    - rust: nightly
-  fast_finish: true
-cache: cargo
-before_cache:
-  - rm -rfv target/debug/incremental/lemmy_server-*
-  - rm -rfv target/debug/.fingerprint/lemmy_server-*
-  - rm -rfv target/debug/build/lemmy_server-*
-  - rm -rfv target/debug/deps/lemmy_server-*
-  - rm -rfv target/debug/lemmy_server.d
-before_script:
-  - psql -c "create user lemmy with password 'password' superuser;" -U postgres
-  - psql -c 'create database lemmy with owner lemmy;' -U postgres
-  - rustup component add clippy --toolchain stable-x86_64-unknown-linux-gnu
-before_install:
-  - cd server
-script:
-  # Default checks, but fail if anything is detected
-  - cargo build
-  - cargo clippy -- -D clippy::style -D clippy::correctness -D clippy::complexity -D clippy::perf
-  - cargo install diesel_cli --no-default-features --features postgres --force
-  - diesel migration run
-  - cargo test --workspace
+sudo: required
+language: node_js
+node_js:
+- 14
+services:
+- docker
 env:
+  matrix:
+  - DOCKER_COMPOSE_VERSION=1.25.5
   global:
-    - DATABASE_URL=postgres://lemmy:password@localhost:5432/lemmy
-    - LEMMY_DATABASE_URL=postgres://lemmy:password@localhost:5432/lemmy
-    - RUST_TEST_THREADS=1
-
-addons:
-  postgresql: "9.4"
+  - secure: nzmFoTxPn7OT+qcTULezSCT6B44j/q8RxERBQSr1FVXaCcDrBr6q9ewhGy7BHWP74r4qbif4m9r3sNELZCoFYFP3JwLnrZfX/xUwU8p61eFD2PMOJAdOywDxb94SvooOSnjBmxNvRsuqf6Zmnw378mbsSVCi9Xbx9jpoV4Jq8zKgO0M8WIl/lj2dijD95WIMrHcorbzKS3+2zW3LkPiC2bnfDAUmUDfaCj1gh9FCvzZMtrSxu7kxAeFCkR16TJUciIcGgag8rLHfxwG0h2uEJJ+3/62qCWUdgnj171oTE4ZRi0hdvt2HOY5wjHfS2y1ZxWYgo31uws3pyoTNeQZi0o7Q9Xe/4JXYZXvDfuscSZ9RiuhAstCVswtXPJJVVJQ9cdl5eX1TI0bz8eVRvRy4p40OIBjKiobkmRjl8sXjFbpYAIvFr+TgSa/K/bxm3POfI0B8bIHI85zFxUMrWt5i2IJ0dWvDNHrz+CWWKn1vVFYbBNPgDDHtE0P3LWLEioWFf+ULycjW8DefWc+b63Lf9SSaEE7FnX2mc+BaHCgubCDkJy9Au4xP8zQlJjgZwOdTedw5jvmwz3fqMZBpHypVUXzZs7cRhMWtQ7TAoGb8TOqXNgPEVW+BARNXl0wAamTgjt9v20x0wkp+/SLJwMNY+zvwmzxzd5R9TPgDOqyIRTU=
+  - secure: ALZqC4OYV315P7EZyk+c/PLJdneeU7jMC30TTzMcX3hospIu7naWekZ+HUnziFDQKZxIHWKZsq1R52DWhsERLrPF3SVa+QiXu8vTTPrETBWnu9VgyFzgdEbUKRas1X3qerEAHcNBms1EAl2FOiQM1k5EDygrClv4KWgyzntEtKJbN2UCFKxtoBSdMZA6fcGtCwffcj8uIAIP2NhZixbU+smVgVbpMpe6QEuuEoVlVrfH8iXxb8Gi+qkd0YIYAHkjtTqQ/nHuAUhcuEE0mORTNGPv7CmTwpuQiGCCdtySZc7Qq8z1x2y7RLy0+RVxM0PR8UV6iy4ipyTgZ6wTF30ksLDxOI3GlRaKF3F6kLErOiEiEUOqa+zLgUM0OLGTn+KLATQDx74in5NcKjKUAnkuxdZyuDbifvQb5tqfrGdXd22pzVZbielRJRW59ig0Nr5cxEpRtoRkoFKNk7o3XlD6JmIBjKn1UHkZ4H/oLUKIXT2qOP2fIEzgLjfpSuGwhvJRz1KRP49HYVl7Gkd45/RdZ519W0gnMkIrEaod90iXSFNTgmJTGeH0Mv0jHameN47PIT3c49MOy5Hj0XCHUPfc6qqrdGnliS5hTnrFThCfn5ZuSZxVdgGLJUQvV+D+5KDqjFdGyNGVGoEg0YdrDtGXmpojbyQDJAT7ToL3yIBF7co=
+before_install:
+# Install docker-compose
+- sudo rm /usr/local/bin/docker-compose
+- curl -L https://github.com/docker/compose/releases/download/${DOCKER_COMPOSE_VERSION}/docker-compose-`uname
+  -s`-`uname -m` > docker-compose
+- chmod +x docker-compose
+- sudo mv docker-compose /usr/local/bin
+# Change dir
+- cd docker/travis
+script:
+- "./run-tests.sh"
+deploy:
+  provider: script
+  script: bash docker_push.sh
+  on:
+    tags: true
index 3668d147e84ae136b03f5a3644d650f39b24cc5e..57b3d63e54c59a7ace158d4ba6f3e4a05ba8fd71 100644 (file)
--- a/README.md
+++ b/README.md
@@ -1,7 +1,7 @@
 <div align="center">
 
 ![GitHub tag (latest SemVer)](https://img.shields.io/github/tag/LemmyNet/lemmy.svg)
-[![Build Status](https://travis-ci.org/LemmyNet/lemmy.svg?branch=master)](https://travis-ci.org/LemmyNet/lemmy)
+[![Build Status](https://travis-ci.org/LemmyNet/lemmy.svg?branch=main)](https://travis-ci.org/LemmyNet/lemmy)
 [![GitHub issues](https://img.shields.io/github/issues-raw/LemmyNet/lemmy.svg)](https://github.com/LemmyNet/lemmy/issues)
 [![Docker Pulls](https://img.shields.io/docker/pulls/dessalines/lemmy.svg)](https://cloud.docker.com/repository/docker/dessalines/lemmy/)
 [![Translation status](http://weblate.yerbamate.dev/widgets/lemmy/-/lemmy/svg-badge.svg)](http://weblate.yerbamate.dev/engage/lemmy/)
@@ -26,7 +26,7 @@
     ·
     <a href="https://github.com/LemmyNet/lemmy/issues">Request Feature</a>
     ·
-    <a href="https://github.com/LemmyNet/lemmy/blob/master/RELEASES.md">Releases</a>
+    <a href="https://github.com/LemmyNet/lemmy/blob/main/RELEASES.md">Releases</a>
   </p>
 </p>
 
@@ -34,7 +34,7 @@
 
 Front Page|Post
 ---|---
-![main screen](https://raw.githubusercontent.com/LemmyNet/lemmy/master/docs/img/main_screen.png)|![chat screen](https://raw.githubusercontent.com/LemmyNet/lemmy/master/docs/img/chat_screen.png)
+![main screen](https://raw.githubusercontent.com/LemmyNet/lemmy/main/docs/img/main_screen.png)|![chat screen](https://raw.githubusercontent.com/LemmyNet/lemmy/main/docs/img/chat_screen.png)
 
 [Lemmy](https://github.com/LemmyNet/lemmy) is similar to sites like [Reddit](https://reddit.com), [Lobste.rs](https://lobste.rs), [Raddle](https://raddle.me), or [Hacker News](https://news.ycombinator.com/): you subscribe to forums you're interested in, post links and discussions, then vote, and comment on them. Behind the scenes, it is very different; anyone can easily run a server, and all these servers are federated (think email), and connected to the same universe, called the [Fediverse](https://en.wikipedia.org/wiki/Fediverse).
 
@@ -104,6 +104,17 @@ Each Lemmy server can set its own moderation policy; appointing site-wide admins
 - [Ansible](https://dev.lemmy.ml/docs/administration_install_ansible.html)
 - [Kubernetes](https://dev.lemmy.ml/docs/administration_install_kubernetes.html)
 
+## Lemmy Projects
+
+### Apps
+
+- [Lemmy-mobile (Android / IOS) - React native ( under development )](https://github.com/koredefashokun/lemmy-mobile)
+
+### Libraries
+
+- [lemmy-js-client](https://github.com/LemmyNet/lemmy-js-client)
+- [Kotlin API ( under development )](https://github.com/eiknat/lemmy-client)
+
 ## Support / Donate
 
 Lemmy is free, open-source software, meaning no advertising, monetizing, or venture capital, ever. Your donations directly support full-time development of the project.
index 9946ae32184f3b512a0c46fe51de5ca6de319cd4..31cab59d724212d42bbdfcddd4ec762a6d2df674 100644 (file)
@@ -1,3 +1,43 @@
+# Lemmy v0.7.40 Pre-Release (2020-08-05)
+
+We've [added a lot](https://github.com/LemmyNet/lemmy/compare/v0.7.40...v0.7.0) in this pre-release:
+
+- New post sorts `Active` (previously called hot), and `Hot`. Active shows posts with recent comments, hot shows highly ranked posts.
+- Customizeable site icon and banner, user icon and banner, and community icon and banner.
+- Added user preferred names / display names, bios, and cakedays.
+- User settings are now shared across browsers (a page refresh will pick up changes).
+- Visual / Audio captchas through the lemmy API.
+- Lots of UI prettiness.
+- Lots of bug fixes.
+- Lots of additional translations.
+- Lots of federation prepping / additions / refactors.
+
+This release removes the need for you to have a pictrs nginx route (the requests are now routed through lemmy directly). Follow the upgrade instructions below to replace your nginx with the new one.
+
+## Upgrading
+
+**With Ansible:**
+
+```
+# run these commands locally
+git pull
+cd ansible
+ansible-playbook lemmy.yml
+```
+
+**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
+sudo docker-compose up -d
+```
+
+
 # Lemmy v0.7.0 Release (2020-06-23)
 
 This release replaces [pictshare](https://github.com/HaschekSolutions/pictshare)
index 913ced209c4b246b99872f13a665da8eeecdde09..588cf6d2b46c0fb77ae5dc4386bc597f8c144d64 100644 (file)
@@ -1 +1 @@
-v0.7.21
+v0.7.55
index 5847bad016b14408dec92c98bf36ea8c41014d5a..886ce0b78bfb8f771382d976ceda3cb5bfe48d68 100644 (file)
@@ -1,4 +1,3 @@
-proxy_cache_path /var/cache/lemmy_frontend levels=1:2 keys_zone=lemmy_frontend_cache:10m max_size=100m                 use_temp_path=off;
 limit_req_zone $binary_remote_addr zone=lemmy_ratelimit:10m rate=1r/s;
 
 server {
@@ -52,26 +51,22 @@ server {
     # Upload limit for pictrs
     client_max_body_size 20M;
 
-    # Rate limit
-    limit_req zone=lemmy_ratelimit burst=30 nodelay;
-
     location / {
         proxy_pass http://0.0.0.0:8536;
         proxy_set_header X-Real-IP $remote_addr;
         proxy_set_header Host $host;
         proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
 
+        # Cuts off the trailing slash on URLs to make them valid
+        rewrite ^(.+)/+$ $1 permanent;
+
         # WebSocket support
         proxy_http_version 1.1;
         proxy_set_header Upgrade $http_upgrade;
         proxy_set_header Connection "upgrade";
 
-        # Proxy Cache
-        proxy_cache             lemmy_frontend_cache;
-        proxy_cache_use_stale   error timeout http_500 http_502 http_503 http_504;
-        proxy_cache_revalidate  on;
-        proxy_cache_lock        on;
-        proxy_cache_min_uses    5;
+        # Rate limit
+        limit_req zone=lemmy_ratelimit burst=30 nodelay;
     }    
 
     # Redirect pictshare images to pictrs
@@ -79,16 +74,12 @@ server {
       return 301 /pictrs/image/$1;
     }
 
-    # pict-rs images
+    # Separate location block to disable rate limiting for 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 Host $host;
-        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
-      }
-      # Block the import
-      return 403;
+      proxy_pass http://0.0.0.0:8536/pictrs;
+      proxy_set_header X-Real-IP $remote_addr;
+      proxy_set_header Host $host;
+      proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
     }
 
     location /iframely/ {
index b86618d8574b478d6356f103b5fd57e0967c0953..afbdbbbe128ab359fafadd37e6ff0587f47cd8ad 100644 (file)
@@ -41,6 +41,9 @@ FROM alpine:3.12
 # Install libpq for postgres
 RUN apk add libpq
 
+# Install Espeak for captchas
+RUN apk add espeak
+
 # Copy resources
 COPY server/config/defaults.hjson /config/defaults.hjson
 COPY --from=rust /app/server/target/x86_64-unknown-linux-musl/debug/lemmy_server /app/lemmy
index 51a3ecdab307bb219f23db28d61de440ebd698fd..257ad6c63b0eb44421cee47b581dfbff8e7713c4 100644 (file)
@@ -21,7 +21,8 @@ services:
   postgres:
     image: postgres:12-alpine
     ports:
-      - "127.0.0.1:5432:5432"
+      # use a different port so it doesnt conflict with postgres running on the host
+      - "127.0.0.1:5433:5432"
     environment:
       - POSTGRES_USER=lemmy
       - POSTGRES_PASSWORD=password
index 3848414b9b871de07b369b15b5300202e1d45334..f166d4903e6fd1b2f07b7fd344521457d1171313 100755 (executable)
@@ -13,8 +13,8 @@ pushd ../../ui
 yarn
 popd
 
-mkdir -p volumes/pictrs_{alpha,beta,gamma}
-sudo chown -R 991:991 volumes/pictrs_{alpha,beta,gamma}
+mkdir -p volumes/pictrs_{alpha,beta,gamma,delta,epsilon}
+sudo chown -R 991:991 volumes/pictrs_{alpha,beta,gamma,delta,epsilon}
 
 sudo docker build ../../ --file ../federation/Dockerfile --tag lemmy-federation:latest
 
@@ -28,6 +28,8 @@ echo "Waiting for Lemmy to start..."
 while [[ "$(curl -s -o /dev/null -w '%{http_code}' 'localhost:8540/api/v1/site')" != "200" ]]; do sleep 1; done
 while [[ "$(curl -s -o /dev/null -w '%{http_code}' 'localhost:8550/api/v1/site')" != "200" ]]; do sleep 1; done
 while [[ "$(curl -s -o /dev/null -w '%{http_code}' 'localhost:8560/api/v1/site')" != "200" ]]; do sleep 1; done
+while [[ "$(curl -s -o /dev/null -w '%{http_code}' 'localhost:8570/api/v1/site')" != "200" ]]; do sleep 1; done
+while [[ "$(curl -s -o /dev/null -w '%{http_code}' 'localhost:8580/api/v1/site')" != "200" ]]; do sleep 1; done
 yarn api-test || true
 popd
 
index 36f10cd82e5c43704ab7783b351c5e08bafa6f27..5b09bc952f76d2ba25e403b2ce21b92866d408e6 100755 (executable)
@@ -1,6 +1,7 @@
 #!/bin/bash
 set -e
 
+sudo docker-compose --file ../federation/docker-compose.yml --project-directory . down
 sudo rm -rf volumes
 
 pushd ../../server/
@@ -11,8 +12,8 @@ pushd ../../ui
 yarn
 popd
 
-mkdir -p volumes/pictrs_{alpha,beta,gamma}
-sudo chown -R 991:991 volumes/pictrs_{alpha,beta,gamma}
+mkdir -p volumes/pictrs_{alpha,beta,gamma,delta,epsilon}
+sudo chown -R 991:991 volumes/pictrs_{alpha,beta,gamma,delta,epsilon}
 
 sudo docker build ../../ --file ../federation/Dockerfile --tag lemmy-federation:latest
 
index 2e88ffb25c8316daffd254b95024213f035c86c8..58472e95cc424fb1d68de452ca1ba5866242fb8a 100755 (executable)
@@ -6,5 +6,7 @@ echo "Waiting for Lemmy to start..."
 while [[ "$(curl -s -o /dev/null -w '%{http_code}' 'localhost:8540/api/v1/site')" != "200" ]]; do sleep 1; done
 while [[ "$(curl -s -o /dev/null -w '%{http_code}' 'localhost:8550/api/v1/site')" != "200" ]]; do sleep 1; done
 while [[ "$(curl -s -o /dev/null -w '%{http_code}' 'localhost:8560/api/v1/site')" != "200" ]]; do sleep 1; done
+while [[ "$(curl -s -o /dev/null -w '%{http_code}' 'localhost:8570/api/v1/site')" != "200" ]]; do sleep 1; done
+while [[ "$(curl -s -o /dev/null -w '%{http_code}' 'localhost:8580/api/v1/site')" != "200" ]]; do sleep 1; done
 yarn api-test || true
 popd
index c552d18fd4862a5708bc0ec9f39e427e92fcb5ae..32fee74ab5bad3b0cfee0321921ba29ab4cb3a99 100644 (file)
@@ -7,16 +7,20 @@ services:
       - "8540:8540"
       - "8550:8550"
       - "8560:8560"
+      - "8570:8570"
+      - "8580:8580"
     volumes:
       # Hack to make this work from both docker/federation/ and docker/federation-test/
       - ../federation/nginx.conf:/etc/nginx/nginx.conf
     restart: on-failure
     depends_on:
-      - lemmy-alpha
       - pictrs
+      - iframely
+      - lemmy-alpha
       - lemmy-beta
       - lemmy-gamma
-      - iframely
+      - lemmy-delta
+      - lemmy-epsilon
 
   pictrs:
     restart: always
@@ -34,11 +38,14 @@ services:
       - LEMMY_FRONT_END_DIR=/app/dist
       - LEMMY_FEDERATION__ENABLED=true
       - LEMMY_FEDERATION__TLS_ENABLED=false
-      - LEMMY_FEDERATION__ALLOWED_INSTANCES=lemmy-beta,lemmy-gamma
+      - LEMMY_FEDERATION__ALLOWED_INSTANCES=lemmy-beta,lemmy-gamma,lemmy-delta,lemmy-epsilon
       - LEMMY_PORT=8540
       - LEMMY_SETUP__ADMIN_USERNAME=lemmy_alpha
       - LEMMY_SETUP__ADMIN_PASSWORD=lemmy
       - LEMMY_SETUP__SITE_NAME=lemmy-alpha
+      - LEMMY_RATE_LIMIT__POST=99999
+      - LEMMY_RATE_LIMIT__REGISTER=99999
+      - LEMMY_CAPTCHA__ENABLED=false
       - RUST_BACKTRACE=1
       - RUST_LOG=debug
     depends_on:
@@ -61,11 +68,14 @@ services:
       - LEMMY_FRONT_END_DIR=/app/dist
       - LEMMY_FEDERATION__ENABLED=true
       - LEMMY_FEDERATION__TLS_ENABLED=false
-      - LEMMY_FEDERATION__ALLOWED_INSTANCES=lemmy-alpha,lemmy-gamma
+      - LEMMY_FEDERATION__ALLOWED_INSTANCES=lemmy-alpha,lemmy-gamma,lemmy-delta,lemmy-epsilon
       - LEMMY_PORT=8550
       - LEMMY_SETUP__ADMIN_USERNAME=lemmy_beta
       - LEMMY_SETUP__ADMIN_PASSWORD=lemmy
       - LEMMY_SETUP__SITE_NAME=lemmy-beta
+      - LEMMY_RATE_LIMIT__POST=99999
+      - LEMMY_RATE_LIMIT__REGISTER=99999
+      - LEMMY_CAPTCHA__ENABLED=false
       - RUST_BACKTRACE=1
       - RUST_LOG=debug
     depends_on:
@@ -88,11 +98,14 @@ services:
       - LEMMY_FRONT_END_DIR=/app/dist
       - LEMMY_FEDERATION__ENABLED=true
       - LEMMY_FEDERATION__TLS_ENABLED=false
-      - LEMMY_FEDERATION__ALLOWED_INSTANCES=lemmy-alpha,lemmy-beta
+      - LEMMY_FEDERATION__ALLOWED_INSTANCES=lemmy-alpha,lemmy-beta,lemmy-delta,lemmy-epsilon
       - LEMMY_PORT=8560
       - LEMMY_SETUP__ADMIN_USERNAME=lemmy_gamma
       - LEMMY_SETUP__ADMIN_PASSWORD=lemmy
       - LEMMY_SETUP__SITE_NAME=lemmy-gamma
+      - LEMMY_RATE_LIMIT__POST=99999
+      - LEMMY_RATE_LIMIT__REGISTER=99999
+      - LEMMY_CAPTCHA__ENABLED=false
       - RUST_BACKTRACE=1
       - RUST_LOG=debug
     depends_on:
@@ -106,6 +119,68 @@ services:
     volumes:
       - ./volumes/postgres_gamma:/var/lib/postgresql/data
 
+  # An instance with only an allowlist for beta
+  lemmy-delta:
+    image: lemmy-federation:latest
+    environment:
+      - LEMMY_HOSTNAME=lemmy-delta:8570
+      - LEMMY_DATABASE_URL=postgres://lemmy:password@postgres_delta:5432/lemmy
+      - LEMMY_JWT_SECRET=changeme
+      - LEMMY_FRONT_END_DIR=/app/dist
+      - LEMMY_FEDERATION__ENABLED=true
+      - LEMMY_FEDERATION__TLS_ENABLED=false
+      - LEMMY_FEDERATION__ALLOWED_INSTANCES=lemmy-beta
+      - LEMMY_PORT=8570
+      - LEMMY_SETUP__ADMIN_USERNAME=lemmy_delta
+      - LEMMY_SETUP__ADMIN_PASSWORD=lemmy
+      - LEMMY_SETUP__SITE_NAME=lemmy-delta
+      - LEMMY_RATE_LIMIT__POST=99999
+      - LEMMY_RATE_LIMIT__REGISTER=99999
+      - LEMMY_CAPTCHA__ENABLED=false
+      - RUST_BACKTRACE=1
+      - RUST_LOG=debug
+    depends_on:
+      - postgres_delta
+  postgres_delta:
+    image: postgres:12-alpine
+    environment:
+      - POSTGRES_USER=lemmy
+      - POSTGRES_PASSWORD=password
+      - POSTGRES_DB=lemmy
+    volumes:
+      - ./volumes/postgres_delta:/var/lib/postgresql/data
+
+  # An instance who has a blocklist, with lemmy-alpha blocked
+  lemmy-epsilon:
+    image: lemmy-federation:latest
+    environment:
+      - LEMMY_HOSTNAME=lemmy-epsilon:8580
+      - LEMMY_DATABASE_URL=postgres://lemmy:password@postgres_epsilon:5432/lemmy
+      - LEMMY_JWT_SECRET=changeme
+      - LEMMY_FRONT_END_DIR=/app/dist
+      - LEMMY_FEDERATION__ENABLED=true
+      - LEMMY_FEDERATION__TLS_ENABLED=false
+      - LEMMY_FEDERATION__BLOCKED_INSTANCES=lemmy-alpha
+      - LEMMY_PORT=8580
+      - LEMMY_SETUP__ADMIN_USERNAME=lemmy_epsilon
+      - LEMMY_SETUP__ADMIN_PASSWORD=lemmy
+      - LEMMY_SETUP__SITE_NAME=lemmy-epsilon
+      - LEMMY_RATE_LIMIT__POST=99999
+      - LEMMY_RATE_LIMIT__REGISTER=99999
+      - LEMMY_CAPTCHA__ENABLED=false
+      - RUST_BACKTRACE=1
+      - RUST_LOG=debug
+    depends_on:
+      - postgres_epsilon
+  postgres_epsilon:
+    image: postgres:12-alpine
+    environment:
+      - POSTGRES_USER=lemmy
+      - POSTGRES_PASSWORD=password
+      - POSTGRES_DB=lemmy
+    volumes:
+      - ./volumes/postgres_epsilon:/var/lib/postgresql/data
+
   iframely:
     image: dogbin/iframely:latest
     volumes:
index 2093297eb83f4ed191ec5f7dbafd3e1bfa50a017..6d062f70b0cf1019feafd1ade621cdfd8f4f387d 100644 (file)
@@ -17,24 +17,15 @@ http {
             proxy_set_header Host $host;
             proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
 
+            # Cuts off the trailing slash on URLs to make them valid
+            rewrite ^(.+)/+$ $1 permanent;
+
             # WebSocket support
             proxy_http_version 1.1;
             proxy_set_header Upgrade $http_upgrade;
             proxy_set_header Connection "upgrade";
         }
 
-        # pict-rs images
-        location /pictrs {
-          location /pictrs/image {
-            proxy_pass http://pictrs:8080/image;
-            proxy_set_header X-Real-IP $remote_addr;
-            proxy_set_header Host $host;
-            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
-          }
-          # Block the import
-          return 403;
-        }
-
         location /iframely/ {
             proxy_pass http://iframely:80/;
             proxy_set_header X-Real-IP $remote_addr;
@@ -57,22 +48,44 @@ http {
             proxy_set_header Host $host;
             proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
 
+            # Cuts off the trailing slash on URLs to make them valid
+            rewrite ^(.+)/+$ $1 permanent;
+
             # WebSocket support
             proxy_http_version 1.1;
             proxy_set_header Upgrade $http_upgrade;
             proxy_set_header Connection "upgrade";
         }
 
-        # pict-rs images
-        location /pictrs {
-          location /pictrs/image {
-            proxy_pass http://pictrs:8080/image;
+        location /iframely/ {
+            proxy_pass http://iframely:80/;
+            proxy_set_header X-Real-IP $remote_addr;
+            proxy_set_header Host $host;
+            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+        }
+    }
+
+    server {
+        listen 8560;
+        server_name 127.0.0.1;
+        access_log off;
+
+        # Upload limit for pictshare
+        client_max_body_size 50M;
+
+        location / {
+            proxy_pass http://lemmy-gamma:8560;
             proxy_set_header X-Real-IP $remote_addr;
             proxy_set_header Host $host;
             proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
-          }
-          # Block the import
-          return 403;
+
+            # Cuts off the trailing slash on URLs to make them valid
+            rewrite ^(.+)/+$ $1 permanent;
+
+            # WebSocket support
+            proxy_http_version 1.1;
+            proxy_set_header Upgrade $http_upgrade;
+            proxy_set_header Connection "upgrade";
         }
 
         location /iframely/ {
@@ -84,7 +97,7 @@ http {
     }
 
     server {
-        listen 8560;
+        listen 8570;
         server_name 127.0.0.1;
         access_log off;
 
@@ -92,27 +105,49 @@ http {
         client_max_body_size 50M;
 
         location / {
-            proxy_pass http://lemmy-gamma:8560;
+            proxy_pass http://lemmy-delta:8570;
             proxy_set_header X-Real-IP $remote_addr;
             proxy_set_header Host $host;
             proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
 
+            # Cuts off the trailing slash on URLs to make them valid
+            rewrite ^(.+)/+$ $1 permanent;
+
             # WebSocket support
             proxy_http_version 1.1;
             proxy_set_header Upgrade $http_upgrade;
             proxy_set_header Connection "upgrade";
         }
 
-        # pict-rs images
-        location /pictrs {
-          location /pictrs/image {
-            proxy_pass http://pictrs:8080/image;
+        location /iframely/ {
+            proxy_pass http://iframely:80/;
+            proxy_set_header X-Real-IP $remote_addr;
+            proxy_set_header Host $host;
+            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+        }
+    }
+
+    server {
+        listen 8580;
+        server_name 127.0.0.1;
+        access_log off;
+
+        # Upload limit for pictshare
+        client_max_body_size 50M;
+
+        location / {
+            proxy_pass http://lemmy-epsilon:8580;
             proxy_set_header X-Real-IP $remote_addr;
             proxy_set_header Host $host;
             proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
-          }
-          # Block the import
-          return 403;
+
+            # Cuts off the trailing slash on URLs to make them valid
+            rewrite ^(.+)/+$ $1 permanent;
+
+            # WebSocket support
+            proxy_http_version 1.1;
+            proxy_set_header Upgrade $http_upgrade;
+            proxy_set_header Connection "upgrade";
         }
 
         location /iframely/ {
index bc73fff63140093df71368a67997f97de96f9b98..0fe03aa176b089342aa4730fb53cfc3165abf648 100755 (executable)
@@ -6,7 +6,7 @@ pushd ../../server/ || exit
 cargo build &
 popd || exit
 
-if [ "$1" = "-yarn" ]; then
+if [ "$1" != "--no-yarn-build" ]; then
   pushd ../../ui/ || exit
   yarn
   yarn build
@@ -20,7 +20,7 @@ popd || exit
 
 sudo docker build ../../ --file Dockerfile -t lemmy-federation:latest
 
-for Item in alpha beta gamma ; do
+for Item in alpha beta gamma delta epsilon ; do
   sudo mkdir -p volumes/pictrs_$Item
   sudo chown -R 991:991 volumes/pictrs_$Item
 done
index 89da46891364e07efa3cc23614abbda26462b299..d17394767433b018e75563ff8f938873a3100a70 100644 (file)
@@ -2,6 +2,15 @@
   # for more info about the config, check out the documentation
   # https://dev.lemmy.ml/docs/administration_configuration.html
 
+  setup: {
+    # username for the admin user
+    admin_username: "lemmy"
+    # password for the admin user
+    admin_password: "lemmy"
+    # name of the site (can be changed later)
+    site_name: "lemmy-test"
+  }
+
   # the domain name of your instance (eg "dev.lemmy.ml")
   hostname: "my_domain"
   # address where lemmy should listen for incoming requests
index 774387404d930cdbc7b807ef72fac03c428eca08..845df88de49c0204cb2357081376c16e3861e386 100644 (file)
@@ -50,6 +50,10 @@ FROM alpine:3.12 as lemmy
 
 # Install libpq for postgres
 RUN apk add libpq
+
+# Install Espeak for captchas
+RUN apk add espeak
+
 RUN addgroup -g 1000 lemmy
 RUN adduser -D -s /bin/sh -u 1000 -G lemmy lemmy
 
index 2c6e3d312758b583eec6212724af909468b326d6..3c12df204f989dc33fea59c90b3ce67c62fc92cf 100755 (executable)
@@ -1,10 +1,10 @@
 #!/bin/sh
 set -e
-git checkout master
+git checkout main
 
 # Import translations
 git fetch weblate
-git merge weblate/master
+git merge weblate/main
 
 # Creating the new tag
 new_tag="$1"
@@ -12,8 +12,6 @@ third_semver=$(echo $new_tag | cut -d "." -f 3)
 
 # Setting the version on the front end
 cd ../../
-echo "export const version: string = '$new_tag';" > "ui/src/version.ts"
-git add "ui/src/version.ts"
 # Setting the version on the backend
 echo "pub const VERSION: &str = \"$new_tag\";" > "server/src/version.rs"
 git add "server/src/version.rs"
@@ -26,35 +24,39 @@ cd docker/prod || exit
 # 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/" ../../ansible/templates/docker-compose.yml
+sed -i "s/dessalines\/lemmy:v.*/dessalines\/lemmy:$new_tag/" ../travis/docker_push.sh
 git add ../prod/docker-compose.yml
 git add ../../ansible/templates/docker-compose.yml
+git add ../travis/docker_push.sh
 
 # The commit
 git commit -m"Version $new_tag"
 git tag $new_tag
 
-export COMPOSE_DOCKER_CLI_BUILD=1
-export DOCKER_BUILDKIT=1
-
-# Rebuilding docker
-if [ $third_semver -eq 0 ]; then
-  # TODO get linux/arm/v7 build working
-  # Build for Raspberry Pi / other archs too
-  docker buildx build --platform linux/amd64,linux/arm64 ../../ \
-    --file Dockerfile \
-    --tag dessalines/lemmy:$new_tag \
-    --push
-else
-  docker buildx build --platform linux/amd64 ../../ \
-    --file Dockerfile \
-    --tag dessalines/lemmy:$new_tag \
-    --push
-fi
+# Now doing the building on travis, but leave this in for when you need to do an arm build
+
+# export COMPOSE_DOCKER_CLI_BUILD=1
+# export DOCKER_BUILDKIT=1
+
+# # Rebuilding docker
+# if [ $third_semver -eq 0 ]; then
+#   # TODO get linux/arm/v7 build working
+#   # Build for Raspberry Pi / other archs too
+#   docker buildx build --platform linux/amd64,linux/arm64 ../../ \
+#     --file Dockerfile \
+#     --tag dessalines/lemmy:$new_tag \
+#     --push
+# else
+#   docker buildx build --platform linux/amd64 ../../ \
+#     --file Dockerfile \
+#     --tag dessalines/lemmy:$new_tag \
+#     --push
+# fi
 
 # Push
 git push origin $new_tag
 git push
 
 # Pushing to any ansible deploys
-cd ../../../lemmy-ansible || exit
-ansible-playbook -i prod playbooks/site.yml --vault-password-file vault_pass
+cd ../../../lemmy-ansible || exit
+ansible-playbook -i prod playbooks/site.yml --vault-password-file vault_pass
index 38f9c7db427c4dd5b9524dc8ba13b513a2b1f0bb..e9369583a12c2a412f30ec076151e79296046938 100644 (file)
@@ -12,7 +12,7 @@ services:
     restart: always
 
   lemmy:
-    image: dessalines/lemmy:v0.7.21
+    image: dessalines/lemmy:v0.7.55
     ports:
       - "127.0.0.1:8536:8536"
     restart: always
diff --git a/docker/travis/docker-compose.yml b/docker/travis/docker-compose.yml
new file mode 100644 (file)
index 0000000..9aa7750
--- /dev/null
@@ -0,0 +1,188 @@
+version: '3.3'
+
+services:
+  nginx:
+    image: nginx:1.17-alpine
+    ports:
+      - "8540:8540"
+      - "8550:8550"
+      - "8560:8560"
+      - "8570:8570"
+      - "8580:8580"
+    volumes:
+      # Hack to make this work from both docker/federation/ and docker/federation-test/
+      - ../federation/nginx.conf:/etc/nginx/nginx.conf
+    restart: on-failure
+    depends_on:
+      - pictrs
+      - iframely
+      - lemmy-alpha
+      - lemmy-beta
+      - lemmy-gamma
+      - lemmy-delta
+      - lemmy-epsilon
+
+  pictrs:
+    restart: always
+    image: asonix/pictrs:v0.1.13-r0
+    user: 991:991
+    volumes:
+      - ./volumes/pictrs_alpha:/mnt
+
+  lemmy-alpha:
+    image: dessalines/lemmy:travis
+    environment:
+      - LEMMY_HOSTNAME=lemmy-alpha:8540
+      - LEMMY_DATABASE_URL=postgres://lemmy:password@postgres_alpha:5432/lemmy
+      - LEMMY_JWT_SECRET=changeme
+      - LEMMY_FRONT_END_DIR=/app/dist
+      - LEMMY_FEDERATION__ENABLED=true
+      - LEMMY_FEDERATION__TLS_ENABLED=false
+      - LEMMY_FEDERATION__ALLOWED_INSTANCES=lemmy-beta,lemmy-gamma,lemmy-delta,lemmy-epsilon
+      - LEMMY_PORT=8540
+      - LEMMY_SETUP__ADMIN_USERNAME=lemmy_alpha
+      - LEMMY_SETUP__ADMIN_PASSWORD=lemmy
+      - LEMMY_SETUP__SITE_NAME=lemmy-alpha
+      - LEMMY_RATE_LIMIT__POST=99999
+      - LEMMY_RATE_LIMIT__REGISTER=99999
+      - LEMMY_CAPTCHA__ENABLED=false
+      - RUST_BACKTRACE=1
+      - RUST_LOG=debug
+    depends_on:
+      - postgres_alpha
+  postgres_alpha:
+    image: postgres:12-alpine
+    environment:
+      - POSTGRES_USER=lemmy
+      - POSTGRES_PASSWORD=password
+      - POSTGRES_DB=lemmy
+    volumes:
+      - ./volumes/postgres_alpha:/var/lib/postgresql/data
+
+  lemmy-beta:
+    image: dessalines/lemmy:travis
+    environment:
+      - LEMMY_HOSTNAME=lemmy-beta:8550
+      - LEMMY_DATABASE_URL=postgres://lemmy:password@postgres_beta:5432/lemmy
+      - LEMMY_JWT_SECRET=changeme
+      - LEMMY_FRONT_END_DIR=/app/dist
+      - LEMMY_FEDERATION__ENABLED=true
+      - LEMMY_FEDERATION__TLS_ENABLED=false
+      - LEMMY_FEDERATION__ALLOWED_INSTANCES=lemmy-alpha,lemmy-gamma,lemmy-delta,lemmy-epsilon
+      - LEMMY_PORT=8550
+      - LEMMY_SETUP__ADMIN_USERNAME=lemmy_beta
+      - LEMMY_SETUP__ADMIN_PASSWORD=lemmy
+      - LEMMY_SETUP__SITE_NAME=lemmy-beta
+      - LEMMY_RATE_LIMIT__POST=99999
+      - LEMMY_RATE_LIMIT__REGISTER=99999
+      - LEMMY_CAPTCHA__ENABLED=false
+      - RUST_BACKTRACE=1
+      - RUST_LOG=debug
+    depends_on:
+      - postgres_beta
+  postgres_beta:
+    image: postgres:12-alpine
+    environment:
+      - POSTGRES_USER=lemmy
+      - POSTGRES_PASSWORD=password
+      - POSTGRES_DB=lemmy
+    volumes:
+      - ./volumes/postgres_beta:/var/lib/postgresql/data
+
+  lemmy-gamma:
+    image: dessalines/lemmy:travis
+    environment:
+      - LEMMY_HOSTNAME=lemmy-gamma:8560
+      - LEMMY_DATABASE_URL=postgres://lemmy:password@postgres_gamma:5432/lemmy
+      - LEMMY_JWT_SECRET=changeme
+      - LEMMY_FRONT_END_DIR=/app/dist
+      - LEMMY_FEDERATION__ENABLED=true
+      - LEMMY_FEDERATION__TLS_ENABLED=false
+      - LEMMY_FEDERATION__ALLOWED_INSTANCES=lemmy-alpha,lemmy-beta,lemmy-delta,lemmy-epsilon
+      - LEMMY_PORT=8560
+      - LEMMY_SETUP__ADMIN_USERNAME=lemmy_gamma
+      - LEMMY_SETUP__ADMIN_PASSWORD=lemmy
+      - LEMMY_SETUP__SITE_NAME=lemmy-gamma
+      - LEMMY_RATE_LIMIT__POST=99999
+      - LEMMY_RATE_LIMIT__REGISTER=99999
+      - LEMMY_CAPTCHA__ENABLED=false
+      - RUST_BACKTRACE=1
+      - RUST_LOG=debug
+    depends_on:
+      - postgres_gamma
+  postgres_gamma:
+    image: postgres:12-alpine
+    environment:
+      - POSTGRES_USER=lemmy
+      - POSTGRES_PASSWORD=password
+      - POSTGRES_DB=lemmy
+    volumes:
+      - ./volumes/postgres_gamma:/var/lib/postgresql/data
+
+  # An instance with only an allowlist for beta
+  lemmy-delta:
+    image: dessalines/lemmy:travis
+    environment:
+      - LEMMY_HOSTNAME=lemmy-delta:8570
+      - LEMMY_DATABASE_URL=postgres://lemmy:password@postgres_delta:5432/lemmy
+      - LEMMY_JWT_SECRET=changeme
+      - LEMMY_FRONT_END_DIR=/app/dist
+      - LEMMY_FEDERATION__ENABLED=true
+      - LEMMY_FEDERATION__TLS_ENABLED=false
+      - LEMMY_FEDERATION__ALLOWED_INSTANCES=lemmy-beta
+      - LEMMY_PORT=8570
+      - LEMMY_SETUP__ADMIN_USERNAME=lemmy_delta
+      - LEMMY_SETUP__ADMIN_PASSWORD=lemmy
+      - LEMMY_SETUP__SITE_NAME=lemmy-delta
+      - LEMMY_RATE_LIMIT__POST=99999
+      - LEMMY_RATE_LIMIT__REGISTER=99999
+      - LEMMY_CAPTCHA__ENABLED=false
+      - RUST_BACKTRACE=1
+      - RUST_LOG=debug
+    depends_on:
+      - postgres_delta
+  postgres_delta:
+    image: postgres:12-alpine
+    environment:
+      - POSTGRES_USER=lemmy
+      - POSTGRES_PASSWORD=password
+      - POSTGRES_DB=lemmy
+    volumes:
+      - ./volumes/postgres_delta:/var/lib/postgresql/data
+
+  # An instance who has a blocklist, with lemmy-alpha blocked
+  lemmy-epsilon:
+    image: dessalines/lemmy:travis
+    environment:
+      - LEMMY_HOSTNAME=lemmy-epsilon:8580
+      - LEMMY_DATABASE_URL=postgres://lemmy:password@postgres_epsilon:5432/lemmy
+      - LEMMY_JWT_SECRET=changeme
+      - LEMMY_FRONT_END_DIR=/app/dist
+      - LEMMY_FEDERATION__ENABLED=true
+      - LEMMY_FEDERATION__TLS_ENABLED=false
+      - LEMMY_FEDERATION__BLOCKED_INSTANCES=lemmy-alpha
+      - LEMMY_PORT=8580
+      - LEMMY_SETUP__ADMIN_USERNAME=lemmy_epsilon
+      - LEMMY_SETUP__ADMIN_PASSWORD=lemmy
+      - LEMMY_SETUP__SITE_NAME=lemmy-epsilon
+      - LEMMY_RATE_LIMIT__POST=99999
+      - LEMMY_RATE_LIMIT__REGISTER=99999
+      - LEMMY_CAPTCHA__ENABLED=false
+      - RUST_BACKTRACE=1
+      - RUST_LOG=debug
+    depends_on:
+      - postgres_epsilon
+  postgres_epsilon:
+    image: postgres:12-alpine
+    environment:
+      - POSTGRES_USER=lemmy
+      - POSTGRES_PASSWORD=password
+      - POSTGRES_DB=lemmy
+    volumes:
+      - ./volumes/postgres_epsilon:/var/lib/postgresql/data
+
+  iframely:
+    image: dogbin/iframely:latest
+    volumes:
+      - ../iframely.config.local.js:/iframely/config.local.js:ro
+    restart: always
diff --git a/docker/travis/docker_push.sh b/docker/travis/docker_push.sh
new file mode 100644 (file)
index 0000000..a04f105
--- /dev/null
@@ -0,0 +1,5 @@
+#!/bin/sh
+echo "$DOCKER_PASSWORD" | docker login -u "$DOCKER_USERNAME" --password-stdin
+docker tag dessalines/lemmy:travis \
+  dessalines/lemmy:v0.7.55
+docker push dessalines/lemmy:v0.7.55
diff --git a/docker/travis/run-tests.sh b/docker/travis/run-tests.sh
new file mode 100755 (executable)
index 0000000..658ffc0
--- /dev/null
@@ -0,0 +1,28 @@
+#!/bin/bash
+set -e
+
+# make sure there are no old containers or old data around
+sudo docker-compose down
+sudo rm -rf volumes
+
+mkdir -p volumes/pictrs_{alpha,beta,gamma,delta,epsilon}
+sudo chown -R 991:991 volumes/pictrs_{alpha,beta,gamma,delta,epsilon}
+
+sudo docker build ../../ --file ../prod/Dockerfile --tag dessalines/lemmy:travis
+
+sudo docker-compose up -d
+
+pushd ../../ui
+echo "Waiting for Lemmy to start..."
+while [[ "$(curl -s -o /dev/null -w '%{http_code}' 'localhost:8540/api/v1/site')" != "200" ]]; do sleep 1; done
+while [[ "$(curl -s -o /dev/null -w '%{http_code}' 'localhost:8550/api/v1/site')" != "200" ]]; do sleep 1; done
+while [[ "$(curl -s -o /dev/null -w '%{http_code}' 'localhost:8560/api/v1/site')" != "200" ]]; do sleep 1; done
+while [[ "$(curl -s -o /dev/null -w '%{http_code}' 'localhost:8570/api/v1/site')" != "200" ]]; do sleep 1; done
+while [[ "$(curl -s -o /dev/null -w '%{http_code}' 'localhost:8580/api/v1/site')" != "200" ]]; do sleep 1; done
+yarn
+yarn api-test
+popd
+
+sudo docker-compose down
+
+sudo rm -r volumes/
index 2c0e418b301ad2c8598a7563bf056999c581ed6e..8db35b4f6c2c0a8372d93cf750638aa70142cfdd 100644 (file)
@@ -2,7 +2,7 @@
 
 Front Page|Post
 ---|---
-![main screen](https://raw.githubusercontent.com/LemmyNet/lemmy/master/docs/img/main_screen.png)|![chat screen](https://raw.githubusercontent.com/LemmyNet/lemmy/master/docs/img/chat_screen.png)
+![main screen](https://raw.githubusercontent.com/LemmyNet/lemmy/main/docs/img/main_screen.png)|![chat screen](https://raw.githubusercontent.com/LemmyNet/lemmy/main/docs/img/chat_screen.png)
 
 [Lemmy](https://github.com/LemmyNet/lemmy) is similar to sites like [Reddit](https://reddit.com), [Lobste.rs](https://lobste.rs), [Raddle](https://raddle.me), or [Hacker News](https://news.ycombinator.com/): you subscribe to forums you're interested in, post links and discussions, then vote, and comment on them. Behind the scenes, it is very different; anyone can easily run a server, and all these servers are federated (think email), and connected to the same universe, called the [Fediverse](https://en.wikipedia.org/wiki/Fediverse).
 
index 6f709b580d11e8189814020cc460b48a5455ad27..3c5e34b4b736d2a5592b33210e29d29383cfba51 100644 (file)
@@ -35,6 +35,8 @@ Horizontal Rule <br>\--- | Horizontal Rule<br>\*\*\* | Horizontal Rule  <br><hr>
 \`Inline code\` with backticks | |`Inline code` with backticks 
 \`\`\`<br>\# code block <br>print '3 backticks or'<br>print 'indent 4 spaces' <br>\`\`\` | ····\# code block<br>····print '3 backticks or'<br>····print 'indent 4 spaces' | \# code block <br>print '3 backticks or'<br>print 'indent 4 spaces'
 ::: spoiler hidden or nsfw stuff<br>*a bunch of spoilers here*<br>::: | | <details><summary> hidden or nsfw stuff </summary><p><em>a bunch of spoilers here</em></p></details>
+Some ~subscript~ text | | Some <sub>subscript</sub> text
+Some ^superscript^ text | | Some <sup>superscript</sup> text
 
 [CommonMark Tutorial](https://commonmark.org/help/tutorial/)
 
index fe9e82bbb6c4e0cdda5658d0c66d937a707f4f66..0f91b7e3936c378697466c52b5f9e77c5f6748a2 100644 (file)
@@ -18,7 +18,9 @@ Score = Upvotes - Downvotes
 Time = time since submission (in hours)
 Gravity = Decay gravity, 1.8 is default
 ```
-- For posts, in order to bring up active posts, it uses the latest comment time (limited to a max creation age of a month ago)
+- Lemmy uses the same `Rank` algorithm above, in two sorts: `Active`, and `Hot`.
+  - `Active` uses the post votes, and latest comment time (limited to two days).
+  - `Hot` uses the post votes, and the post published time.
 - Use Max(1, score) to make sure all comments are affected by time decay.
 - Add 3 to the score, so that everything that has less than 3 downvotes will seem new. Otherwise all new comments would stay at zero, near the bottom.
 - The sign and abs of the score are necessary for dealing with the log of negative scores.
@@ -26,4 +28,4 @@ Gravity = Decay gravity, 1.8 is default
 
 A plot of rank over 24 hours, of scores of 1, 5, 10, 100, 1000, with a scale factor of 10k.
 
-![](https://raw.githubusercontent.com/LemmyNet/lemmy/master/docs/img/rank_algorithm.png)
+![](https://raw.githubusercontent.com/LemmyNet/lemmy/main/docs/img/rank_algorithm.png)
index fe97cf88047ec7ef9f8de8212136e17535637506..633c687fb7d4364ec9b5c040bfce6e1080de1176 100644 (file)
@@ -9,14 +9,14 @@ When using docker or ansible, there should be a `volumes` folder, which contains
 To incrementally backup the DB to an `.sql` file, you can run: 
 
 ```bash
-docker exec -t FOLDERNAME_postgres_1 pg_dumpall -c -U lemmy >  lemmy_dump_`date +%Y-%m-%d"_"%H_%M_%S`.sql
+docker-compose exec postgres pg_dumpall -c -U lemmy >  lemmy_dump_`date +%Y-%m-%d"_"%H_%M_%S`.sql
 ```
 ### A Sample backup script
 
 ```bash
 #!/bin/sh
 # DB Backup
-ssh MY_USER@MY_IP "docker exec -t FOLDERNAME_postgres_1 pg_dumpall -c -U lemmy" >  ~/BACKUP_LOCATION/INSTANCE_NAME_dump_`date +%Y-%m-%d"_"%H_%M_%S`.sql
+ssh MY_USER@MY_IP "docker-compose exec postgres pg_dumpall -c -U lemmy" >  ~/BACKUP_LOCATION/INSTANCE_NAME_dump_`date +%Y-%m-%d"_"%H_%M_%S`.sql
 
 # Volumes folder Backup
 rsync -avP -zz --rsync-path="sudo rsync" MY_USER@MY_IP:/LEMMY_LOCATION/volumes ~/BACKUP_LOCATION/FOLDERNAME
@@ -37,6 +37,45 @@ cat db_dump.sql  |  docker exec -i FOLDERNAME_postgres_1 psql -U lemmy # restore
 docker exec -i FOLDERNAME_postgres_1 psql -U lemmy -c "alter user lemmy with password 'bleh'"
 ```
 
+### Changing your domain name
+
+If you haven't federated yet, you can change your domain name in the DB. **Warning: do not do this after you've federated, or it will break federation.**
+
+Get into `psql` for your docker: 
+
+`docker-compose exec postgres psql -U lemmy`
+
+```
+-- Post
+update post set ap_id = replace (ap_id, 'old_domain', 'new_domain');
+update post set url = replace (url, 'old_domain', 'new_domain');
+update post set body = replace (body, 'old_domain', 'new_domain');
+update post set thumbnail_url = replace (thumbnail_url, 'old_domain', 'new_domain');
+
+delete from post_aggregates_fast;
+insert into post_aggregates_fast select * from post_aggregates_view;
+
+-- Comments
+update comment set ap_id = replace (ap_id, 'old_domain', 'new_domain');
+update comment set content = replace (content, 'old_domain', 'new_domain');
+
+delete from comment_aggregates_fast;
+insert into comment_aggregates_fast select * from comment_aggregates_view;
+
+-- User
+update user_ set actor_id = replace (actor_id, 'old_domain', 'new_domain');
+update user_ set avatar = replace (avatar, 'old_domain', 'new_domain');
+
+delete from user_fast;
+insert into user_fast select * from user_view;
+
+-- Community
+update community set actor_id = replace (actor_id, 'old_domain', 'new_domain');
+
+delete from community_aggregates_fast;
+insert into community_aggregates_fast select * from community_aggregates_view;
+```
+
 ## More resources
 
 - https://stackoverflow.com/questions/24718706/backup-restore-a-dockerized-postgresql-database
index cc4c568987a4d0b309d9ed8cf0eb9a66937f4316..ce9dc2ba835cf600abcf01607378766ff02b59fc 100644 (file)
@@ -1,11 +1,11 @@
 # Configuration
 
 The configuration is based on the file
-[defaults.hjson](https://yerbamate.dev/LemmyNet/lemmy/src/branch/master/server/config/defaults.hjson).
+[defaults.hjson](https://yerbamate.dev/LemmyNet/lemmy/src/branch/main/server/config/defaults.hjson).
 This file also contains documentation for all the available options. To override the defaults, you
 can copy the options you want to change into your local `config.hjson` file.
 
-To use a different `config.hjson` location than the current directory, set the environment variable `LEMMY_CONFIG_LOCATION`.
+To use a different `config.hjson` location than the current directory, set the environment variable `LEMMY_CONFIG_LOCATION`. Make sure you copy the `defaults.hjson` if you do this, otherwise you will be missing settings.
 
 Additionally, you can override any config files with environment variables. These have the same
 name as the config options, and are prefixed with `LEMMY_`. For example, you can override the
index 4676f47d43309a8921f2a923381b9e6a6717419d..849957ad10910bdd2ddc4b699cc1f092dd1be54a 100644 (file)
@@ -19,7 +19,7 @@ ansible-playbook lemmy.yml --become
 
 To update to a new version, just run the following in your local Lemmy repo:
 ```bash
-git pull origin master
+git pull origin main
 cd ansible
 ansible-playbook lemmy.yml --become
 ```
index a2bed794f9fb0a0d4a0b34c1441574d5f5f0710a..ae0375a81614ffd0ed4b049b5c4a6dd413fe92e6 100644 (file)
@@ -8,9 +8,9 @@ mkdir /lemmy
 cd /lemmy
 
 # download default config files
-wget https://raw.githubusercontent.com/LemmyNet/lemmy/master/docker/prod/docker-compose.yml
-wget https://raw.githubusercontent.com/LemmyNet/lemmy/master/docker/lemmy.hjson
-wget https://raw.githubusercontent.com/LemmyNet/lemmy/master/docker/iframely.config.local.js
+wget https://raw.githubusercontent.com/LemmyNet/lemmy/main/docker/prod/docker-compose.yml
+wget https://raw.githubusercontent.com/LemmyNet/lemmy/main/docker/lemmy.hjson
+wget https://raw.githubusercontent.com/LemmyNet/lemmy/main/docker/iframely.config.local.js
 
 # Set correct permissions for pictrs folder
 mkdir -p volumes/pictrs
@@ -21,10 +21,10 @@ After this, have a look at the [config file](administration_configuration.md) na
 
 `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/LemmyNet/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/main/ansible/templates/nginx.conf), could be setup with:
 
 ```bash
-wget https://raw.githubusercontent.com/LemmyNet/lemmy/master/ansible/templates/nginx.conf
+wget https://raw.githubusercontent.com/LemmyNet/lemmy/main/ansible/templates/nginx.conf
 # Replace the {{ vars }}
 sudo mv nginx.conf /etc/nginx/sites-enabled/lemmy.conf
 ```
@@ -36,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:
 
 ```bash
-wget https://raw.githubusercontent.com/LemmyNet/lemmy/master/docker/prod/docker-compose.yml
+wget https://raw.githubusercontent.com/LemmyNet/lemmy/main/docker/prod/docker-compose.yml
 docker-compose up -d
 ```
index 143ae9f8bd1e0532bcab644fd8d09c08ca4c7e21..8af38a077d11cfe6a6ba4ae7409f20897e8b05e9 100644 (file)
@@ -68,3 +68,16 @@ cd /lemmy/
 sudo docker-compose pull
 sudo docker-compose up -d
 ```
+
+## Security Model
+
+- HTTP signature verify: This ensures that activity really comes from the activity that it claims
+- check_is_apub_valid : Makes sure its in our allowed instances list
+- Lower level checks: To make sure that the user that creates/updates/removes a post is actually on the same instance as that post
+
+For the last point, note that we are *not* checking whether the actor that sends the create activity for a post is
+actually identical to the post's creator, or that the user that removes a post is a mod/admin. These things are checked
+by the API code, and its the responsibility of each instance to check user permissions. This does not leave any attack
+vector, as a normal instance user cant do actions that violate the API rules. The only one who could do that is the
+admin (and the software deployed by the admin). But the admin can do anything on the instance, including send activities
+from other user accounts. So we wouldnt actually gain any security by checking mod permissions or similar.
\ No newline at end of file
index 066386f50747eac553368c0ec998e7e7ca77a98f..b95c308da2c2d67816ed234e0bdf31ae77eccc4c 100644 (file)
@@ -1,16 +1,26 @@
-### Ubuntu
-
-
-#### Build requirements:
+### Install build requirements
+#### Ubuntu
 ```
-sudo apt install git cargo libssl-dev pkg-config libpq-dev yarn curl gnupg2 git
+sudo apt install git cargo libssl-dev pkg-config libpq-dev yarn curl gnupg2 espeak
 # 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
 ```
 
-#### Get the source code
+#### macOS
+
+Install Rust using [the recommended option on rust-lang.org](https://www.rust-lang.org/tools/install) (rustup).
+
+Then, install [Homebrew](https://brew.sh/) if you don't already have it installed.
+
+Finally, install Node and Yarn.
+
+```
+brew install node yarn
+```
+
+### Get the source code
 ```
 git clone https://github.com/LemmyNet/lemmy.git
 # or alternatively from gitea
@@ -20,36 +30,49 @@ git clone https://github.com/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)
+### Build the backend (Rust)
 ```
 cd server
 cargo build
 # for development, use `cargo check` instead)
 ```
 
-#### Build the frontend (Typescript)
+### Build the frontend (Typescript)
 ```
 cd ui
 yarn
 yarn build
 ```
 
-#### Setup postgresql
+### Setup postgresql
+#### Ubuntu
 ```
 sudo apt install postgresql
 sudo systemctl start postgresql
-# initialize postgres database
+
+# Either execute server/db-init.sh, or manually initialize the 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
-# or execute server/db-init.sh
 ```
 
-#### Run a local development instance
+#### macOS
+```
+brew install postgresql
+brew services start postgresql
+/usr/local/opt/postgres/bin/createuser -s postgres
+
+# Either execute server/db-init.sh, or manually initialize the postgres database:
+psql -c "create user lemmy with password 'password' superuser;" -U postgres
+psql -c 'create database lemmy with owner lemmy;' -U postgres
+export LEMMY_DATABASE_URL=postgres://lemmy:password@localhost:5432/lemmy
+```
+
+### Run a local development instance
 ```
 # run each of these in a seperate terminal
 cd server && cargo run
-ui & yarn start
+cd ui && yarn start
 ```
 
 Then open [localhost:4444](http://localhost:4444) in your browser. It will auto-refresh if you edit
index 6ed25b98ebe09290185a3d5d8cb48e287ac70a93..8ad7fbbf8ecb2afd22a33e69987b42c12fefc63f 100644 (file)
@@ -17,6 +17,7 @@
 - [Errors](#errors)
 - [API documentation](#api-documentation)
   * [Sort Types](#sort-types)
+  * [Undoing actions](#undoing-actions)
   * [Websocket vs HTTP](#websocket-vs-http)
   * [User / Authentication / Admin actions](#user--authentication--admin-actions)
     + [Login](#login)
       - [Request](#request-5)
       - [Response](#response-5)
       - [HTTP](#http-6)
-    + [Edit User Mention](#edit-user-mention)
+    + [Mark User Mention as read](#mark-user-mention-as-read)
       - [Request](#request-6)
       - [Response](#response-6)
       - [HTTP](#http-7)
-    + [Mark All As Read](#mark-all-as-read)
+    + [Get Private Messages](#get-private-messages)
       - [Request](#request-7)
       - [Response](#response-7)
       - [HTTP](#http-8)
-    + [Delete Account](#delete-account)
+    + [Create Private Message](#create-private-message)
       - [Request](#request-8)
       - [Response](#response-8)
       - [HTTP](#http-9)
-    + [Add admin](#add-admin)
+    + [Edit Private Message](#edit-private-message)
       - [Request](#request-9)
       - [Response](#response-9)
       - [HTTP](#http-10)
-    + [Ban user](#ban-user)
+    + [Delete Private Message](#delete-private-message)
       - [Request](#request-10)
       - [Response](#response-10)
       - [HTTP](#http-11)
-  * [Site](#site)
-    + [List Categories](#list-categories)
+    + [Mark Private Message as Read](#mark-private-message-as-read)
       - [Request](#request-11)
       - [Response](#response-11)
       - [HTTP](#http-12)
-    + [Search](#search)
+    + [Mark All As Read](#mark-all-as-read)
       - [Request](#request-12)
       - [Response](#response-12)
       - [HTTP](#http-13)
-    + [Get Modlog](#get-modlog)
+    + [Delete Account](#delete-account)
       - [Request](#request-13)
       - [Response](#response-13)
       - [HTTP](#http-14)
-    + [Create Site](#create-site)
+    + [Add admin](#add-admin)
       - [Request](#request-14)
       - [Response](#response-14)
       - [HTTP](#http-15)
-    + [Edit Site](#edit-site)
+    + [Ban user](#ban-user)
       - [Request](#request-15)
       - [Response](#response-15)
       - [HTTP](#http-16)
-    + [Get Site](#get-site)
+  * [Site](#site)
+    + [List Categories](#list-categories)
       - [Request](#request-16)
       - [Response](#response-16)
       - [HTTP](#http-17)
-    + [Transfer Site](#transfer-site)
+    + [Search](#search)
       - [Request](#request-17)
       - [Response](#response-17)
       - [HTTP](#http-18)
-    + [Get Site Config](#get-site-config)
+    + [Get Modlog](#get-modlog)
       - [Request](#request-18)
       - [Response](#response-18)
       - [HTTP](#http-19)
-    + [Save Site Config](#save-site-config)
+    + [Create Site](#create-site)
       - [Request](#request-19)
       - [Response](#response-19)
       - [HTTP](#http-20)
-  * [Community](#community)
-    + [Get Community](#get-community)
+    + [Edit Site](#edit-site)
       - [Request](#request-20)
       - [Response](#response-20)
       - [HTTP](#http-21)
-    + [Create Community](#create-community)
+    + [Get Site](#get-site)
       - [Request](#request-21)
       - [Response](#response-21)
       - [HTTP](#http-22)
-    + [List Communities](#list-communities)
+    + [Transfer Site](#transfer-site)
       - [Request](#request-22)
       - [Response](#response-22)
       - [HTTP](#http-23)
-    + [Ban from Community](#ban-from-community)
+    + [Get Site Config](#get-site-config)
       - [Request](#request-23)
       - [Response](#response-23)
       - [HTTP](#http-24)
-    + [Add Mod to Community](#add-mod-to-community)
+    + [Save Site Config](#save-site-config)
       - [Request](#request-24)
       - [Response](#response-24)
       - [HTTP](#http-25)
-    + [Edit Community](#edit-community)
+  * [Community](#community)
+    + [Get Community](#get-community)
       - [Request](#request-25)
       - [Response](#response-25)
       - [HTTP](#http-26)
-    + [Follow Community](#follow-community)
+    + [Create Community](#create-community)
       - [Request](#request-26)
       - [Response](#response-26)
       - [HTTP](#http-27)
-    + [Get Followed Communities](#get-followed-communities)
+    + [List Communities](#list-communities)
       - [Request](#request-27)
       - [Response](#response-27)
       - [HTTP](#http-28)
-    + [Transfer Community](#transfer-community)
+    + [Ban from Community](#ban-from-community)
       - [Request](#request-28)
       - [Response](#response-28)
       - [HTTP](#http-29)
-  * [Post](#post)
-    + [Create Post](#create-post)
+    + [Add Mod to Community](#add-mod-to-community)
       - [Request](#request-29)
       - [Response](#response-29)
       - [HTTP](#http-30)
-    + [Get Post](#get-post)
+    + [Edit Community](#edit-community)
       - [Request](#request-30)
       - [Response](#response-30)
       - [HTTP](#http-31)
-    + [Get Posts](#get-posts)
+    + [Delete Community](#delete-community)
       - [Request](#request-31)
       - [Response](#response-31)
       - [HTTP](#http-32)
-    + [Create Post Like](#create-post-like)
+    + [Remove Community](#remove-community)
       - [Request](#request-32)
       - [Response](#response-32)
       - [HTTP](#http-33)
-    + [Edit Post](#edit-post)
+    + [Follow Community](#follow-community)
       - [Request](#request-33)
       - [Response](#response-33)
       - [HTTP](#http-34)
-    + [Save Post](#save-post)
+    + [Get Followed Communities](#get-followed-communities)
       - [Request](#request-34)
       - [Response](#response-34)
       - [HTTP](#http-35)
-  * [Comment](#comment)
-    + [Create Comment](#create-comment)
+    + [Transfer Community](#transfer-community)
       - [Request](#request-35)
       - [Response](#response-35)
       - [HTTP](#http-36)
-    + [Edit Comment](#edit-comment)
+  * [Post](#post)
+    + [Create Post](#create-post)
       - [Request](#request-36)
       - [Response](#response-36)
       - [HTTP](#http-37)
-    + [Save Comment](#save-comment)
+    + [Get Post](#get-post)
       - [Request](#request-37)
       - [Response](#response-37)
       - [HTTP](#http-38)
-    + [Create Comment Like](#create-comment-like)
+    + [Get Posts](#get-posts)
       - [Request](#request-38)
       - [Response](#response-38)
       - [HTTP](#http-39)
+    + [Create Post Like](#create-post-like)
+      - [Request](#request-39)
+      - [Response](#response-39)
+      - [HTTP](#http-40)
+    + [Edit Post](#edit-post)
+      - [Request](#request-40)
+      - [Response](#response-40)
+      - [HTTP](#http-41)
+    + [Delete Post](#delete-post)
+      - [Request](#request-41)
+      - [Response](#response-41)
+      - [HTTP](#http-42)
+    + [Remove Post](#remove-post)
+      - [Request](#request-42)
+      - [Response](#response-42)
+      - [HTTP](#http-43)
+    + [Lock Post](#lock-post)
+      - [Request](#request-43)
+      - [Response](#response-43)
+      - [HTTP](#http-44)
+    + [Sticky Post](#sticky-post)
+      - [Request](#request-44)
+      - [Response](#response-44)
+      - [HTTP](#http-45)
+    + [Save Post](#save-post)
+      - [Request](#request-45)
+      - [Response](#response-45)
+      - [HTTP](#http-46)
+  * [Comment](#comment)
+    + [Create Comment](#create-comment)
+      - [Request](#request-46)
+      - [Response](#response-46)
+      - [HTTP](#http-47)
+    + [Edit Comment](#edit-comment)
+      - [Request](#request-47)
+      - [Response](#response-47)
+      - [HTTP](#http-48)
+    + [Delete Comment](#delete-comment)
+      - [Request](#request-48)
+      - [Response](#response-48)
+      - [HTTP](#http-49)
+    + [Remove Comment](#remove-comment)
+      - [Request](#request-49)
+      - [Response](#response-49)
+      - [HTTP](#http-50)
+    + [Mark Comment as Read](#mark-comment-as-read)
+      - [Request](#request-50)
+      - [Response](#response-50)
+      - [HTTP](#http-51)
+    + [Save Comment](#save-comment)
+      - [Request](#request-51)
+      - [Response](#response-51)
+      - [HTTP](#http-52)
+    + [Create Comment Like](#create-comment-like)
+      - [Request](#request-52)
+      - [Response](#response-52)
+      - [HTTP](#http-53)
   * [RSS / Atom feeds](#rss--atom-feeds)
     + [All](#all)
     + [Community](#community-1)
@@ -273,7 +330,8 @@ curl -i -H \
 
 These go wherever there is a `sort` field. The available sort types are:
 
-- `Hot` - the hottest posts/communities, depending on votes, views, comments and publish date
+- `Active` - the hottest posts/communities, depending on votes, and newest comment publish date.
+- `Hot` - the hottest posts/communities, depending on votes and publish date.
 - `New` - the newest posts/communities
 - `TopDay` - the most upvoted posts/communities of the current day.
 - `TopWeek` - the most upvoted posts/communities of the current week.
@@ -281,6 +339,10 @@ These go wherever there is a `sort` field. The available sort types are:
 - `TopYear` - the most upvoted posts/communities of the current year.
 - `TopAll` - the most upvoted posts/communities on the current instance.
 
+### Undoing actions
+
+Whenever you see a `deleted: bool`, `removed: bool`, `read: bool`, `locked: bool`, etc, you can undo this action by sending `false`.
+
 ### Websocket vs HTTP
 
 - Below are the websocket JSON requests / responses. For HTTP, ignore all fields except those inside `data`.
@@ -329,7 +391,9 @@ Only the first user will be able to be the admin.
     email: Option<String>,
     password: String,
     password_verify: String,
-    admin: bool
+    admin: bool,
+    captcha_uuid: Option<String>, // Only checked if these are enabled in the server
+    captcha_answer: Option<String>,
   }
 }
 ```
@@ -347,6 +411,34 @@ Only the first user will be able to be the admin.
 
 `POST /user/register`
 
+#### Get Captcha
+
+These expire after 10 minutes.
+
+##### Request
+```rust
+{
+  op: "GetCaptcha",
+}
+```
+##### Response
+```rust
+{
+  op: "GetCaptcha",
+  data: {
+    ok?: { // Will be undefined if captchas are disabled
+      png: String, // A Base64 encoded png
+      wav: Option<String>, // A Base64 encoded wav audio file
+      uuid: String,
+    }
+  }
+}
+```
+
+##### HTTP
+
+`GET /user/get_captcha`
+
 #### Get User Details
 ##### Request
 ```rust
@@ -391,7 +483,19 @@ Only the first user will be able to be the admin.
     theme: String, // Default 'darkly'
     default_sort_type: i16, // The Sort types from above, zero indexed as a number
     default_listing_type: i16, // Post listing types are `All, Subscribed, Community`
-    auth: String
+    lang: String,
+    avatar: Option<String>,
+    banner: Option<String>,
+    preferred_username: Option<String>,
+    email: Option<String>,
+    bio: Option<String>,
+    matrix_user_id: Option<String>,
+    new_password: Option<String>,
+    new_password_verify: Option<String>,
+    old_password: Option<String>,
+    show_avatars: bool,
+    send_notifications_to_email: bool,
+    auth: String,
   }
 }
 ```
@@ -464,14 +568,17 @@ Only the first user will be able to be the admin.
 
 `GET /user/mentions`
 
-#### Edit User Mention
+#### Mark User Mention as read
+
+Only the recipient can do this.
+
 ##### Request
 ```rust
 {
-  op: "EditUserMention",
+  op: "MarkUserMentionAsRead",
   data: {
     user_mention_id: i32,
-    read: Option<bool>,
+    read: bool,
     auth: String,
   }
 }
@@ -479,7 +586,7 @@ Only the first user will be able to be the admin.
 ##### Response
 ```rust
 {
-  op: "EditUserMention",
+  op: "MarkUserMentionAsRead",
   data: {
     mention: UserMentionView,
   }
@@ -487,7 +594,141 @@ Only the first user will be able to be the admin.
 ```
 ##### HTTP
 
-`PUT /user/mention`
+`POST /user/mention/mark_as_read`
+
+#### Get Private Messages
+##### Request
+```rust
+{
+  op: "GetPrivateMessages",
+  data: {
+    unread_only: bool,
+    page: Option<i64>,
+    limit: Option<i64>,
+    auth: String,
+  }
+}
+```
+##### Response
+```rust
+{
+  op: "GetPrivateMessages",
+  data: {
+    messages: Vec<PrivateMessageView>,
+  }
+}
+```
+
+##### HTTP
+
+`GET /private_message/list`
+
+#### Create Private Message
+##### Request
+```rust
+{
+  op: "CreatePrivateMessage",
+  data: {
+    content: String,
+    recipient_id: i32,
+    auth: String,
+  }
+}
+```
+##### Response
+```rust
+{
+  op: "CreatePrivateMessage",
+  data: {
+    message: PrivateMessageView,
+  }
+}
+```
+
+##### HTTP
+
+`POST /private_message`
+
+#### Edit Private Message
+##### Request
+```rust
+{
+  op: "EditPrivateMessage",
+  data: {
+    edit_id: i32,
+    content: String,
+    auth: String,
+  }
+}
+```
+##### Response
+```rust
+{
+  op: "EditPrivateMessage",
+  data: {
+    message: PrivateMessageView,
+  }
+}
+```
+
+##### HTTP
+
+`PUT /private_message`
+
+#### Delete Private Message
+##### Request
+```rust
+{
+  op: "DeletePrivateMessage",
+  data: {
+    edit_id: i32,
+    deleted: bool,
+    auth: String,
+  }
+}
+```
+##### Response
+```rust
+{
+  op: "DeletePrivateMessage",
+  data: {
+    message: PrivateMessageView,
+  }
+}
+```
+
+##### HTTP
+
+`POST /private_message/delete`
+
+#### Mark Private Message as Read
+
+Only the recipient can do this.
+
+##### Request
+```rust
+{
+  op: "MarkPrivateMessageAsRead",
+  data: {
+    edit_id: i32,
+    read: bool,
+    auth: String,
+  }
+}
+```
+##### Response
+```rust
+{
+  op: "MarkPrivateMessageAsRead",
+  data: {
+    message: PrivateMessageView,
+  }
+}
+```
+
+##### HTTP
+
+`POST /private_message/mark_as_read`
 
 #### Mark All As Read
 
@@ -577,6 +818,7 @@ Marks all user replies and mentions as read.
   data: {
     user_id: i32,
     ban: bool,
+    remove_data: Option<bool>, // Removes/Restores their comments, posts, and communities
     reason: Option<String>,
     expires: Option<i64>,
     auth: String
@@ -696,6 +938,8 @@ Search types are `All, Comments, Posts, Communities, Users, Url`
   data: {
     name: String,
     description: Option<String>,
+    icon: Option<String>,
+    banner: Option<String>,
     auth: String
   }
 }
@@ -722,6 +966,8 @@ Search types are `All, Comments, Posts, Communities, Users, Url`
   data: {
     name: String,
     description: Option<String>,
+    icon: Option<String>,
+    banner: Option<String>,
     auth: String
   }
 }
@@ -744,6 +990,10 @@ Search types are `All, Comments, Posts, Communities, Users, Url`
 ```rust
 {
   op: "GetSite"
+  data: {
+    auth: Option<String>,
+  }
+
 }
 ```
 ##### Response
@@ -754,6 +1004,9 @@ Search types are `All, Comments, Posts, Communities, Users, Url`
     site: Option<SiteView>,
     admins: Vec<UserView>,
     banned: Vec<UserView>,
+    online: usize, // This is currently broken
+    version: String,
+    my_user: Option<User_>, // Gives back your user and settings if logged in
   }
 }
 ```
@@ -854,7 +1107,6 @@ Search types are `All, Comments, Posts, Communities, Users, Url`
   data: {
     community: CommunityView,
     moderators: Vec<CommunityModeratorView>,
-    admins: Vec<UserView>,
   }
 }
 ```
@@ -871,6 +1123,8 @@ Search types are `All, Comments, Posts, Communities, Users, Url`
     name: String,
     title: String,
     description: Option<String>,
+    icon: Option<String>,
+    banner: Option<String>,
     category_id: i32 ,
     auth: String
   }
@@ -924,6 +1178,7 @@ Search types are `All, Comments, Posts, Communities, Users, Url`
     community_id: i32,
     user_id: i32,
     ban: bool,
+    remove_data: Option<bool>, // Removes/Restores their comments and posts for that community
     reason: Option<String>,
     expires: Option<i64>,
     auth: String
@@ -971,7 +1226,7 @@ Search types are `All, Comments, Posts, Communities, Users, Url`
 `POST /community/mod`
 
 #### Edit Community
-Mods and admins can remove and lock a community, creators can delete it.
+Only mods can edit a community.
 
 ##### Request
 ```rust
@@ -979,14 +1234,11 @@ Mods and admins can remove and lock a community, creators can delete it.
   op: "EditCommunity",
   data: {
     edit_id: i32,
-    name: String,
     title: String,
     description: Option<String>,
+    icon: Option<String>,
+    banner: Option<String>,
     category_id: i32,
-    removed: Option<bool>,
-    deleted: Option<bool>,
-    reason: Option<String>,
-    expires: Option<i64>,
     auth: String
   }
 }
@@ -1004,6 +1256,62 @@ Mods and admins can remove and lock a community, creators can delete it.
 
 `PUT /community`
 
+#### Delete Community
+Only a creator can delete a community
+
+##### Request
+```rust
+{
+  op: "DeleteCommunity",
+  data: {
+    edit_id: i32,
+    deleted: bool,
+    auth: String,
+  }
+}
+```
+##### Response
+```rust
+{
+  op: "DeleteCommunity",
+  data: {
+    community: CommunityView
+  }
+}
+```
+##### HTTP
+
+`POST /community/delete`
+
+#### Remove Community
+Only admins can remove a community.
+
+##### Request
+```rust
+{
+  op: "RemoveCommunity",
+  data: {
+    edit_id: i32,
+    removed: bool,
+    reason: Option<String>,
+    expires: Option<i64>,
+    auth: String,
+  }
+}
+```
+##### Response
+```rust
+{
+  op: "RemoveCommunity",
+  data: {
+    community: CommunityView
+  }
+}
+```
+##### HTTP
+
+`POST /community/remove`
+
 #### Follow Community
 ##### Request
 ```rust
@@ -1089,8 +1397,9 @@ Mods and admins can remove and lock a community, creators can delete it.
     name: String,
     url: Option<String>,
     body: Option<String>,
+    nsfw: bool,
     community_id: i32,
-    auth: String
+    auth: String,
   }
 }
 ```
@@ -1127,7 +1436,6 @@ Mods and admins can remove and lock a community, creators can delete it.
     comments: Vec<CommentView>,
     community: CommunityView,
     moderators: Vec<CommunityModeratorView>,
-    admins: Vec<UserView>,
   }
 }
 ```
@@ -1196,25 +1504,17 @@ Post listing types are `All, Subscribed, Community`
 `POST /post/like`
 
 #### Edit Post
-
-Mods and admins can remove and lock a post, creators can delete it.
-
 ##### Request
 ```rust
 {
   op: "EditPost",
   data: {
     edit_id: i32,
-    creator_id: i32,
-    community_id: i32,
     name: String,
     url: Option<String>,
     body: Option<String>,
-    removed: Option<bool>,
-    deleted: Option<bool>,
-    locked: Option<bool>,
-    reason: Option<String>,
-    auth: String
+    nsfw: bool,
+    auth: String,
   }
 }
 ```
@@ -1232,6 +1532,120 @@ Mods and admins can remove and lock a post, creators can delete it.
 
 `PUT /post`
 
+#### Delete Post
+##### Request
+```rust
+{
+  op: "DeletePost",
+  data: {
+    edit_id: i32,
+    deleted: bool,
+    auth: String,
+  }
+}
+```
+##### Response
+```rust
+{
+  op: "DeletePost",
+  data: {
+    post: PostView
+  }
+}
+```
+
+##### HTTP
+
+`POST /post/delete`
+
+#### Remove Post
+
+Only admins and mods can remove a post.
+
+##### Request
+```rust
+{
+  op: "RemovePost",
+  data: {
+    edit_id: i32,
+    removed: bool,
+    reason: Option<String>,
+    auth: String,
+  }
+}
+```
+##### Response
+```rust
+{
+  op: "RemovePost",
+  data: {
+    post: PostView
+  }
+}
+```
+
+##### HTTP
+
+`POST /post/remove`
+
+#### Lock Post
+
+Only admins and mods can lock a post.
+
+##### Request
+```rust
+{
+  op: "LockPost",
+  data: {
+    edit_id: i32,
+    locked: bool,
+    auth: String,
+  }
+}
+```
+##### Response
+```rust
+{
+  op: "LockPost",
+  data: {
+    post: PostView
+  }
+}
+```
+
+##### HTTP
+
+`POST /post/lock`
+
+#### Sticky Post
+
+Only admins and mods can sticky a post.
+
+##### Request
+```rust
+{
+  op: "StickyPost",
+  data: {
+    edit_id: i32,
+    stickied: bool,
+    auth: String,
+  }
+}
+```
+##### Response
+```rust
+{
+  op: "StickyPost",
+  data: {
+    post: PostView
+  }
+}
+```
+
+##### HTTP
+
+`POST /post/sticky`
+
 #### Save Post
 ##### Request
 ```rust
@@ -1266,8 +1680,8 @@ Mods and admins can remove and lock a post, creators can delete it.
   data: {
     content: String,
     parent_id: Option<i32>,
-    edit_id: Option<i32>,
     post_id: i32,
+    form_id: Option<String>, // An optional form id, so you know which message came back
     auth: String
   }
 }
@@ -1288,7 +1702,7 @@ Mods and admins can remove and lock a post, creators can delete it.
 
 #### Edit Comment
 
-Mods and admins can remove a comment, creators can delete it.
+Only the creator can edit the comment.
 
 ##### Request
 ```rust
@@ -1296,15 +1710,9 @@ Mods and admins can remove a comment, creators can delete it.
   op: "EditComment",
   data: {
     content: String,
-    parent_id: Option<i32>,
     edit_id: i32,
-    creator_id: i32,
-    post_id: i32,
-    removed: Option<bool>,
-    deleted: Option<bool>,
-    reason: Option<String>,
-    read: Option<bool>,
-    auth: String
+    form_id: Option<String>,
+    auth: String,
   }
 }
 ```
@@ -1321,6 +1729,92 @@ Mods and admins can remove a comment, creators can delete it.
 
 `PUT /comment`
 
+#### Delete Comment
+
+Only the creator can delete the comment.
+
+##### Request
+```rust
+{
+  op: "DeleteComment",
+  data: {
+    edit_id: i32,
+    deleted: bool,
+    auth: String,
+  }
+}
+```
+##### Response
+```rust
+{
+  op: "DeleteComment",
+  data: {
+    comment: CommentView
+  }
+}
+```
+##### HTTP
+
+`POST /comment/delete`
+
+
+#### Remove Comment
+
+Only a mod or admin can remove the comment.
+
+##### Request
+```rust
+{
+  op: "RemoveComment",
+  data: {
+    edit_id: i32,
+    removed: bool,
+    reason: Option<String>,
+    auth: String,
+  }
+}
+```
+##### Response
+```rust
+{
+  op: "RemoveComment",
+  data: {
+    comment: CommentView
+  }
+}
+```
+##### HTTP
+
+`POST /comment/remove`
+
+#### Mark Comment as Read
+
+Only the recipient can do this.
+
+##### Request
+```rust
+{
+  op: "MarkCommentAsRead",
+  data: {
+    edit_id: i32,
+    read: bool,
+    auth: String,
+  }
+}
+```
+##### Response
+```rust
+{
+  op: "MarkCommentAsRead",
+  data: {
+    comment: CommentView
+  }
+}
+```
+##### HTTP
+
+`POST /comment/mark_as_read`
+
 #### Save Comment
 ##### Request
 ```rust
@@ -1356,7 +1850,6 @@ Mods and admins can remove a comment, creators can delete it.
   op: "CreateCommentLike",
   data: {
     comment_id: i32,
-    post_id: i32,
     score: i16,
     auth: String
   }
index d90b96799b2e5fdb583135c78054e1eceaafd92b..9781adaf12e4f5ebd435fae43f552290cef5b2f9 100644 (file)
@@ -2,11 +2,10 @@
 # It is not intended for manual editing.
 [[package]]
 name = "activitystreams"
-version = "0.6.2"
+version = "0.7.0-alpha.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "464cb473bfb402b857cc15b1153974c203a43f1485da4dda15cd17a738548958"
+checksum = "e3490e8e9d7744aada19fb2fb4e2564f8c22fd080a3561093ac91ed7d10bfe78"
 dependencies = [
- "activitystreams-derive",
  "chrono",
  "mime",
  "serde 1.0.114",
@@ -15,36 +14,15 @@ dependencies = [
  "url",
 ]
 
-[[package]]
-name = "activitystreams-derive"
-version = "0.6.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c39ba5929399e9f921055bac76dd8f47419fa5b6b6da1ac4c1e82b94ed0ac7b4"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn",
-]
-
 [[package]]
 name = "activitystreams-ext"
-version = "0.1.0"
-source = "git+https://git.asonix.dog/asonix/activitystreams-ext#e5c97f4ea9f60e49bc7ff27fb0fb515d3190fd25"
-dependencies = [
- "activitystreams-new",
- "serde 1.0.114",
- "serde_json",
-]
-
-[[package]]
-name = "activitystreams-new"
-version = "0.1.0"
-source = "git+https://git.asonix.dog/asonix/activitystreams-sketch#99c7e9aa5596eda846a1ebd5978ca72d11d4c08a"
+version = "0.1.0-alpha.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bb8e19a0810cc25df3535061a08b7d8f8a734d309ea4411c57a9767e4a2ffa0e"
 dependencies = [
  "activitystreams",
  "serde 1.0.114",
  "serde_json",
- "typed-builder",
 ]
 
 [[package]]
@@ -55,7 +33,7 @@ checksum = "a9028932f36d45df020c92317ccb879ab77d8f066f57ff143dd5bee93ba3de0d"
 dependencies = [
  "actix-rt",
  "actix_derive",
- "bitflags",
+ "bitflags 1.2.1",
  "bytes",
  "crossbeam-channel",
  "derive_more",
@@ -78,7 +56,7 @@ version = "0.2.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "09e55f0a5c2ca15795035d90c46bd0e73a5123b72f68f12596d6ba5282051380"
 dependencies = [
- "bitflags",
+ "bitflags 1.2.1",
  "bytes",
  "futures-core",
  "futures-sink",
@@ -111,14 +89,14 @@ dependencies = [
 
 [[package]]
 name = "actix-files"
-version = "0.3.0-alpha.1"
+version = "0.3.0-beta.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "23b32e0fdd5998c2712549cbc39dff46c8754d55e3dd9f4d017d9e28de30cac6"
+checksum = "627f597ad98061816766201db8afc7444752992f2919b2e60f53a7fa27f01aed"
 dependencies = [
  "actix-http",
  "actix-service",
  "actix-web",
- "bitflags",
+ "bitflags 1.2.1",
  "bytes",
  "derive_more",
  "futures-core",
@@ -132,9 +110,9 @@ dependencies = [
 
 [[package]]
 name = "actix-http"
-version = "2.0.0-alpha.4"
+version = "2.0.0-beta.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fd7ea0568480d199952a51de70271946da57c33cc0e8b83f54383e70958dff21"
+checksum = "44529cd6813ebf4a2f2a6ea36ffe88a0e4b0bc08b26ad0b8f7f4581d4d4f3247"
 dependencies = [
  "actix-codec",
  "actix-connect",
@@ -144,9 +122,10 @@ dependencies = [
  "actix-tls",
  "actix-utils",
  "base64 0.12.3",
- "bitflags",
+ "bitflags 1.2.1",
  "brotli2",
  "bytes",
+ "cookie",
  "copyless",
  "derive_more",
  "either",
@@ -160,6 +139,7 @@ dependencies = [
  "http",
  "httparse",
  "indexmap",
+ "itoa",
  "language-tags",
  "lazy_static",
  "log",
@@ -171,7 +151,7 @@ dependencies = [
  "serde 1.0.114",
  "serde_json",
  "serde_urlencoded",
- "sha-1",
+ "sha-1 0.9.1",
  "slab",
  "time 0.2.16",
 ]
@@ -260,16 +240,16 @@ dependencies = [
 
 [[package]]
 name = "actix-threadpool"
-version = "0.3.2"
+version = "0.3.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "91164716d956745c79dcea5e66d2aa04506549958accefcede5368c70f2fd4ff"
+checksum = "d209f04d002854b9afd3743032a27b066158817965bf5d036824d19ac2cc0e30"
 dependencies = [
  "derive_more",
  "futures-channel",
  "lazy_static",
  "log",
  "num_cpus",
- "parking_lot 0.10.2",
+ "parking_lot 0.11.0",
  "threadpool",
 ]
 
@@ -302,7 +282,7 @@ dependencies = [
  "actix-codec",
  "actix-rt",
  "actix-service",
- "bitflags",
+ "bitflags 1.2.1",
  "bytes",
  "either",
  "futures",
@@ -313,9 +293,9 @@ dependencies = [
 
 [[package]]
 name = "actix-web"
-version = "3.0.0-alpha.3"
+version = "3.0.0-beta.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8bd6df56ec5f9a1a0d8335f156f36e1e8f76dbd736fa0cc0f6bc3a69be1e6124"
+checksum = "9125c29b7d9911bfdb4d0d4d8f1cf4fee4f21515cf2a405a423c30c245364297"
 dependencies = [
  "actix-codec",
  "actix-http",
@@ -353,24 +333,25 @@ dependencies = [
 
 [[package]]
 name = "actix-web-actors"
-version = "3.0.0-alpha.1"
+version = "3.0.0-beta.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2b5efeb3907582f9c724ce27be093ab8aafabd97be828bc6750c0d467f5e1aa3"
+checksum = "55ef22b33c49a28dda61866d5573c5b8ceb080a099cd59e7371b78b48bbf1bc0"
 dependencies = [
  "actix",
  "actix-codec",
  "actix-http",
  "actix-web",
  "bytes",
- "futures",
+ "futures-channel",
+ "futures-core",
  "pin-project",
 ]
 
 [[package]]
 name = "actix-web-codegen"
-version = "0.2.2"
+version = "0.3.0-beta.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a71bf475cbe07281d0b3696abb48212db118e7e23219f13596ce865235ff5766"
+checksum = "df9679f5b1f4c819de08b63b0a61a131b2fdc30b367c2c208984fda8eaa07fa0"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -390,24 +371,24 @@ dependencies = [
 
 [[package]]
 name = "addr2line"
-version = "0.12.2"
+version = "0.13.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "602d785912f476e480434627e8732e6766b760c045bbf897d9dfaa9f4fbd399c"
+checksum = "1b6a2d3371669ab3ca9797670853d61402b03d0b4b9ebf33d677dfa720203072"
 dependencies = [
  "gimli",
 ]
 
 [[package]]
 name = "adler"
-version = "0.2.2"
+version = "0.2.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ccc9a9dd069569f212bc4330af9f17c4afb5e8ce185e83dbb14f1349dda18b10"
+checksum = "ee2a4ec343196209d6594e19543ae87a39f96d5534d7174822a3ad825dd6ed7e"
 
 [[package]]
 name = "adler32"
-version = "1.1.0"
+version = "1.2.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "567b077b825e468cc974f0020d4082ee6e03132512f207ef1a02fd5d00d1f32d"
+checksum = "aae1277d39aeec15cb388266ecc24b11c80469deae6067e17a1a7aa9e5c1f234"
 
 [[package]]
 name = "aho-corasick"
@@ -427,6 +408,12 @@ dependencies = [
  "winapi 0.3.9",
 ]
 
+[[package]]
+name = "anyhow"
+version = "1.0.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6b602bfe940d21c130f3895acd65221e8a61270debe89d628b9cb4e3ccb8569b"
+
 [[package]]
 name = "arc-swap"
 version = "0.4.7"
@@ -481,9 +468,9 @@ checksum = "f8aac770f1885fd7e387acedd76065302551364496e46b3dd00860b2f8359b9d"
 
 [[package]]
 name = "awc"
-version = "2.0.0-alpha.2"
+version = "2.0.0-beta.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a7038a9747cd5159b9f0550895eaf865c0143baa7e4eee834e9294d0a7e0e4be"
+checksum = "eafb5c150b1dc89bf6aa5907ed6900534320c41920b5d6f13ff3ddb40f14dfac"
 dependencies = [
  "actix-codec",
  "actix-http",
@@ -505,14 +492,14 @@ dependencies = [
 
 [[package]]
 name = "backtrace"
-version = "0.3.49"
+version = "0.3.50"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "05100821de9e028f12ae3d189176b41ee198341eb8f369956407fea2f5cc666c"
+checksum = "46254cf2fdcdf1badb5934448c1bcbe046a56537b3987d96c51a7afc5d03f293"
 dependencies = [
  "addr2line",
  "cfg-if",
  "libc",
- "miniz_oxide 0.3.7",
+ "miniz_oxide",
  "object",
  "rustc-demangle",
 ]
@@ -523,6 +510,15 @@ version = "0.2.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "1b20b618342cf9891c292c4f5ac2cde7287cc5c87e87e9c769d617793607dec1"
 
+[[package]]
+name = "base64"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "30e93c03064e7590d0466209155251b90c22e37fab1daf2771582598b5827557"
+dependencies = [
+ "byteorder",
+]
+
 [[package]]
 name = "base64"
 version = "0.9.3"
@@ -566,6 +562,12 @@ dependencies = [
  "getrandom",
 ]
 
+[[package]]
+name = "bitflags"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "aad18937a628ec6abcd26d1489012cc0e18c21798210f491af69ded9b881106d"
+
 [[package]]
 name = "bitflags"
 version = "1.2.1"
@@ -590,7 +592,7 @@ version = "0.9.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4"
 dependencies = [
- "generic-array 0.14.2",
+ "generic-array 0.14.3",
 ]
 
 [[package]]
@@ -599,7 +601,7 @@ version = "0.7.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "fa136449e765dc7faa244561ccae839c394048667929af599b5d931ebe7b7f10"
 dependencies = [
- "generic-array 0.14.2",
+ "generic-array 0.14.3",
 ]
 
 [[package]]
@@ -642,6 +644,15 @@ dependencies = [
  "libc",
 ]
 
+[[package]]
+name = "buf-min"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6ae7069aad07c7cdefe6a22a671f00650728bd2331a4cc62e1e5d0becdf9ca4"
+dependencies = [
+ "bytes",
+]
+
 [[package]]
 name = "bufstream"
 version = "0.1.4"
@@ -660,6 +671,12 @@ version = "0.3.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "e3b5ca7a04898ad4bcd41c90c5285445ff5b791899bb1b0abdd2a2aa791211d7"
 
+[[package]]
+name = "bytemuck"
+version = "1.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "db7a1029718df60331e557c9e83a55523c955e5dd2a7bfeffad6bbd50b538ae9"
+
 [[package]]
 name = "byteorder"
 version = "1.3.4"
@@ -668,12 +685,9 @@ checksum = "08c48aae112d48ed9f069b33538ea9e3e90aa263cfa3d1c24309612b1f7472de"
 
 [[package]]
 name = "bytes"
-version = "0.5.5"
+version = "0.5.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "118cf036fbb97d0816e3c34b2d7a1e8cfc60f68fcf63d550ddbe9bd5f59c213b"
-dependencies = [
- "loom",
-]
+checksum = "0e4cec68f03f32e44924783795810fa50a7035d8c8ebe78580ad7e6c703fba38"
 
 [[package]]
 name = "bytestring"
@@ -684,11 +698,31 @@ dependencies = [
  "bytes",
 ]
 
+[[package]]
+name = "c_vec"
+version = "1.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f8a318911dce53b5f1ca6539c44f5342c632269f0fa7ea3e35f32458c27a7c30"
+
+[[package]]
+name = "captcha"
+version = "0.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4d060a3be43adb2fe89d3448e9a193149806139b1ce99281865fcab7aeaf04ed"
+dependencies = [
+ "base64 0.5.2",
+ "image",
+ "lodepng",
+ "rand 0.3.23",
+ "serde_json",
+ "time 0.1.43",
+]
+
 [[package]]
 name = "cc"
-version = "1.0.57"
+version = "1.0.58"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0fde55d2a2bfaa4c9668bbc63f531fbdeee3ffe188f4662511ce2c22b3eedebe"
+checksum = "f9a06fb2e53271d7c279ec1efea6ab691c35a2ae67ec0d91d7acec0caf13b518"
 
 [[package]]
 name = "cfg-if"
@@ -698,9 +732,9 @@ checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822"
 
 [[package]]
 name = "chrono"
-version = "0.4.12"
+version = "0.4.13"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f0fee792e164f78f5fe0c296cc2eb3688a2ca2b70cdff33040922d298203f0c4"
+checksum = "c74d84029116787153e02106bf53e66828452a4b325cc8652b788b5967c0a0b6"
 dependencies = [
  "num-integer",
  "num-traits 0.2.12",
@@ -716,7 +750,7 @@ checksum = "bdfa80d47f954d53a35a64987ca1422f495b8d6483c0fe9f7117b36c2a792129"
 dependencies = [
  "ansi_term",
  "atty",
- "bitflags",
+ "bitflags 1.2.1",
  "strsim 0.8.0",
  "textwrap",
  "unicode-width",
@@ -729,7 +763,7 @@ version = "0.0.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "ddfc5b9aa5d4507acaf872de71051dfd0e309860e88966e1051e462a077aac4f"
 dependencies = [
- "bitflags",
+ "bitflags 1.2.1",
 ]
 
 [[package]]
@@ -738,9 +772,15 @@ version = "0.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "4344512281c643ae7638bbabc3af17a11307803ec8f0fcad9fae512a8bf36467"
 dependencies = [
- "bitflags",
+ "bitflags 1.2.1",
 ]
 
+[[package]]
+name = "color_quant"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0dbbb57365263e881e805dc77d94697c9118fd94d8da011240555aa7b23445bd"
+
 [[package]]
 name = "comrak"
 version = "0.7.0"
@@ -770,6 +810,17 @@ dependencies = [
  "serde-hjson",
 ]
 
+[[package]]
+name = "cookie"
+version = "0.14.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1373a16a4937bc34efec7b391f9c1500c30b8478a701a4f44c9165cc0475a6e0"
+dependencies = [
+ "percent-encoding",
+ "time 0.2.16",
+ "version_check 0.9.2",
+]
+
 [[package]]
 name = "copyless"
 version = "0.1.5"
@@ -794,9 +845,9 @@ checksum = "b3a71ab494c0b5b860bdc8407ae08978052417070c2ced38573a9157ad75b8ac"
 
 [[package]]
 name = "cpuid-bool"
-version = "0.1.0"
+version = "0.1.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6d375c433320f6c5057ae04a04376eef4d04ce2801448cf8863a78da99107be4"
+checksum = "8aebca1129a03dc6dc2b127edd729435bbc4a37e1d5f4d7513165089ceb02634"
 
 [[package]]
 name = "crc32fast"
@@ -809,10 +860,47 @@ dependencies = [
 
 [[package]]
 name = "crossbeam-channel"
-version = "0.4.2"
+version = "0.4.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cced8691919c02aac3cb0a1bc2e9b73d89e832bf9a06fc579d4e71b68a2da061"
+checksum = "09ee0cc8804d5393478d743b035099520087a5186f3b93fa58cec08fa62407b6"
 dependencies = [
+ "cfg-if",
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "crossbeam-deque"
+version = "0.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9f02af974daeee82218205558e51ec8768b48cf524bd01d550abe5573a608285"
+dependencies = [
+ "crossbeam-epoch",
+ "crossbeam-utils",
+ "maybe-uninit",
+]
+
+[[package]]
+name = "crossbeam-epoch"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "058ed274caafc1f60c4997b5fc07bf7dc7cca454af7c6e81edffe5f33f70dace"
+dependencies = [
+ "autocfg 1.0.0",
+ "cfg-if",
+ "crossbeam-utils",
+ "lazy_static",
+ "maybe-uninit",
+ "memoffset",
+ "scopeguard",
+]
+
+[[package]]
+name = "crossbeam-queue"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "774ba60a54c213d409d5353bda12d49cd68d14e45036a285234c8d6f91f92570"
+dependencies = [
+ "cfg-if",
  "crossbeam-utils",
  "maybe-uninit",
 ]
@@ -863,6 +951,16 @@ dependencies = [
  "syn",
 ]
 
+[[package]]
+name = "deflate"
+version = "0.7.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "707b6a7b384888a70c8d2e8650b3e60170dfc6a67bb4aa67b6dfca57af4bedb4"
+dependencies = [
+ "adler32",
+ "byteorder",
+]
+
 [[package]]
 name = "derive_builder"
 version = "0.9.0"
@@ -905,7 +1003,7 @@ version = "1.4.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "3e2de9deab977a153492a1468d1b1c0662c1cf39e5ea87d0c060ecd59ef18d8c"
 dependencies = [
- "bitflags",
+ "bitflags 1.2.1",
  "byteorder",
  "chrono",
  "diesel_derives",
@@ -950,7 +1048,7 @@ version = "0.9.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066"
 dependencies = [
- "generic-array 0.14.2",
+ "generic-array 0.14.3",
 ]
 
 [[package]]
@@ -1083,6 +1181,15 @@ dependencies = [
  "syn",
 ]
 
+[[package]]
+name = "enum_primitive"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "be4551092f4d519593039259a9ed8daedf0da12e5109c5280338073eaeb81180"
+dependencies = [
+ "num-traits 0.1.43",
+]
+
 [[package]]
 name = "env_logger"
 version = "0.7.1"
@@ -1096,28 +1203,6 @@ dependencies = [
  "termcolor",
 ]
 
-[[package]]
-name = "failure"
-version = "0.1.8"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d32e9bd16cc02eae7db7ef620b392808b89f6a5e16bb3497d159c6b92a0f4f86"
-dependencies = [
- "backtrace",
- "failure_derive",
-]
-
-[[package]]
-name = "failure_derive"
-version = "0.1.8"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "aa4da3c766cd7a0db8242e326e9e4e081edd567072893ed320008189715366a4"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn",
- "synstructure",
-]
-
 [[package]]
 name = "fake-simd"
 version = "0.1.2"
@@ -1142,7 +1227,7 @@ dependencies = [
  "cfg-if",
  "crc32fast",
  "libc",
- "miniz_oxide 0.4.0",
+ "miniz_oxide",
 ]
 
 [[package]]
@@ -1178,7 +1263,7 @@ version = "0.3.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "2e9763c69ebaae630ba35f74888db465e49e259ba1bc0eda7d06f4a067615d82"
 dependencies = [
- "bitflags",
+ "bitflags 1.2.1",
  "fuchsia-zircon-sys",
 ]
 
@@ -1292,19 +1377,6 @@ dependencies = [
  "byteorder",
 ]
 
-[[package]]
-name = "generator"
-version = "0.6.21"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "add72f17bb81521258fcc8a7a3245b1e184e916bfbe34f0ea89558f440df5c68"
-dependencies = [
- "cc",
- "libc",
- "log",
- "rustc_version",
- "winapi 0.3.9",
-]
-
 [[package]]
 name = "generic-array"
 version = "0.12.3"
@@ -1316,9 +1388,9 @@ dependencies = [
 
 [[package]]
 name = "generic-array"
-version = "0.14.2"
+version = "0.14.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ac746a5f3bbfdadd6106868134545e684693d54d9d44f6e9588a7d54af0bf980"
+checksum = "60fb4bb6bba52f78a471264d9a3b7d026cc0af47b22cd2cffbc0b787ca003e63"
 dependencies = [
  "typenum",
  "version_check 0.9.2",
@@ -1335,17 +1407,27 @@ dependencies = [
  "wasi",
 ]
 
+[[package]]
+name = "gif"
+version = "0.9.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e2e41945ba23db3bf51b24756d73d81acb4f28d85c3dccc32c6fae904438c25f"
+dependencies = [
+ "color_quant",
+ "lzw",
+]
+
 [[package]]
 name = "gimli"
-version = "0.21.0"
+version = "0.22.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bcc8e0c9bce37868955864dbecd2b1ab2bdf967e6f28066d65aaac620444b65c"
+checksum = "aaf91faf136cb47367fa430cd46e37a788775e7fa104f8b4bcb3861dc389b724"
 
 [[package]]
 name = "h2"
-version = "0.2.5"
+version = "0.2.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "79b7246d7e4b979c03fa093da39cfb3617a96bbeee6310af63991668d7e843ff"
+checksum = "993f9e0baeed60001cf565546b0d3dbe6a6ad23f2bd31644a133c641eccf6d53"
 dependencies = [
  "bytes",
  "fnv",
@@ -1354,10 +1436,19 @@ dependencies = [
  "futures-util",
  "http",
  "indexmap",
- "log",
  "slab",
  "tokio",
  "tokio-util 0.3.1",
+ "tracing",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "34f595585f103464d8d2f6e9864682d74c1601fed5e07d62b1c9058dba8246fb"
+dependencies = [
+ "autocfg 1.0.0",
 ]
 
 [[package]]
@@ -1371,9 +1462,9 @@ dependencies = [
 
 [[package]]
 name = "hermit-abi"
-version = "0.1.14"
+version = "0.1.15"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b9586eedd4ce6b3c498bc3b4dd92fc9f11166aa908a914071953768066c67909"
+checksum = "3deed196b6e7f9e44a2ae8d94225d80302d81208b1bb673fd21fe634645c85a9"
 dependencies = [
  "libc",
 ]
@@ -1412,9 +1503,9 @@ dependencies = [
 
 [[package]]
 name = "http-signature-normalization"
-version = "0.5.1"
+version = "0.5.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "648233553603e7bb55bc1ea08a514661e212c09c10f6434507894273d8b5e773"
+checksum = "ee917294413cec0db93a8af6ecfa63730c1d2bb604bd1da69ba75b342fb23f21"
 dependencies = [
  "chrono",
  "thiserror",
@@ -1470,20 +1561,44 @@ dependencies = [
  "unicode-normalization",
 ]
 
+[[package]]
+name = "image"
+version = "0.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1c3f4f5ea213ed9899eca760a8a14091d4b82d33e27cf8ced336ff730e9f6da8"
+dependencies = [
+ "byteorder",
+ "enum_primitive",
+ "gif",
+ "jpeg-decoder",
+ "num-iter",
+ "num-rational",
+ "num-traits 0.1.43",
+ "png",
+ "scoped_threadpool",
+]
+
 [[package]]
 name = "indexmap"
-version = "1.4.0"
+version = "1.5.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c398b2b113b55809ceb9ee3e753fcbac793f1956663f3c36549c1346015c2afe"
+checksum = "5b88cd59ee5f71fea89a62248fc8f387d44400cefe05ef548466d61ced9029a7"
 dependencies = [
  "autocfg 1.0.0",
+ "hashbrown",
 ]
 
+[[package]]
+name = "inflate"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d1238524675af3938a7c74980899535854b88ba07907bb1c944abe5b8fc437e5"
+
 [[package]]
 name = "instant"
-version = "0.1.5"
+version = "0.1.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "69da7ce1490173c2bf4d26bc8be429aaeeaf4cce6c4b970b7949651fa17655fe"
+checksum = "5b141fdc7836c525d4d594027d318c84161ca17aaf8113ab1f81ab93ae897485"
 
 [[package]]
 name = "iovec"
@@ -1521,11 +1636,21 @@ version = "0.4.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "dc6f3ad7b9d11a0c00842ff8de1b60ee58661048eb8049ed33c73594f359d7e6"
 
+[[package]]
+name = "jpeg-decoder"
+version = "0.1.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cc797adac5f083b8ff0ca6f6294a999393d76e197c36488e2ef732c4715f6fa3"
+dependencies = [
+ "byteorder",
+ "rayon",
+]
+
 [[package]]
 name = "js-sys"
-version = "0.3.41"
+version = "0.3.44"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c4b9172132a62451e56142bff9afc91c8e4a4500aa5b847da36815b63bfda916"
+checksum = "85a7e2c92a4804dd459b86c339278d0fe87cf93757fae222c3fa3ae75458bc73"
 dependencies = [
  "wasm-bindgen",
 ]
@@ -1573,12 +1698,15 @@ dependencies = [
  "bcrypt",
  "chrono",
  "diesel",
+ "lazy_static",
  "log",
+ "regex",
  "serde 1.0.114",
  "serde_json",
  "sha2",
  "strum",
  "strum_macros",
+ "url",
 ]
 
 [[package]]
@@ -1587,22 +1715,22 @@ version = "0.0.1"
 dependencies = [
  "activitystreams",
  "activitystreams-ext",
- "activitystreams-new",
  "actix",
  "actix-files",
  "actix-rt",
  "actix-web",
  "actix-web-actors",
+ "anyhow",
  "async-trait",
  "awc",
  "base64 0.12.3",
  "bcrypt",
+ "captcha",
  "chrono",
  "diesel",
  "diesel_migrations",
  "dotenv",
  "env_logger",
- "failure",
  "futures",
  "http",
  "http-signature-normalization-actix",
@@ -1621,6 +1749,7 @@ dependencies = [
  "sha2",
  "strum",
  "strum_macros",
+ "thiserror",
  "tokio",
  "url",
  "uuid 0.8.1",
@@ -1685,7 +1814,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "db65c6da02e61f55dae90a0ae427b2a5f6b3e8db09f58d10efab23af92592616"
 dependencies = [
  "arrayvec",
- "bitflags",
+ "bitflags 1.2.1",
  "cfg-if",
  "ryu",
  "static_assertions",
@@ -1693,9 +1822,9 @@ dependencies = [
 
 [[package]]
 name = "libc"
-version = "0.2.71"
+version = "0.2.74"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9457b06509d27052635f90d6466700c65095fdf75409b3fbdd903e988b886f49"
+checksum = "a2f02823cf78b754822df5f7f268fb59822e7296276d3e069d8e8cb26a14bd10"
 
 [[package]]
 name = "linked-hash-map"
@@ -1724,31 +1853,32 @@ dependencies = [
 
 [[package]]
 name = "lock_api"
-version = "0.4.0"
+version = "0.4.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "de302ce1fe7482db13738fbaf2e21cfb06a986b89c0bf38d88abf16681aada4e"
+checksum = "28247cc5a5be2f05fbcd76dd0cf2c7d3b5400cb978a28042abcd4fa0b3f8261c"
 dependencies = [
  "scopeguard",
 ]
 
 [[package]]
-name = "log"
-version = "0.4.8"
+name = "lodepng"
+version = "1.2.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "14b6052be84e6b71ab17edffc2eeabf5c2c3ae1fdb464aae35ac50c67a44e1f7"
+checksum = "8ac1dfdf85b7d5dea61a620e12c051a72078189366a0b3c0ab331e30847def2f"
 dependencies = [
- "cfg-if",
+ "c_vec",
+ "cc",
+ "libc",
+ "rgb",
 ]
 
 [[package]]
-name = "loom"
-version = "0.3.4"
+name = "log"
+version = "0.4.11"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4ecc775857611e1df29abba5c41355cdf540e7e9d4acfdf0f355eefee82330b7"
+checksum = "4fabed175da42fed1fa0746b0ea71f412aa9d35e76e95e59b192c64b9dc2bf8b"
 dependencies = [
  "cfg-if",
- "generator",
- "scoped-tls",
 ]
 
 [[package]]
@@ -1760,6 +1890,12 @@ dependencies = [
  "linked-hash-map 0.5.3",
 ]
 
+[[package]]
+name = "lzw"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7d947cbb889ed21c2a84be6ffbaebf5b4e0f4340638cba0444907e38b56be084"
+
 [[package]]
 name = "maplit"
 version = "1.0.2"
@@ -1790,6 +1926,15 @@ version = "2.3.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "3728d817d99e5ac407411fa471ff9800a778d88a24685968b36824eaf4bee400"
 
+[[package]]
+name = "memoffset"
+version = "0.5.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c198b026e1bbf08a937e94c6c60f9ec4a2267f5b0d2eec9c1b21b061ce2be55f"
+dependencies = [
+ "autocfg 1.0.0",
+]
+
 [[package]]
 name = "migrations_internals"
 version = "1.4.1"
@@ -1827,15 +1972,6 @@ dependencies = [
  "unicase",
 ]
 
-[[package]]
-name = "miniz_oxide"
-version = "0.3.7"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "791daaae1ed6889560f8c4359194f56648355540573244a5448a83ba1ecc7435"
-dependencies = [
- "adler32",
-]
-
 [[package]]
 name = "miniz_oxide"
 version = "0.4.0"
@@ -1958,6 +2094,27 @@ dependencies = [
  "num-traits 0.2.12",
 ]
 
+[[package]]
+name = "num-iter"
+version = "0.1.41"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7a6e6b7c748f995c4c29c5f5ae0248536e04a5739927c74ec0fa564805094b9f"
+dependencies = [
+ "autocfg 1.0.0",
+ "num-integer",
+ "num-traits 0.2.12",
+]
+
+[[package]]
+name = "num-rational"
+version = "0.1.42"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ee314c74bd753fc86b4780aa9475da469155f3848473a261d2d18e35245a784e"
+dependencies = [
+ "num-integer",
+ "num-traits 0.2.12",
+]
+
 [[package]]
 name = "num-traits"
 version = "0.1.43"
@@ -2016,7 +2173,7 @@ version = "0.10.30"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "8d575eff3665419f9b83678ff2815858ad9d11567e082f5ac1814baba4e2bcb4"
 dependencies = [
- "bitflags",
+ "bitflags 1.2.1",
  "cfg-if",
  "foreign-types",
  "lazy_static",
@@ -2060,7 +2217,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "a4893845fa2ca272e647da5d0e46660a314ead9c2fdd9a883aabc32e481a8733"
 dependencies = [
  "instant",
- "lock_api 0.4.0",
+ "lock_api 0.4.1",
  "parking_lot_core 0.8.0",
 ]
 
@@ -2150,23 +2307,23 @@ checksum = "54be6e404f5317079812fc8f9f5279de376d8856929e21c184ecf6bbd692a11d"
 dependencies = [
  "maplit",
  "pest",
- "sha-1",
+ "sha-1 0.8.2",
 ]
 
 [[package]]
 name = "pin-project"
-version = "0.4.22"
+version = "0.4.23"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "12e3a6cdbfe94a5e4572812a0201f8c0ed98c1c452c7b8563ce2276988ef9c17"
+checksum = "ca4433fff2ae79342e497d9f8ee990d174071408f28f726d6d83af93e58e48aa"
 dependencies = [
  "pin-project-internal",
 ]
 
 [[package]]
 name = "pin-project-internal"
-version = "0.4.22"
+version = "0.4.23"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6a0ffd45cf79d88737d7cc85bfd5d2894bee1139b356e616fe85dc389c61aaf7"
+checksum = "2c0e815c3ee9a031fdf5af21c10aa17c573c9c6a566328d99e3936c34e36461f"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -2187,9 +2344,21 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
 
 [[package]]
 name = "pkg-config"
-version = "0.3.17"
+version = "0.3.18"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "05da548ad6865900e60eaba7f589cc0783590a92e940c26953ff81ddbab2d677"
+checksum = "d36492546b6af1463394d46f0c834346f31548646f6ba10849802c9c9a27ac33"
+
+[[package]]
+name = "png"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "48f397b84083c2753ba53c7b56ad023edb94512b2885ffe227c66ff7edb61868"
+dependencies = [
+ "bitflags 0.7.0",
+ "deflate",
+ "inflate",
+ "num-iter",
+]
 
 [[package]]
 name = "ppv-lite86"
@@ -2208,9 +2377,9 @@ dependencies = [
 
 [[package]]
 name = "proc-macro-hack"
-version = "0.5.16"
+version = "0.5.18"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7e0456befd48169b9f13ef0f0ad46d492cf9d2dbb918bcf38e01eed4ce3ec5e4"
+checksum = "99c605b9a0adc77b7211c6b1f722dcb613d68d66859a44f3d485a6da332b0598"
 
 [[package]]
 name = "proc-macro-nested"
@@ -2220,9 +2389,9 @@ checksum = "eba180dafb9038b050a4c280019bbedf9f2467b61e5d892dcad585bb57aadc5a"
 
 [[package]]
 name = "proc-macro2"
-version = "1.0.18"
+version = "1.0.19"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "beae6331a816b1f65d04c45b078fd8e6c93e8071771f41b8163255bbd8d7c8fa"
+checksum = "04f5f085b5d71e2188cb8271e5da0161ad52c3f227a661a3c135fdf28e258b12"
 dependencies = [
  "unicode-xid",
 ]
@@ -2263,6 +2432,16 @@ dependencies = [
  "scheduled-thread-pool",
 ]
 
+[[package]]
+name = "rand"
+version = "0.3.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "64ac302d8f83c0c1974bf758f6b041c6c8ada916fbb44a609158ca8b064cc76c"
+dependencies = [
+ "libc",
+ "rand 0.4.6",
+]
+
 [[package]]
 name = "rand"
 version = "0.4.6"
@@ -2423,6 +2602,31 @@ dependencies = [
  "rand_core 0.3.1",
 ]
 
+[[package]]
+name = "rayon"
+version = "1.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "62f02856753d04e03e26929f820d0a0a337ebe71f849801eea335d464b349080"
+dependencies = [
+ "autocfg 1.0.0",
+ "crossbeam-deque",
+ "either",
+ "rayon-core",
+]
+
+[[package]]
+name = "rayon-core"
+version = "1.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e92e15d89083484e11353891f1af602cc661426deb9564c298b270c726973280"
+dependencies = [
+ "crossbeam-deque",
+ "crossbeam-queue",
+ "crossbeam-utils",
+ "lazy_static",
+ "num_cpus",
+]
+
 [[package]]
 name = "rdrand"
 version = "0.4.0"
@@ -2434,9 +2638,9 @@ dependencies = [
 
 [[package]]
 name = "redox_syscall"
-version = "0.1.56"
+version = "0.1.57"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2439c63f3f6139d1b57529d16bc3b8bb855230c8efcc5d3a896c8bea7c3b1e84"
+checksum = "41cc0f7e4d5d4544e8861606a285bb08d3e70712ccc7d2b84d7c0ccfaf4b05ce"
 
 [[package]]
 name = "regex"
@@ -2475,6 +2679,15 @@ dependencies = [
  "quick-error",
 ]
 
+[[package]]
+name = "rgb"
+version = "0.8.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "90ef54b45ae131327a88597e2463fee4098ad6c88ba7b6af4b3987db8aad4098"
+dependencies = [
+ "bytemuck",
+]
+
 [[package]]
 name = "ring"
 version = "0.16.15"
@@ -2560,10 +2773,10 @@ dependencies = [
 ]
 
 [[package]]
-name = "scoped-tls"
-version = "0.1.2"
+name = "scoped_threadpool"
+version = "0.1.9"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "332ffa32bf586782a3efaeb58f127980944bbc8c4d6913a86107ac2a5ab24b28"
+checksum = "1d51f5df5af43ab3f1360b429fa5e0152ac5ce8c0bd6485cae490332e96846a8"
 
 [[package]]
 name = "scopeguard"
@@ -2587,7 +2800,7 @@ version = "0.4.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "64808902d7d99f78eaddd2b4e2509713babc3dc3c85ad6f4c447680f3c01e535"
 dependencies = [
- "bitflags",
+ "bitflags 1.2.1",
  "core-foundation",
  "core-foundation-sys",
  "libc",
@@ -2660,9 +2873,9 @@ dependencies = [
 
 [[package]]
 name = "serde_json"
-version = "1.0.56"
+version = "1.0.57"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3433e879a558dde8b5e8feb2a04899cf34fdde1fafb894687e52105fc1162ac3"
+checksum = "164eacbdb13512ec2745fb09d51fd5b22b0d65ed294a1dcf7285a360c80a675c"
 dependencies = [
  "indexmap",
  "itoa",
@@ -2703,6 +2916,19 @@ dependencies = [
  "opaque-debug 0.2.3",
 ]
 
+[[package]]
+name = "sha-1"
+version = "0.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "170a36ea86c864a3f16dd2687712dd6646f7019f301e57537c7f4dc9f5916770"
+dependencies = [
+ "block-buffer 0.9.0",
+ "cfg-if",
+ "cpuid-bool",
+ "digest 0.9.0",
+ "opaque-debug 0.3.0",
+]
+
 [[package]]
 name = "sha1"
 version = "0.6.0"
@@ -2734,9 +2960,9 @@ dependencies = [
 
 [[package]]
 name = "simple_asn1"
-version = "0.4.0"
+version = "0.4.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2b25ecba7165254f0c97d6c22a64b1122a03634b18d20a34daf21e18f892e618"
+checksum = "692ca13de57ce0613a363c8c2f1de925adebc81b04c923ac60c5488bb44abe4b"
 dependencies = [
  "chrono",
  "num-bigint",
@@ -2751,9 +2977,9 @@ checksum = "c111b5bd5695e56cffe5129854aa230b39c93a305372fdbb2668ca2394eea9f8"
 
 [[package]]
 name = "smallvec"
-version = "1.4.0"
+version = "1.4.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c7cb5678e1615754284ec264d9bb5b4c27d2018577fd90ac0ceb578591ed5ee4"
+checksum = "3757cb9d89161a2f24e1cf78efa0c1fcff485d18e3f55e0aa3480824ddaa0f3f"
 
 [[package]]
 name = "socket2"
@@ -2869,24 +3095,12 @@ dependencies = [
 
 [[package]]
 name = "syn"
-version = "1.0.33"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e8d5d96e8cbb005d6959f119f773bfaebb5684296108fb32600c00cde305b2cd"
-dependencies = [
- "proc-macro2",
- "quote",
- "unicode-xid",
-]
-
-[[package]]
-name = "synstructure"
-version = "0.12.4"
+version = "1.0.36"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b834f2d66f734cb897113e34aaff2f1ab4719ca946f9a7358dba8f8064148701"
+checksum = "4cdb98bcb1f9d81d07b536179c269ea15999b5d14ea958196413869445bb5250"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn",
  "unicode-xid",
 ]
 
@@ -3016,9 +3230,9 @@ checksum = "53953d2d3a5ad81d9f844a32f14ebb121f50b650cd59d0ee2a07cf13c617efed"
 
 [[package]]
 name = "tokio"
-version = "0.2.21"
+version = "0.2.22"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d099fa27b9702bed751524694adbe393e18b36b204da91eb1cbbbbb4a5ee2d58"
+checksum = "5d34ca54d84bf2b5b4d7d31e901a8464f7b60ac145a284fba25ceb801f2ddccd"
 dependencies = [
  "bytes",
  "fnv",
@@ -3076,6 +3290,26 @@ dependencies = [
  "tokio",
 ]
 
+[[package]]
+name = "tracing"
+version = "0.1.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f0aae59226cf195d8e74d4b34beae1859257efb4e5fed3f147d2dc2c7d372178"
+dependencies = [
+ "cfg-if",
+ "log",
+ "tracing-core",
+]
+
+[[package]]
+name = "tracing-core"
+version = "0.1.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b2734b5a028fa697686f16c6d18c2c6a3c7e41513f9a213abb6754c4acb3c8d7"
+dependencies = [
+ "lazy_static",
+]
+
 [[package]]
 name = "trust-dns-proto"
 version = "0.19.5"
@@ -3132,17 +3366,6 @@ version = "1.7.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "a9b2228007eba4120145f785df0f6c92ea538f5a3635a612ecf4e334c8c1446d"
 
-[[package]]
-name = "typed-builder"
-version = "0.6.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "85fc4459191c621a53ef6c6ca5642e6e0e5ccc61f3e5b8ad6b6ab5317f0200fb"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn",
-]
-
 [[package]]
 name = "typenum"
 version = "1.12.0"
@@ -3251,18 +3474,19 @@ dependencies = [
 
 [[package]]
 name = "v_escape"
-version = "0.7.4"
+version = "0.12.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "660b101c07b5d0863deb9e7fb3138777e858d6d2a79f9e6049a27d1cc77c6da6"
+checksum = "7b2d5ca56f0412d5ad5e642202e5c8fb61b61ad39435a53ed501fbd45380e8d3"
 dependencies = [
+ "buf-min",
  "v_escape_derive",
 ]
 
 [[package]]
 name = "v_escape_derive"
-version = "0.5.6"
+version = "0.8.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c2ca2a14bc3fc5b64d188b087a7d3a927df87b152e941ccfbc66672e20c467ae"
+checksum = "cae7cffca0b1f9af9b20610f6fdeee9ffcce61417b5ad186a5d482dc904e24cd"
 dependencies = [
  "nom 4.2.3",
  "proc-macro2",
@@ -3272,9 +3496,9 @@ dependencies = [
 
 [[package]]
 name = "v_htmlescape"
-version = "0.4.5"
+version = "0.10.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e33e939c0d8cf047514fb6ba7d5aac78bc56677a6938b2ee67000b91f2e97e41"
+checksum = "f5fd25529cb2f78527b5ee507bcfb357b26d057b5e480853c26d49a4ead5c629"
 dependencies = [
  "cfg-if",
  "v_escape",
@@ -3312,9 +3536,9 @@ checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519"
 
 [[package]]
 name = "wasm-bindgen"
-version = "0.2.64"
+version = "0.2.67"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6a634620115e4a229108b71bde263bb4220c483b3f07f5ba514ee8d15064c4c2"
+checksum = "f0563a9a4b071746dd5aedbc3a28c6fe9be4586fb3fbadb67c400d4f53c6b16c"
 dependencies = [
  "cfg-if",
  "wasm-bindgen-macro",
@@ -3322,9 +3546,9 @@ dependencies = [
 
 [[package]]
 name = "wasm-bindgen-backend"
-version = "0.2.64"
+version = "0.2.67"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3e53963b583d18a5aa3aaae4b4c1cb535218246131ba22a71f05b518098571df"
+checksum = "bc71e4c5efa60fb9e74160e89b93353bc24059999c0ae0fb03affc39770310b0"
 dependencies = [
  "bumpalo",
  "lazy_static",
@@ -3337,9 +3561,9 @@ dependencies = [
 
 [[package]]
 name = "wasm-bindgen-macro"
-version = "0.2.64"
+version = "0.2.67"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3fcfd5ef6eec85623b4c6e844293d4516470d8f19cd72d0d12246017eb9060b8"
+checksum = "97c57cefa5fa80e2ba15641578b44d36e7a64279bc5ed43c6dbaf329457a2ed2"
 dependencies = [
  "quote",
  "wasm-bindgen-macro-support",
@@ -3347,9 +3571,9 @@ dependencies = [
 
 [[package]]
 name = "wasm-bindgen-macro-support"
-version = "0.2.64"
+version = "0.2.67"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9adff9ee0e94b926ca81b57f57f86d5545cdcb1d259e21ec9bdd95b901754c75"
+checksum = "841a6d1c35c6f596ccea1f82504a192a60378f64b3bb0261904ad8f2f5657556"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -3360,15 +3584,15 @@ dependencies = [
 
 [[package]]
 name = "wasm-bindgen-shared"
-version = "0.2.64"
+version = "0.2.67"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7f7b90ea6c632dd06fd765d44542e234d5e63d9bb917ecd64d79778a13bd79ae"
+checksum = "93b162580e34310e5931c4b792560108b10fd14d64915d7fff8ff00180e70092"
 
 [[package]]
 name = "web-sys"
-version = "0.3.41"
+version = "0.3.44"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "863539788676619aac1a23e2df3655e96b32b0e05eb72ca34ba045ad573c625d"
+checksum = "dda38f4e5ca63eda02c059d243aa25b5f35ab98451e518c51612cd0f1bd19a47"
 dependencies = [
  "js-sys",
  "wasm-bindgen",
index 2aa3c139b7c8b1511527c69efd959925de0b3865..dba0fee6fe0e0c6d0bef5e00f970c0858e5e2eb6 100644 (file)
@@ -18,13 +18,11 @@ lemmy_db = { path = "./lemmy_db" }
 diesel = "1.4.4"
 diesel_migrations = "1.4.0"
 dotenv = "0.15.0"
-activitystreams = "0.6.2"
-activitystreams-new = { git = "https://git.asonix.dog/asonix/activitystreams-sketch" }
-activitystreams-ext = { git = "https://git.asonix.dog/asonix/activitystreams-ext" }
+activitystreams = "0.7.0-alpha.3"
+activitystreams-ext = "0.1.0-alpha.2"
 bcrypt = "0.8.0"
 chrono = { version = "0.4.7", features = ["serde"] }
 serde_json = { version = "1.0.52", features = ["preserve_order"]}
-failure = "0.1.8"
 serde = { version = "1.0.105", features = ["derive"] }
 actix = "0.10.0-alpha.2"
 actix-web = { version = "3.0.0-alpha.3", features = ["rustls"] }
@@ -52,3 +50,6 @@ itertools = "0.9.0"
 uuid = { version = "0.8", features = ["serde", "v4"] }
 sha2 = "0.9"
 async-trait = "0.1.36"
+captcha = "0.0.7"
+anyhow = "1.0.32"
+thiserror = "1.0.20"
index 348368f19681be37c8948763b4a30b99924c5af2..591f68f6d11b0f86bcd77a2977d789b9a59c36bf 100644 (file)
@@ -35,6 +35,8 @@
   jwt_secret: "changeme"
   # The location of the frontend
   front_end_dir: "../ui/dist"
+  # address where pictrs is available
+  pictrs_url: "http://pictrs:8080"
   # rate limits for various user actions, by user ip
   rate_limit: {
     # maximum number of messages created in interval
     register: 3
     # interval length for registration limit
     register_per_second: 3600
+    # maximum number of image uploads in interval
+    image: 6
+    # interval length for image uploads
+    image_per_second: 3600
   }
   # settings related to activitypub federation
   federation: {
     enabled: false
     # whether tls is required for activitypub. only disable this for debugging, never for producion.
     tls_enabled: true
-    # comma seperated list of instances with which federation is allowed
+    # comma separated list of instances with which federation is allowed
     allowed_instances: ""
+    # comma separated list of instances which are blocked from federating
+    blocked_instances: ""
+  }
+  captcha: {
+    enabled: true
+    difficulty: medium # Can be easy, medium, or hard
   }
 #  # email sending configuration
 #  email: {
index d94cf5fc64ce954cf900d4235d4acd5cf8301194..6d342c1e76f492b0bc6de5b81d56c4e76ce67c0b 100644 (file)
@@ -12,4 +12,7 @@ strum = "0.18.0"
 strum_macros = "0.18.0"
 log = "0.4.0"
 sha2 = "0.9"
-bcrypt = "0.8.0"
\ No newline at end of file
+bcrypt = "0.8.0"
+url = { version = "2.1.1", features = ["serde"] }
+lazy_static = "1.3.0"
+regex = "1.3.5"
index 83f85ca1eea9ae94593e3a16534df2a9e7e4a796..177e6b7cd6221074bff6b228804d73424f8e6b75 100644 (file)
@@ -34,11 +34,6 @@ impl Crud<ActivityForm> for Activity {
     activity.find(activity_id).first::<Self>(conn)
   }
 
-  fn delete(conn: &PgConnection, activity_id: i32) -> Result<usize, Error> {
-    use crate::schema::activity::dsl::*;
-    diesel::delete(activity.find(activity_id)).execute(conn)
-  }
-
   fn create(conn: &PgConnection, new_activity: &ActivityForm) -> Result<Self, Error> {
     use crate::schema::activity::dsl::*;
     insert_into(activity)
@@ -107,6 +102,7 @@ mod tests {
       email: None,
       matrix_user_id: None,
       avatar: None,
+      banner: None,
       admin: false,
       banned: false,
       updated: None,
@@ -117,7 +113,7 @@ mod tests {
       lang: "browser".into(),
       show_avatars: true,
       send_notifications_to_email: false,
-      actor_id: "http://fake.com".into(),
+      actor_id: "changeme_862362".into(),
       bio: None,
       local: true,
       private_key: None,
@@ -155,11 +151,9 @@ mod tests {
     };
 
     let read_activity = Activity::read(&conn, inserted_activity.id).unwrap();
-    let num_deleted = Activity::delete(&conn, inserted_activity.id).unwrap();
     User_::delete(&conn, inserted_creator.id).unwrap();
 
     assert_eq!(expected_activity, read_activity);
     assert_eq!(expected_activity, inserted_activity);
-    assert_eq!(1, num_deleted);
   }
 }
index ec2efc7b7a17aaa9f71fd921d4e305553fad103f..ff4e757bf77531e04c7706b9e40ac9283f6447f3 100644 (file)
@@ -23,10 +23,6 @@ impl Crud<CategoryForm> for Category {
     category.find(category_id).first::<Self>(conn)
   }
 
-  fn delete(conn: &PgConnection, category_id: i32) -> Result<usize, Error> {
-    diesel::delete(category.find(category_id)).execute(conn)
-  }
-
   fn create(conn: &PgConnection, new_category: &CategoryForm) -> Result<Self, Error> {
     insert_into(category)
       .values(new_category)
index 602070d51e405a540ae72e8330bcde79d75e8e5a..b2e22aa625e54b8a55157c88774909dd224337f7 100644 (file)
@@ -1,5 +1,6 @@
 use super::{post::Post, *};
 use crate::schema::{comment, comment_like, comment_saved};
+use url::{ParseError, Url};
 
 // WITH RECURSIVE MyTree AS (
 //     SELECT * FROM comment WHERE parent_id IS NULL
@@ -42,6 +43,12 @@ pub struct CommentForm {
   pub local: bool,
 }
 
+impl CommentForm {
+  pub fn get_ap_id(&self) -> Result<Url, ParseError> {
+    Url::parse(&self.ap_id)
+  }
+}
+
 impl Crud<CommentForm> for Comment {
   fn read(conn: &PgConnection, comment_id: i32) -> Result<Self, Error> {
     use crate::schema::comment::dsl::*;
@@ -90,25 +97,79 @@ impl Comment {
     comment.filter(ap_id.eq(object_id)).first::<Self>(conn)
   }
 
-  pub fn mark_as_read(conn: &PgConnection, comment_id: i32) -> Result<Self, Error> {
+  pub fn permadelete_for_creator(
+    conn: &PgConnection,
+    for_creator_id: i32,
+  ) -> Result<Vec<Self>, Error> {
+    use crate::schema::comment::dsl::*;
+    diesel::update(comment.filter(creator_id.eq(for_creator_id)))
+      .set((
+        content.eq("*Permananently Deleted*"),
+        deleted.eq(true),
+        updated.eq(naive_now()),
+      ))
+      .get_results::<Self>(conn)
+  }
+
+  pub fn update_deleted(
+    conn: &PgConnection,
+    comment_id: i32,
+    new_deleted: bool,
+  ) -> Result<Self, Error> {
     use crate::schema::comment::dsl::*;
+    diesel::update(comment.find(comment_id))
+      .set((deleted.eq(new_deleted), updated.eq(naive_now())))
+      .get_result::<Self>(conn)
+  }
 
+  pub fn update_removed(
+    conn: &PgConnection,
+    comment_id: i32,
+    new_removed: bool,
+  ) -> Result<Self, Error> {
+    use crate::schema::comment::dsl::*;
     diesel::update(comment.find(comment_id))
-      .set(read.eq(true))
+      .set((removed.eq(new_removed), updated.eq(naive_now())))
       .get_result::<Self>(conn)
   }
 
-  pub fn permadelete(conn: &PgConnection, comment_id: i32) -> Result<Self, Error> {
+  pub fn update_removed_for_creator(
+    conn: &PgConnection,
+    for_creator_id: i32,
+    new_removed: bool,
+  ) -> Result<Vec<Self>, Error> {
+    use crate::schema::comment::dsl::*;
+    diesel::update(comment.filter(creator_id.eq(for_creator_id)))
+      .set((removed.eq(new_removed), updated.eq(naive_now())))
+      .get_results::<Self>(conn)
+  }
+
+  pub fn update_read(conn: &PgConnection, comment_id: i32, new_read: bool) -> Result<Self, Error> {
     use crate::schema::comment::dsl::*;
+    diesel::update(comment.find(comment_id))
+      .set(read.eq(new_read))
+      .get_result::<Self>(conn)
+  }
 
+  pub fn update_content(
+    conn: &PgConnection,
+    comment_id: i32,
+    new_content: &str,
+  ) -> Result<Self, Error> {
+    use crate::schema::comment::dsl::*;
     diesel::update(comment.find(comment_id))
-      .set((
-        content.eq("*Permananently Deleted*"),
-        deleted.eq(true),
-        updated.eq(naive_now()),
-      ))
+      .set((content.eq(new_content), updated.eq(naive_now())))
       .get_result::<Self>(conn)
   }
+
+  pub fn upsert(conn: &PgConnection, comment_form: &CommentForm) -> Result<Self, Error> {
+    let existing = Self::read_from_apub_id(conn, &comment_form.ap_id);
+    match existing {
+      Err(NotFound {}) => Ok(Self::create(conn, &comment_form)?),
+      Ok(p) => Ok(Self::update(conn, p.id, &comment_form)?),
+      Err(e) => Err(e),
+    }
+  }
 }
 
 #[derive(Identifiable, Queryable, Associations, PartialEq, Debug, Clone)]
@@ -133,39 +194,23 @@ pub struct CommentLikeForm {
 }
 
 impl Likeable<CommentLikeForm> for CommentLike {
-  fn read(conn: &PgConnection, comment_id_from: i32) -> Result<Vec<Self>, Error> {
-    use crate::schema::comment_like::dsl::*;
-    comment_like
-      .filter(comment_id.eq(comment_id_from))
-      .load::<Self>(conn)
-  }
-
   fn like(conn: &PgConnection, comment_like_form: &CommentLikeForm) -> Result<Self, Error> {
     use crate::schema::comment_like::dsl::*;
     insert_into(comment_like)
       .values(comment_like_form)
       .get_result::<Self>(conn)
   }
-  fn remove(conn: &PgConnection, comment_like_form: &CommentLikeForm) -> Result<usize, Error> {
-    use crate::schema::comment_like::dsl::*;
+  fn remove(conn: &PgConnection, user_id: i32, comment_id: i32) -> Result<usize, Error> {
+    use crate::schema::comment_like::dsl;
     diesel::delete(
-      comment_like
-        .filter(comment_id.eq(comment_like_form.comment_id))
-        .filter(user_id.eq(comment_like_form.user_id)),
+      dsl::comment_like
+        .filter(dsl::comment_id.eq(comment_id))
+        .filter(dsl::user_id.eq(user_id)),
     )
     .execute(conn)
   }
 }
 
-impl CommentLike {
-  pub fn from_post(conn: &PgConnection, post_id_from: i32) -> Result<Vec<Self>, Error> {
-    use crate::schema::comment_like::dsl::*;
-    comment_like
-      .filter(post_id.eq(post_id_from))
-      .load::<Self>(conn)
-  }
-}
-
 #[derive(Identifiable, Queryable, Associations, PartialEq, Debug)]
 #[belongs_to(Comment)]
 #[table_name = "comment_saved"]
@@ -216,6 +261,7 @@ mod tests {
       email: None,
       matrix_user_id: None,
       avatar: None,
+      banner: None,
       admin: false,
       banned: false,
       updated: None,
@@ -226,7 +272,7 @@ mod tests {
       lang: "browser".into(),
       show_avatars: true,
       send_notifications_to_email: false,
-      actor_id: "http://fake.com".into(),
+      actor_id: "changeme_283687".into(),
       bio: None,
       local: true,
       private_key: None,
@@ -246,12 +292,14 @@ mod tests {
       deleted: None,
       updated: None,
       nsfw: false,
-      actor_id: "http://fake.com".into(),
+      actor_id: "changeme_928738972".into(),
       local: true,
       private_key: None,
       public_key: None,
       last_refreshed_at: None,
       published: None,
+      banner: None,
+      icon: None,
     };
 
     let inserted_community = Community::create(&conn, &new_community).unwrap();
@@ -362,7 +410,7 @@ mod tests {
 
     let read_comment = Comment::read(&conn, inserted_comment.id).unwrap();
     let updated_comment = Comment::update(&conn, inserted_comment.id, &comment_form).unwrap();
-    let like_removed = CommentLike::remove(&conn, &comment_like_form).unwrap();
+    let like_removed = CommentLike::remove(&conn, inserted_user.id, inserted_comment.id).unwrap();
     let saved_removed = CommentSaved::unsave(&conn, &comment_saved_form).unwrap();
     let num_deleted = Comment::delete(&conn, inserted_comment.id).unwrap();
     Comment::delete(&conn, inserted_child_comment.id).unwrap();
index 4af13c2d9012abed1f12d878fd56052eb20a0a2d..5b14013770542fd1dbeb4d1f07ee4f1975b013e1 100644 (file)
@@ -23,17 +23,20 @@ table! {
     community_actor_id -> Text,
     community_local -> Bool,
     community_name -> Varchar,
+    community_icon -> Nullable<Text>,
     banned -> Bool,
     banned_from_community -> Bool,
     creator_actor_id -> Text,
     creator_local -> Bool,
     creator_name -> Varchar,
+    creator_preferred_username -> Nullable<Varchar>,
     creator_published -> Timestamp,
     creator_avatar -> Nullable<Text>,
     score -> BigInt,
     upvotes -> BigInt,
     downvotes -> BigInt,
     hot_rank -> Int4,
+    hot_rank_active -> Int4,
     user_id -> Nullable<Int4>,
     my_vote -> Nullable<Int4>,
     subscribed -> Nullable<Bool>,
@@ -60,17 +63,20 @@ table! {
     community_actor_id -> Text,
     community_local -> Bool,
     community_name -> Varchar,
+    community_icon -> Nullable<Text>,
     banned -> Bool,
     banned_from_community -> Bool,
     creator_actor_id -> Text,
     creator_local -> Bool,
     creator_name -> Varchar,
+    creator_preferred_username -> Nullable<Varchar>,
     creator_published -> Timestamp,
     creator_avatar -> Nullable<Text>,
     score -> BigInt,
     upvotes -> BigInt,
     downvotes -> BigInt,
     hot_rank -> Int4,
+    hot_rank_active -> Int4,
     user_id -> Nullable<Int4>,
     my_vote -> Nullable<Int4>,
     subscribed -> Nullable<Bool>,
@@ -100,17 +106,20 @@ pub struct CommentView {
   pub community_actor_id: String,
   pub community_local: bool,
   pub community_name: String,
+  pub community_icon: Option<String>,
   pub banned: bool,
   pub banned_from_community: bool,
   pub creator_actor_id: String,
   pub creator_local: bool,
   pub creator_name: String,
+  pub creator_preferred_username: Option<String>,
   pub creator_published: chrono::NaiveDateTime,
   pub creator_avatar: Option<String>,
   pub score: i64,
   pub upvotes: i64,
   pub downvotes: i64,
   pub hot_rank: i32,
+  pub hot_rank_active: i32,
   pub user_id: Option<i32>,
   pub my_vote: Option<i32>,
   pub subscribed: Option<bool>,
@@ -244,6 +253,9 @@ impl<'a> CommentQueryBuilder<'a> {
       SortType::Hot => query
         .order_by(hot_rank.desc())
         .then_order_by(published.desc()),
+      SortType::Active => query
+        .order_by(hot_rank_active.desc())
+        .then_order_by(published.desc()),
       SortType::New => query.order_by(published.desc()),
       SortType::TopAll => query.order_by(score.desc()),
       SortType::TopYear => query
@@ -315,17 +327,20 @@ table! {
     community_actor_id -> Text,
     community_local -> Bool,
     community_name -> Varchar,
+    community_icon -> Nullable<Varchar>,
     banned -> Bool,
     banned_from_community -> Bool,
     creator_actor_id -> Text,
     creator_local -> Bool,
     creator_name -> Varchar,
+    creator_preferred_username -> Nullable<Varchar>,
     creator_avatar -> Nullable<Text>,
     creator_published -> Timestamp,
     score -> BigInt,
     upvotes -> BigInt,
     downvotes -> BigInt,
     hot_rank -> Int4,
+    hot_rank_active -> Int4,
     user_id -> Nullable<Int4>,
     my_vote -> Nullable<Int4>,
     subscribed -> Nullable<Bool>,
@@ -356,17 +371,20 @@ pub struct ReplyView {
   pub community_actor_id: String,
   pub community_local: bool,
   pub community_name: String,
+  pub community_icon: Option<String>,
   pub banned: bool,
   pub banned_from_community: bool,
   pub creator_actor_id: String,
   pub creator_local: bool,
   pub creator_name: String,
+  pub creator_preferred_username: Option<String>,
   pub creator_avatar: Option<String>,
   pub creator_published: chrono::NaiveDateTime,
   pub score: i64,
   pub upvotes: i64,
   pub downvotes: i64,
   pub hot_rank: i32,
+  pub hot_rank_active: i32,
   pub user_id: Option<i32>,
   pub my_vote: Option<i32>,
   pub subscribed: Option<bool>,
@@ -437,7 +455,7 @@ impl<'a> ReplyQueryBuilder<'a> {
     }
 
     query = match self.sort {
-      // SortType::Hot => query.order_by(hot_rank.desc()),
+      // SortType::Hot => query.order_by(hot_rank.desc()), // TODO why is this commented
       SortType::New => query.order_by(published.desc()),
       SortType::TopAll => query.order_by(score.desc()),
       SortType::TopYear => query
@@ -488,6 +506,7 @@ mod tests {
       email: None,
       matrix_user_id: None,
       avatar: None,
+      banner: None,
       admin: false,
       banned: false,
       updated: None,
@@ -498,7 +517,7 @@ mod tests {
       lang: "browser".into(),
       show_avatars: true,
       send_notifications_to_email: false,
-      actor_id: "http://fake.com".into(),
+      actor_id: "changeme_92873982".into(),
       bio: None,
       local: true,
       private_key: None,
@@ -518,12 +537,14 @@ mod tests {
       deleted: None,
       updated: None,
       nsfw: false,
-      actor_id: "http://fake.com".into(),
+      actor_id: "changeme_7625376".into(),
       local: true,
       private_key: None,
       public_key: None,
       last_refreshed_at: None,
       published: None,
+      icon: None,
+      banner: None,
     };
 
     let inserted_community = Community::create(&conn, &new_community).unwrap();
@@ -584,6 +605,7 @@ mod tests {
       post_name: inserted_post.name.to_owned(),
       community_id: inserted_community.id,
       community_name: inserted_community.name.to_owned(),
+      community_icon: None,
       parent_id: None,
       removed: false,
       deleted: false,
@@ -593,11 +615,13 @@ mod tests {
       published: inserted_comment.published,
       updated: None,
       creator_name: inserted_user.name.to_owned(),
+      creator_preferred_username: None,
       creator_published: inserted_user.published,
       creator_avatar: None,
       score: 1,
       downvotes: 0,
       hot_rank: 0,
+      hot_rank_active: 0,
       upvotes: 1,
       user_id: None,
       my_vote: None,
@@ -619,6 +643,7 @@ mod tests {
       post_name: inserted_post.name.to_owned(),
       community_id: inserted_community.id,
       community_name: inserted_community.name.to_owned(),
+      community_icon: None,
       parent_id: None,
       removed: false,
       deleted: false,
@@ -628,11 +653,13 @@ mod tests {
       published: inserted_comment.published,
       updated: None,
       creator_name: inserted_user.name.to_owned(),
+      creator_preferred_username: None,
       creator_published: inserted_user.published,
       creator_avatar: None,
       score: 1,
       downvotes: 0,
       hot_rank: 0,
+      hot_rank_active: 0,
       upvotes: 1,
       user_id: Some(inserted_user.id),
       my_vote: Some(1),
@@ -651,6 +678,7 @@ mod tests {
       .list()
       .unwrap();
     read_comment_views_no_user[0].hot_rank = 0;
+    read_comment_views_no_user[0].hot_rank_active = 0;
 
     let mut read_comment_views_with_user = CommentQueryBuilder::create(&conn)
       .for_post_id(inserted_post.id)
@@ -658,8 +686,9 @@ mod tests {
       .list()
       .unwrap();
     read_comment_views_with_user[0].hot_rank = 0;
+    read_comment_views_with_user[0].hot_rank_active = 0;
 
-    let like_removed = CommentLike::remove(&conn, &comment_like_form).unwrap();
+    let like_removed = CommentLike::remove(&conn, inserted_user.id, inserted_comment.id).unwrap();
     let num_deleted = Comment::delete(&conn, inserted_comment.id).unwrap();
     Post::delete(&conn, inserted_post.id).unwrap();
     Community::delete(&conn, inserted_community.id).unwrap();
index 607520803b7aa2621a2f10d2e997b4ceb57f1716..df5f129412604ccd6ad94c3d42abd5696747a14e 100644 (file)
@@ -1,4 +1,5 @@
 use crate::{
+  naive_now,
   schema::{community, community_follower, community_moderator, community_user_ban},
   Bannable,
   Crud,
@@ -27,9 +28,10 @@ pub struct Community {
   pub private_key: Option<String>,
   pub public_key: Option<String>,
   pub last_refreshed_at: chrono::NaiveDateTime,
+  pub icon: Option<String>,
+  pub banner: Option<String>,
 }
 
-// TODO add better delete, remove, lock actions here.
 #[derive(Insertable, AsChangeset, Clone, Serialize, Deserialize, Debug)]
 #[table_name = "community"]
 pub struct CommunityForm {
@@ -48,6 +50,8 @@ pub struct CommunityForm {
   pub private_key: Option<String>,
   pub public_key: Option<String>,
   pub last_refreshed_at: Option<chrono::NaiveDateTime>,
+  pub icon: Option<Option<String>>,
+  pub banner: Option<Option<String>>,
 }
 
 impl Crud<CommunityForm> for Community {
@@ -88,16 +92,73 @@ impl Community {
       .first::<Self>(conn)
   }
 
-  pub fn read_from_actor_id(conn: &PgConnection, community_id: &str) -> Result<Self, Error> {
+  pub fn read_from_actor_id(conn: &PgConnection, for_actor_id: &str) -> Result<Self, Error> {
     use crate::schema::community::dsl::*;
     community
-      .filter(actor_id.eq(community_id))
+      .filter(actor_id.eq(for_actor_id))
       .first::<Self>(conn)
   }
 
-  pub fn list_local(conn: &PgConnection) -> Result<Vec<Self>, Error> {
+  pub fn update_deleted(
+    conn: &PgConnection,
+    community_id: i32,
+    new_deleted: bool,
+  ) -> Result<Self, Error> {
     use crate::schema::community::dsl::*;
-    community.filter(local.eq(true)).load::<Community>(conn)
+    diesel::update(community.find(community_id))
+      .set((deleted.eq(new_deleted), updated.eq(naive_now())))
+      .get_result::<Self>(conn)
+  }
+
+  pub fn update_removed(
+    conn: &PgConnection,
+    community_id: i32,
+    new_removed: bool,
+  ) -> Result<Self, Error> {
+    use crate::schema::community::dsl::*;
+    diesel::update(community.find(community_id))
+      .set((removed.eq(new_removed), updated.eq(naive_now())))
+      .get_result::<Self>(conn)
+  }
+
+  pub fn update_removed_for_creator(
+    conn: &PgConnection,
+    for_creator_id: i32,
+    new_removed: bool,
+  ) -> Result<Vec<Self>, Error> {
+    use crate::schema::community::dsl::*;
+    diesel::update(community.filter(creator_id.eq(for_creator_id)))
+      .set((removed.eq(new_removed), updated.eq(naive_now())))
+      .get_results::<Self>(conn)
+  }
+
+  pub fn update_creator(
+    conn: &PgConnection,
+    community_id: i32,
+    new_creator_id: i32,
+  ) -> Result<Self, Error> {
+    use crate::schema::community::dsl::*;
+    diesel::update(community.find(community_id))
+      .set((creator_id.eq(new_creator_id), updated.eq(naive_now())))
+      .get_result::<Self>(conn)
+  }
+
+  fn community_mods_and_admins(conn: &PgConnection, community_id: i32) -> Result<Vec<i32>, Error> {
+    use crate::{community_view::CommunityModeratorView, user_view::UserView};
+    let mut mods_and_admins: Vec<i32> = Vec::new();
+    mods_and_admins.append(
+      &mut CommunityModeratorView::for_community(conn, community_id)
+        .map(|v| v.into_iter().map(|m| m.user_id).collect())?,
+    );
+    mods_and_admins
+      .append(&mut UserView::admins(conn).map(|v| v.into_iter().map(|a| a.id).collect())?);
+    Ok(mods_and_admins)
+  }
+
+  pub fn is_mod_or_admin(conn: &PgConnection, user_id: i32, community_id: i32) -> bool {
+    Self::community_mods_and_admins(conn, community_id)
+      .unwrap_or_default()
+      .contains(&user_id)
   }
 }
 
@@ -248,6 +309,7 @@ mod tests {
       email: None,
       matrix_user_id: None,
       avatar: None,
+      banner: None,
       admin: false,
       banned: false,
       updated: None,
@@ -258,7 +320,7 @@ mod tests {
       lang: "browser".into(),
       show_avatars: true,
       send_notifications_to_email: false,
-      actor_id: "http://fake.com".into(),
+      actor_id: "changeme_8266238".into(),
       bio: None,
       local: true,
       private_key: None,
@@ -278,12 +340,14 @@ mod tests {
       removed: None,
       deleted: None,
       updated: None,
-      actor_id: "http://fake.com".into(),
+      actor_id: "changeme_7625376".into(),
       local: true,
       private_key: None,
       public_key: None,
       last_refreshed_at: None,
       published: None,
+      icon: None,
+      banner: None,
     };
 
     let inserted_community = Community::create(&conn, &new_community).unwrap();
@@ -300,11 +364,13 @@ mod tests {
       deleted: false,
       published: inserted_community.published,
       updated: None,
-      actor_id: "http://fake.com".into(),
+      actor_id: inserted_community.actor_id.to_owned(),
       local: true,
       private_key: None,
       public_key: None,
       last_refreshed_at: inserted_community.published,
+      icon: None,
+      banner: None,
     };
 
     let community_follower_form = CommunityFollowerForm {
index 5c6bd81a19590dc2c0dd62ae221616cc7a2cd983..e8353779c964e3cd8200d5ea7abde2a85139d82c 100644 (file)
@@ -8,6 +8,8 @@ table! {
     id -> Int4,
     name -> Varchar,
     title -> Varchar,
+    icon -> Nullable<Text>,
+    banner -> Nullable<Text>,
     description -> Nullable<Text>,
     category_id -> Int4,
     creator_id -> Int4,
@@ -22,6 +24,7 @@ table! {
     creator_actor_id -> Text,
     creator_local -> Bool,
     creator_name -> Varchar,
+    creator_preferred_username -> Nullable<Varchar>,
     creator_avatar -> Nullable<Text>,
     category_name -> Varchar,
     number_of_subscribers -> BigInt,
@@ -38,6 +41,8 @@ table! {
     id -> Int4,
     name -> Varchar,
     title -> Varchar,
+    icon -> Nullable<Text>,
+    banner -> Nullable<Text>,
     description -> Nullable<Text>,
     category_id -> Int4,
     creator_id -> Int4,
@@ -52,6 +57,7 @@ table! {
     creator_actor_id -> Text,
     creator_local -> Bool,
     creator_name -> Varchar,
+    creator_preferred_username -> Nullable<Varchar>,
     creator_avatar -> Nullable<Text>,
     category_name -> Varchar,
     number_of_subscribers -> BigInt,
@@ -72,10 +78,12 @@ table! {
     user_actor_id -> Text,
     user_local -> Bool,
     user_name -> Varchar,
+    user_preferred_username -> Nullable<Varchar>,
     avatar -> Nullable<Text>,
     community_actor_id -> Text,
     community_local -> Bool,
     community_name -> Varchar,
+    community_icon -> Nullable<Text>,
   }
 }
 
@@ -88,10 +96,12 @@ table! {
     user_actor_id -> Text,
     user_local -> Bool,
     user_name -> Varchar,
+    user_preferred_username -> Nullable<Varchar>,
     avatar -> Nullable<Text>,
     community_actor_id -> Text,
     community_local -> Bool,
     community_name -> Varchar,
+    community_icon -> Nullable<Text>,
   }
 }
 
@@ -104,10 +114,12 @@ table! {
     user_actor_id -> Text,
     user_local -> Bool,
     user_name -> Varchar,
+    user_preferred_username -> Nullable<Varchar>,
     avatar -> Nullable<Text>,
     community_actor_id -> Text,
     community_local -> Bool,
     community_name -> Varchar,
+    community_icon -> Nullable<Text>,
   }
 }
 
@@ -119,6 +131,8 @@ pub struct CommunityView {
   pub id: i32,
   pub name: String,
   pub title: String,
+  pub icon: Option<String>,
+  pub banner: Option<String>,
   pub description: Option<String>,
   pub category_id: i32,
   pub creator_id: i32,
@@ -133,6 +147,7 @@ pub struct CommunityView {
   pub creator_actor_id: String,
   pub creator_local: bool,
   pub creator_name: String,
+  pub creator_preferred_username: Option<String>,
   pub creator_avatar: Option<String>,
   pub category_name: String,
   pub number_of_subscribers: i64,
@@ -217,12 +232,6 @@ impl<'a> CommunityQueryBuilder<'a> {
 
     // The view lets you pass a null user_id, if you're not logged in
     match self.sort {
-      SortType::Hot => {
-        query = query
-          .order_by(hot_rank.desc())
-          .then_order_by(number_of_subscribers.desc())
-          .filter(user_id.is_null())
-      }
       SortType::New => query = query.order_by(published.desc()).filter(user_id.is_null()),
       SortType::TopAll => match self.from_user_id {
         Some(from_user_id) => {
@@ -236,7 +245,13 @@ impl<'a> CommunityQueryBuilder<'a> {
             .filter(user_id.is_null())
         }
       },
-      _ => (),
+      // Covers all other sorts, including hot
+      _ => {
+        query = query
+          .order_by(hot_rank.desc())
+          .then_order_by(number_of_subscribers.desc())
+          .filter(user_id.is_null())
+      }
     };
 
     if !self.show_nsfw {
@@ -288,25 +303,27 @@ pub struct CommunityModeratorView {
   pub user_actor_id: String,
   pub user_local: bool,
   pub user_name: String,
+  pub user_preferred_username: Option<String>,
   pub avatar: Option<String>,
   pub community_actor_id: String,
   pub community_local: bool,
   pub community_name: String,
+  pub community_icon: Option<String>,
 }
 
 impl CommunityModeratorView {
-  pub fn for_community(conn: &PgConnection, from_community_id: i32) -> Result<Vec<Self>, Error> {
+  pub fn for_community(conn: &PgConnection, for_community_id: i32) -> Result<Vec<Self>, Error> {
     use super::community_view::community_moderator_view::dsl::*;
     community_moderator_view
-      .filter(community_id.eq(from_community_id))
+      .filter(community_id.eq(for_community_id))
       .order_by(published)
       .load::<Self>(conn)
   }
 
-  pub fn for_user(conn: &PgConnection, from_user_id: i32) -> Result<Vec<Self>, Error> {
+  pub fn for_user(conn: &PgConnection, for_user_id: i32) -> Result<Vec<Self>, Error> {
     use super::community_view::community_moderator_view::dsl::*;
     community_moderator_view
-      .filter(user_id.eq(from_user_id))
+      .filter(user_id.eq(for_user_id))
       .order_by(published)
       .load::<Self>(conn)
   }
@@ -324,10 +341,12 @@ pub struct CommunityFollowerView {
   pub user_actor_id: String,
   pub user_local: bool,
   pub user_name: String,
+  pub user_preferred_username: Option<String>,
   pub avatar: Option<String>,
   pub community_actor_id: String,
   pub community_local: bool,
   pub community_name: String,
+  pub community_icon: Option<String>,
 }
 
 impl CommunityFollowerView {
@@ -358,27 +377,15 @@ pub struct CommunityUserBanView {
   pub user_actor_id: String,
   pub user_local: bool,
   pub user_name: String,
+  pub user_preferred_username: Option<String>,
   pub avatar: Option<String>,
   pub community_actor_id: String,
   pub community_local: bool,
   pub community_name: String,
+  pub community_icon: Option<String>,
 }
 
 impl CommunityUserBanView {
-  pub fn for_community(conn: &PgConnection, from_community_id: i32) -> Result<Vec<Self>, Error> {
-    use super::community_view::community_user_ban_view::dsl::*;
-    community_user_ban_view
-      .filter(community_id.eq(from_community_id))
-      .load::<Self>(conn)
-  }
-
-  pub fn for_user(conn: &PgConnection, from_user_id: i32) -> Result<Vec<Self>, Error> {
-    use super::community_view::community_user_ban_view::dsl::*;
-    community_user_ban_view
-      .filter(user_id.eq(from_user_id))
-      .load::<Self>(conn)
-  }
-
   pub fn get(
     conn: &PgConnection,
     from_user_id: i32,
index 2eead841d7dc25f3e22170d1fdbf8416f5704731..a5805ef9599fbfdc971571e7ea0a4d3d75ffeb16 100644 (file)
@@ -2,9 +2,12 @@
 pub extern crate diesel;
 #[macro_use]
 pub extern crate strum_macros;
+#[macro_use]
+pub extern crate lazy_static;
 pub extern crate bcrypt;
 pub extern crate chrono;
 pub extern crate log;
+pub extern crate regex;
 pub extern crate serde;
 pub extern crate serde_json;
 pub extern crate sha2;
@@ -12,6 +15,7 @@ pub extern crate strum;
 
 use chrono::NaiveDateTime;
 use diesel::{dsl::*, result::Error, *};
+use regex::Regex;
 use serde::{Deserialize, Serialize};
 use std::{env, env::VarError};
 
@@ -46,9 +50,12 @@ pub trait Crud<T> {
   fn update(conn: &PgConnection, id: i32, form: &T) -> Result<Self, Error>
   where
     Self: Sized;
-  fn delete(conn: &PgConnection, id: i32) -> Result<usize, Error>
+  fn delete(_conn: &PgConnection, _id: i32) -> Result<usize, Error>
   where
-    Self: Sized;
+    Self: Sized,
+  {
+    unimplemented!()
+  }
 }
 
 pub trait Followable<T> {
@@ -70,13 +77,10 @@ pub trait Joinable<T> {
 }
 
 pub trait Likeable<T> {
-  fn read(conn: &PgConnection, id: i32) -> Result<Vec<Self>, Error>
-  where
-    Self: Sized;
   fn like(conn: &PgConnection, form: &T) -> Result<Self, Error>
   where
     Self: Sized;
-  fn remove(conn: &PgConnection, form: &T) -> Result<usize, Error>
+  fn remove(conn: &PgConnection, user_id: i32, item_id: i32) -> Result<usize, Error>
   where
     Self: Sized;
 }
@@ -130,6 +134,7 @@ pub fn get_database_url_from_env() -> Result<String, VarError> {
 
 #[derive(EnumString, ToString, Debug, Serialize, Deserialize)]
 pub enum SortType {
+  Active,
   Hot,
   New,
   TopDay,
@@ -172,10 +177,33 @@ pub fn naive_now() -> NaiveDateTime {
   chrono::prelude::Utc::now().naive_utc()
 }
 
+pub fn is_email_regex(test: &str) -> bool {
+  EMAIL_REGEX.is_match(test)
+}
+
+pub fn diesel_option_overwrite(opt: &Option<String>) -> Option<Option<String>> {
+  match opt {
+    // An empty string is an erase
+    Some(unwrapped) => {
+      if !unwrapped.eq("") {
+        Some(Some(unwrapped.to_owned()))
+      } else {
+        Some(None)
+      }
+    }
+    None => None,
+  }
+}
+
+lazy_static! {
+  static ref EMAIL_REGEX: Regex =
+    Regex::new(r"^[a-zA-Z0-9.!#$%&’*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$").unwrap();
+}
+
 #[cfg(test)]
 mod tests {
   use super::fuzzy_search;
-  use crate::get_database_url_from_env;
+  use crate::{get_database_url_from_env, is_email_regex};
   use diesel::{Connection, PgConnection};
 
   pub fn establish_unpooled_connection() -> PgConnection {
@@ -194,4 +222,10 @@ mod tests {
     let test = "This is a fuzzy search";
     assert_eq!(fuzzy_search(test), "%This%is%a%fuzzy%search%".to_string());
   }
+
+  #[test]
+  fn test_email() {
+    assert!(is_email_regex("gush@gmail.com"));
+    assert!(!is_email_regex("nada_neutho"));
+  }
 }
index f5d33d9672b9c04d228d7cb7813290d0f4dc7cda..1a02d977ca222d6ed53b8d230f327b921a04cbb4 100644 (file)
@@ -41,11 +41,6 @@ impl Crud<ModRemovePostForm> for ModRemovePost {
     mod_remove_post.find(from_id).first::<Self>(conn)
   }
 
-  fn delete(conn: &PgConnection, from_id: i32) -> Result<usize, Error> {
-    use crate::schema::mod_remove_post::dsl::*;
-    diesel::delete(mod_remove_post.find(from_id)).execute(conn)
-  }
-
   fn create(conn: &PgConnection, form: &ModRemovePostForm) -> Result<Self, Error> {
     use crate::schema::mod_remove_post::dsl::*;
     insert_into(mod_remove_post)
@@ -85,11 +80,6 @@ impl Crud<ModLockPostForm> for ModLockPost {
     mod_lock_post.find(from_id).first::<Self>(conn)
   }
 
-  fn delete(conn: &PgConnection, from_id: i32) -> Result<usize, Error> {
-    use crate::schema::mod_lock_post::dsl::*;
-    diesel::delete(mod_lock_post.find(from_id)).execute(conn)
-  }
-
   fn create(conn: &PgConnection, form: &ModLockPostForm) -> Result<Self, Error> {
     use crate::schema::mod_lock_post::dsl::*;
     insert_into(mod_lock_post)
@@ -129,11 +119,6 @@ impl Crud<ModStickyPostForm> for ModStickyPost {
     mod_sticky_post.find(from_id).first::<Self>(conn)
   }
 
-  fn delete(conn: &PgConnection, from_id: i32) -> Result<usize, Error> {
-    use crate::schema::mod_sticky_post::dsl::*;
-    diesel::delete(mod_sticky_post.find(from_id)).execute(conn)
-  }
-
   fn create(conn: &PgConnection, form: &ModStickyPostForm) -> Result<Self, Error> {
     use crate::schema::mod_sticky_post::dsl::*;
     insert_into(mod_sticky_post)
@@ -175,11 +160,6 @@ impl Crud<ModRemoveCommentForm> for ModRemoveComment {
     mod_remove_comment.find(from_id).first::<Self>(conn)
   }
 
-  fn delete(conn: &PgConnection, from_id: i32) -> Result<usize, Error> {
-    use crate::schema::mod_remove_comment::dsl::*;
-    diesel::delete(mod_remove_comment.find(from_id)).execute(conn)
-  }
-
   fn create(conn: &PgConnection, form: &ModRemoveCommentForm) -> Result<Self, Error> {
     use crate::schema::mod_remove_comment::dsl::*;
     insert_into(mod_remove_comment)
@@ -223,11 +203,6 @@ impl Crud<ModRemoveCommunityForm> for ModRemoveCommunity {
     mod_remove_community.find(from_id).first::<Self>(conn)
   }
 
-  fn delete(conn: &PgConnection, from_id: i32) -> Result<usize, Error> {
-    use crate::schema::mod_remove_community::dsl::*;
-    diesel::delete(mod_remove_community.find(from_id)).execute(conn)
-  }
-
   fn create(conn: &PgConnection, form: &ModRemoveCommunityForm) -> Result<Self, Error> {
     use crate::schema::mod_remove_community::dsl::*;
     insert_into(mod_remove_community)
@@ -277,11 +252,6 @@ impl Crud<ModBanFromCommunityForm> for ModBanFromCommunity {
     mod_ban_from_community.find(from_id).first::<Self>(conn)
   }
 
-  fn delete(conn: &PgConnection, from_id: i32) -> Result<usize, Error> {
-    use crate::schema::mod_ban_from_community::dsl::*;
-    diesel::delete(mod_ban_from_community.find(from_id)).execute(conn)
-  }
-
   fn create(conn: &PgConnection, form: &ModBanFromCommunityForm) -> Result<Self, Error> {
     use crate::schema::mod_ban_from_community::dsl::*;
     insert_into(mod_ban_from_community)
@@ -329,11 +299,6 @@ impl Crud<ModBanForm> for ModBan {
     mod_ban.find(from_id).first::<Self>(conn)
   }
 
-  fn delete(conn: &PgConnection, from_id: i32) -> Result<usize, Error> {
-    use crate::schema::mod_ban::dsl::*;
-    diesel::delete(mod_ban.find(from_id)).execute(conn)
-  }
-
   fn create(conn: &PgConnection, form: &ModBanForm) -> Result<Self, Error> {
     use crate::schema::mod_ban::dsl::*;
     insert_into(mod_ban).values(form).get_result::<Self>(conn)
@@ -373,11 +338,6 @@ impl Crud<ModAddCommunityForm> for ModAddCommunity {
     mod_add_community.find(from_id).first::<Self>(conn)
   }
 
-  fn delete(conn: &PgConnection, from_id: i32) -> Result<usize, Error> {
-    use crate::schema::mod_add_community::dsl::*;
-    diesel::delete(mod_add_community.find(from_id)).execute(conn)
-  }
-
   fn create(conn: &PgConnection, form: &ModAddCommunityForm) -> Result<Self, Error> {
     use crate::schema::mod_add_community::dsl::*;
     insert_into(mod_add_community)
@@ -417,11 +377,6 @@ impl Crud<ModAddForm> for ModAdd {
     mod_add.find(from_id).first::<Self>(conn)
   }
 
-  fn delete(conn: &PgConnection, from_id: i32) -> Result<usize, Error> {
-    use crate::schema::mod_add::dsl::*;
-    diesel::delete(mod_add.find(from_id)).execute(conn)
-  }
-
   fn create(conn: &PgConnection, form: &ModAddForm) -> Result<Self, Error> {
     use crate::schema::mod_add::dsl::*;
     insert_into(mod_add).values(form).get_result::<Self>(conn)
@@ -460,6 +415,7 @@ mod tests {
       email: None,
       matrix_user_id: None,
       avatar: None,
+      banner: None,
       admin: false,
       banned: false,
       updated: None,
@@ -470,7 +426,7 @@ mod tests {
       lang: "browser".into(),
       show_avatars: true,
       send_notifications_to_email: false,
-      actor_id: "http://fake.com".into(),
+      actor_id: "changeme_829398".into(),
       bio: None,
       local: true,
       private_key: None,
@@ -487,6 +443,7 @@ mod tests {
       email: None,
       matrix_user_id: None,
       avatar: None,
+      banner: None,
       admin: false,
       banned: false,
       updated: None,
@@ -497,7 +454,7 @@ mod tests {
       lang: "browser".into(),
       show_avatars: true,
       send_notifications_to_email: false,
-      actor_id: "http://fake.com".into(),
+      actor_id: "changeme_82982738".into(),
       bio: None,
       local: true,
       private_key: None,
@@ -517,12 +474,14 @@ mod tests {
       deleted: None,
       updated: None,
       nsfw: false,
-      actor_id: "http://fake.com".into(),
+      actor_id: "changeme_283687".into(),
       local: true,
       private_key: None,
       public_key: None,
       last_refreshed_at: None,
       published: None,
+      icon: None,
+      banner: None,
     };
 
     let inserted_community = Community::create(&conn, &new_community).unwrap();
@@ -748,16 +707,6 @@ mod tests {
       when_: inserted_mod_add.when_,
     };
 
-    ModRemovePost::delete(&conn, inserted_mod_remove_post.id).unwrap();
-    ModLockPost::delete(&conn, inserted_mod_lock_post.id).unwrap();
-    ModStickyPost::delete(&conn, inserted_mod_sticky_post.id).unwrap();
-    ModRemoveComment::delete(&conn, inserted_mod_remove_comment.id).unwrap();
-    ModRemoveCommunity::delete(&conn, inserted_mod_remove_community.id).unwrap();
-    ModBanFromCommunity::delete(&conn, inserted_mod_ban_from_community.id).unwrap();
-    ModBan::delete(&conn, inserted_mod_ban.id).unwrap();
-    ModAddCommunity::delete(&conn, inserted_mod_add_community.id).unwrap();
-    ModAdd::delete(&conn, inserted_mod_add.id).unwrap();
-
     Comment::delete(&conn, inserted_comment.id).unwrap();
     Post::delete(&conn, inserted_post.id).unwrap();
     Community::delete(&conn, inserted_community.id).unwrap();
index a2692add863bdda3a3aac2fa3d5d8a61c0d400b0..06615187ea7720ba723d0a743d791d1076826c44 100644 (file)
@@ -28,9 +28,6 @@ impl Crud<PasswordResetRequestForm> for PasswordResetRequest {
       .find(password_reset_request_id)
       .first::<Self>(conn)
   }
-  fn delete(conn: &PgConnection, password_reset_request_id: i32) -> Result<usize, Error> {
-    diesel::delete(password_reset_request.find(password_reset_request_id)).execute(conn)
-  }
   fn create(conn: &PgConnection, form: &PasswordResetRequestForm) -> Result<Self, Error> {
     insert_into(password_reset_request)
       .values(form)
@@ -95,6 +92,7 @@ mod tests {
       email: None,
       matrix_user_id: None,
       avatar: None,
+      banner: None,
       admin: false,
       banned: false,
       updated: None,
@@ -105,7 +103,7 @@ mod tests {
       lang: "browser".into(),
       show_avatars: true,
       send_notifications_to_email: false,
-      actor_id: "http://fake.com".into(),
+      actor_id: "changeme_8292378".into(),
       bio: None,
       local: true,
       private_key: None,
index 1525a675f169e18346ac2c8b800d0289a9e4d762..a6df50bff6618ea485ecc06d7a089b17e257946a 100644 (file)
@@ -8,6 +8,7 @@ use crate::{
 };
 use diesel::{dsl::*, result::Error, *};
 use serde::{Deserialize, Serialize};
+use url::{ParseError, Url};
 
 #[derive(Queryable, Identifiable, PartialEq, Debug, Serialize, Deserialize)]
 #[table_name = "post"]
@@ -56,6 +57,12 @@ pub struct PostForm {
   pub local: bool,
 }
 
+impl PostForm {
+  pub fn get_ap_id(&self) -> Result<Url, ParseError> {
+    Url::parse(&self.ap_id)
+  }
+}
+
 impl Post {
   pub fn read(conn: &PgConnection, post_id: i32) -> Result<Self, Error> {
     use crate::schema::post::dsl::*;
@@ -69,6 +76,9 @@ impl Post {
     use crate::schema::post::dsl::*;
     post
       .filter(community_id.eq(the_community_id))
+      .then_order_by(published.desc())
+      .then_order_by(stickied.desc())
+      .limit(20)
       .load::<Self>(conn)
   }
 
@@ -85,13 +95,16 @@ impl Post {
       .get_result::<Self>(conn)
   }
 
-  pub fn permadelete(conn: &PgConnection, post_id: i32) -> Result<Self, Error> {
+  pub fn permadelete_for_creator(
+    conn: &PgConnection,
+    for_creator_id: i32,
+  ) -> Result<Vec<Self>, Error> {
     use crate::schema::post::dsl::*;
 
     let perma_deleted = "*Permananently Deleted*";
     let perma_deleted_url = "https://deleted.com";
 
-    diesel::update(post.find(post_id))
+    diesel::update(post.filter(creator_id.eq(for_creator_id)))
       .set((
         name.eq(perma_deleted),
         url.eq(perma_deleted_url),
@@ -99,8 +112,81 @@ impl Post {
         deleted.eq(true),
         updated.eq(naive_now()),
       ))
+      .get_results::<Self>(conn)
+  }
+
+  pub fn update_deleted(
+    conn: &PgConnection,
+    post_id: i32,
+    new_deleted: bool,
+  ) -> Result<Self, Error> {
+    use crate::schema::post::dsl::*;
+    diesel::update(post.find(post_id))
+      .set((deleted.eq(new_deleted), updated.eq(naive_now())))
       .get_result::<Self>(conn)
   }
+
+  pub fn update_removed(
+    conn: &PgConnection,
+    post_id: i32,
+    new_removed: bool,
+  ) -> Result<Self, Error> {
+    use crate::schema::post::dsl::*;
+    diesel::update(post.find(post_id))
+      .set((removed.eq(new_removed), updated.eq(naive_now())))
+      .get_result::<Self>(conn)
+  }
+
+  pub fn update_removed_for_creator(
+    conn: &PgConnection,
+    for_creator_id: i32,
+    for_community_id: Option<i32>,
+    new_removed: bool,
+  ) -> Result<Vec<Self>, Error> {
+    use crate::schema::post::dsl::*;
+
+    let mut update = diesel::update(post).into_boxed();
+    update = update.filter(creator_id.eq(for_creator_id));
+
+    if let Some(for_community_id) = for_community_id {
+      update = update.filter(community_id.eq(for_community_id));
+    }
+
+    update
+      .set((removed.eq(new_removed), updated.eq(naive_now())))
+      .get_results::<Self>(conn)
+  }
+
+  pub fn update_locked(conn: &PgConnection, post_id: i32, new_locked: bool) -> Result<Self, Error> {
+    use crate::schema::post::dsl::*;
+    diesel::update(post.find(post_id))
+      .set(locked.eq(new_locked))
+      .get_result::<Self>(conn)
+  }
+
+  pub fn update_stickied(
+    conn: &PgConnection,
+    post_id: i32,
+    new_stickied: bool,
+  ) -> Result<Self, Error> {
+    use crate::schema::post::dsl::*;
+    diesel::update(post.find(post_id))
+      .set(stickied.eq(new_stickied))
+      .get_result::<Self>(conn)
+  }
+
+  pub fn is_post_creator(user_id: i32, post_creator_id: i32) -> bool {
+    user_id == post_creator_id
+  }
+
+  pub fn upsert(conn: &PgConnection, post_form: &PostForm) -> Result<Post, Error> {
+    let existing = Self::read_from_apub_id(conn, &post_form.ap_id);
+    match existing {
+      Err(NotFound {}) => Ok(Self::create(conn, &post_form)?),
+      Ok(p) => Ok(Self::update(conn, p.id, &post_form)?),
+      Err(e) => Err(e),
+    }
+  }
 }
 
 impl Crud<PostForm> for Post {
@@ -147,24 +233,18 @@ pub struct PostLikeForm {
 }
 
 impl Likeable<PostLikeForm> for PostLike {
-  fn read(conn: &PgConnection, post_id_from: i32) -> Result<Vec<Self>, Error> {
-    use crate::schema::post_like::dsl::*;
-    post_like
-      .filter(post_id.eq(post_id_from))
-      .load::<Self>(conn)
-  }
   fn like(conn: &PgConnection, post_like_form: &PostLikeForm) -> Result<Self, Error> {
     use crate::schema::post_like::dsl::*;
     insert_into(post_like)
       .values(post_like_form)
       .get_result::<Self>(conn)
   }
-  fn remove(conn: &PgConnection, post_like_form: &PostLikeForm) -> Result<usize, Error> {
-    use crate::schema::post_like::dsl::*;
+  fn remove(conn: &PgConnection, user_id: i32, post_id: i32) -> Result<usize, Error> {
+    use crate::schema::post_like::dsl;
     diesel::delete(
-      post_like
-        .filter(post_id.eq(post_like_form.post_id))
-        .filter(user_id.eq(post_like_form.user_id)),
+      dsl::post_like
+        .filter(dsl::post_id.eq(post_id))
+        .filter(dsl::user_id.eq(user_id)),
     )
     .execute(conn)
   }
@@ -210,8 +290,11 @@ impl Saveable<PostSavedForm> for PostSaved {
 #[table_name = "post_read"]
 pub struct PostRead {
   pub id: i32,
+
   pub post_id: i32,
+
   pub user_id: i32,
+
   pub published: chrono::NaiveDateTime,
 }
 
@@ -219,6 +302,7 @@ pub struct PostRead {
 #[table_name = "post_read"]
 pub struct PostReadForm {
   pub post_id: i32,
+
   pub user_id: i32,
 }
 
@@ -229,6 +313,7 @@ impl Readable<PostReadForm> for PostRead {
       .values(post_read_form)
       .get_result::<Self>(conn)
   }
+
   fn mark_as_unread(conn: &PgConnection, post_read_form: &PostReadForm) -> Result<usize, Error> {
     use crate::schema::post_read::dsl::*;
     diesel::delete(
@@ -262,6 +347,7 @@ mod tests {
       email: None,
       matrix_user_id: None,
       avatar: None,
+      banner: None,
       admin: false,
       banned: false,
       updated: None,
@@ -272,7 +358,7 @@ mod tests {
       lang: "browser".into(),
       show_avatars: true,
       send_notifications_to_email: false,
-      actor_id: "http://fake.com".into(),
+      actor_id: "changeme_8292683678".into(),
       bio: None,
       local: true,
       private_key: None,
@@ -292,12 +378,14 @@ mod tests {
       deleted: None,
       updated: None,
       nsfw: false,
-      actor_id: "http://fake.com".into(),
+      actor_id: "changeme_8223262378".into(),
       local: true,
       private_key: None,
       public_key: None,
       last_refreshed_at: None,
       published: None,
+      icon: None,
+      banner: None,
     };
 
     let inserted_community = Community::create(&conn, &new_community).unwrap();
@@ -396,7 +484,7 @@ mod tests {
 
     let read_post = Post::read(&conn, inserted_post.id).unwrap();
     let updated_post = Post::update(&conn, inserted_post.id, &new_post).unwrap();
-    let like_removed = PostLike::remove(&conn, &post_like_form).unwrap();
+    let like_removed = PostLike::remove(&conn, inserted_user.id, inserted_post.id).unwrap();
     let saved_removed = PostSaved::unsave(&conn, &post_saved_form).unwrap();
     let read_removed = PostRead::mark_as_unread(&conn, &post_read_form).unwrap();
     let num_deleted = Post::delete(&conn, inserted_post.id).unwrap();
index 3e9f8737676891d4cb5a07ce4c69a7379479c1ce..35bfc7ab3958b15666147919211b0e1f79e3c323 100644 (file)
@@ -28,6 +28,7 @@ table! {
     creator_actor_id -> Text,
     creator_local -> Bool,
     creator_name -> Varchar,
+    creator_preferred_username -> Nullable<Varchar>,
     creator_published -> Timestamp,
     creator_avatar -> Nullable<Text>,
     banned -> Bool,
@@ -35,6 +36,7 @@ table! {
     community_actor_id -> Text,
     community_local -> Bool,
     community_name -> Varchar,
+    community_icon -> Nullable<Text>,
     community_removed -> Bool,
     community_deleted -> Bool,
     community_nsfw -> Bool,
@@ -43,6 +45,7 @@ table! {
     upvotes -> BigInt,
     downvotes -> BigInt,
     hot_rank -> Int4,
+    hot_rank_active -> Int4,
     newest_activity_time -> Timestamp,
     user_id -> Nullable<Int4>,
     my_vote -> Nullable<Int4>,
@@ -76,6 +79,7 @@ table! {
     creator_actor_id -> Text,
     creator_local -> Bool,
     creator_name -> Varchar,
+    creator_preferred_username -> Nullable<Varchar>,
     creator_published -> Timestamp,
     creator_avatar -> Nullable<Text>,
     banned -> Bool,
@@ -83,6 +87,7 @@ table! {
     community_actor_id -> Text,
     community_local -> Bool,
     community_name -> Varchar,
+    community_icon -> Nullable<Text>,
     community_removed -> Bool,
     community_deleted -> Bool,
     community_nsfw -> Bool,
@@ -91,6 +96,7 @@ table! {
     upvotes -> BigInt,
     downvotes -> BigInt,
     hot_rank -> Int4,
+    hot_rank_active -> Int4,
     newest_activity_time -> Timestamp,
     user_id -> Nullable<Int4>,
     my_vote -> Nullable<Int4>,
@@ -127,6 +133,7 @@ pub struct PostView {
   pub creator_actor_id: String,
   pub creator_local: bool,
   pub creator_name: String,
+  pub creator_preferred_username: Option<String>,
   pub creator_published: chrono::NaiveDateTime,
   pub creator_avatar: Option<String>,
   pub banned: bool,
@@ -134,6 +141,7 @@ pub struct PostView {
   pub community_actor_id: String,
   pub community_local: bool,
   pub community_name: String,
+  pub community_icon: Option<String>,
   pub community_removed: bool,
   pub community_deleted: bool,
   pub community_nsfw: bool,
@@ -142,6 +150,7 @@ pub struct PostView {
   pub upvotes: i64,
   pub downvotes: i64,
   pub hot_rank: i32,
+  pub hot_rank_active: i32,
   pub newest_activity_time: chrono::NaiveDateTime,
   pub user_id: Option<i32>,
   pub my_vote: Option<i32>,
@@ -243,11 +252,6 @@ impl<'a> PostQueryBuilder<'a> {
     self
   }
 
-  pub fn unread_only(mut self, unread_only: bool) -> Self {
-    self.unread_only = unread_only;
-    self
-  }
-
   pub fn page<T: MaybeOptional<i64>>(mut self, page: T) -> Self {
     self.page = page.get_optional();
     self
@@ -289,6 +293,9 @@ impl<'a> PostQueryBuilder<'a> {
     }
 
     query = match self.sort {
+      SortType::Active => query
+        .then_order_by(hot_rank_active.desc())
+        .then_order_by(published.desc()),
       SortType::Hot => query
         .then_order_by(hot_rank.desc())
         .then_order_by(published.desc()),
@@ -405,6 +412,7 @@ mod tests {
       email: None,
       matrix_user_id: None,
       avatar: None,
+      banner: None,
       updated: None,
       admin: false,
       banned: false,
@@ -415,7 +423,7 @@ mod tests {
       lang: "browser".into(),
       show_avatars: true,
       send_notifications_to_email: false,
-      actor_id: "http://fake.com".into(),
+      actor_id: "changeme_8282738268".into(),
       bio: None,
       local: true,
       private_key: None,
@@ -435,12 +443,14 @@ mod tests {
       deleted: None,
       updated: None,
       nsfw: false,
-      actor_id: "http://fake.com".into(),
+      actor_id: "changeme_2763".into(),
       local: true,
       private_key: None,
       public_key: None,
       last_refreshed_at: None,
       published: None,
+      icon: None,
+      banner: None,
     };
 
     let inserted_community = Community::create(&conn, &new_community).unwrap();
@@ -484,12 +494,6 @@ mod tests {
       score: 1,
     };
 
-    let post_like_form = PostLikeForm {
-      post_id: inserted_post.id,
-      user_id: inserted_user.id,
-      score: 1,
-    };
-
     let read_post_listings_with_user = PostQueryBuilder::create(&conn)
       .listing_type(ListingType::Community)
       .sort(&SortType::New)
@@ -519,6 +523,7 @@ mod tests {
       body: None,
       creator_id: inserted_user.id,
       creator_name: user_name.to_owned(),
+      creator_preferred_username: None,
       creator_published: inserted_user.published,
       creator_avatar: None,
       banned: false,
@@ -529,6 +534,7 @@ mod tests {
       locked: false,
       stickied: false,
       community_name: community_name.to_owned(),
+      community_icon: None,
       community_removed: false,
       community_deleted: false,
       community_nsfw: false,
@@ -537,6 +543,7 @@ mod tests {
       upvotes: 1,
       downvotes: 0,
       hot_rank: read_post_listing_no_user.hot_rank,
+      hot_rank_active: read_post_listing_no_user.hot_rank_active,
       published: inserted_post.published,
       newest_activity_time: inserted_post.published,
       updated: None,
@@ -569,12 +576,14 @@ mod tests {
       stickied: false,
       creator_id: inserted_user.id,
       creator_name: user_name,
+      creator_preferred_username: None,
       creator_published: inserted_user.published,
       creator_avatar: None,
       banned: false,
       banned_from_community: false,
       community_id: inserted_community.id,
       community_name,
+      community_icon: None,
       community_removed: false,
       community_deleted: false,
       community_nsfw: false,
@@ -583,6 +592,7 @@ mod tests {
       upvotes: 1,
       downvotes: 0,
       hot_rank: read_post_listing_with_user.hot_rank,
+      hot_rank_active: read_post_listing_with_user.hot_rank_active,
       published: inserted_post.published,
       newest_activity_time: inserted_post.published,
       updated: None,
@@ -602,7 +612,7 @@ mod tests {
       community_local: true,
     };
 
-    let like_removed = PostLike::remove(&conn, &post_like_form).unwrap();
+    let like_removed = PostLike::remove(&conn, inserted_user.id, inserted_post.id).unwrap();
     let num_deleted = Post::delete(&conn, inserted_post.id).unwrap();
     Community::delete(&conn, inserted_community.id).unwrap();
     User_::delete(&conn, inserted_user.id).unwrap();
index 1c0b455f3cbbd6584c943e4e009eaf8b9be05023..007a962084d54ea7268754a515195a8afe9b4e73 100644 (file)
@@ -1,4 +1,4 @@
-use crate::{schema::private_message, Crud};
+use crate::{naive_now, schema::private_message, Crud};
 use diesel::{dsl::*, result::Error, *};
 use serde::{Deserialize, Serialize};
 
@@ -37,11 +37,6 @@ impl Crud<PrivateMessageForm> for PrivateMessage {
     private_message.find(private_message_id).first::<Self>(conn)
   }
 
-  fn delete(conn: &PgConnection, private_message_id: i32) -> Result<usize, Error> {
-    use crate::schema::private_message::dsl::*;
-    diesel::delete(private_message.find(private_message_id)).execute(conn)
-  }
-
   fn create(conn: &PgConnection, private_message_form: &PrivateMessageForm) -> Result<Self, Error> {
     use crate::schema::private_message::dsl::*;
     insert_into(private_message)
@@ -80,6 +75,50 @@ impl PrivateMessage {
       .filter(ap_id.eq(object_id))
       .first::<Self>(conn)
   }
+
+  pub fn update_content(
+    conn: &PgConnection,
+    private_message_id: i32,
+    new_content: &str,
+  ) -> Result<Self, Error> {
+    use crate::schema::private_message::dsl::*;
+    diesel::update(private_message.find(private_message_id))
+      .set((content.eq(new_content), updated.eq(naive_now())))
+      .get_result::<Self>(conn)
+  }
+
+  pub fn update_deleted(
+    conn: &PgConnection,
+    private_message_id: i32,
+    new_deleted: bool,
+  ) -> Result<Self, Error> {
+    use crate::schema::private_message::dsl::*;
+    diesel::update(private_message.find(private_message_id))
+      .set(deleted.eq(new_deleted))
+      .get_result::<Self>(conn)
+  }
+
+  pub fn update_read(
+    conn: &PgConnection,
+    private_message_id: i32,
+    new_read: bool,
+  ) -> Result<Self, Error> {
+    use crate::schema::private_message::dsl::*;
+    diesel::update(private_message.find(private_message_id))
+      .set(read.eq(new_read))
+      .get_result::<Self>(conn)
+  }
+
+  pub fn mark_all_as_read(conn: &PgConnection, for_recipient_id: i32) -> Result<Vec<Self>, Error> {
+    use crate::schema::private_message::dsl::*;
+    diesel::update(
+      private_message
+        .filter(recipient_id.eq(for_recipient_id))
+        .filter(read.eq(false)),
+    )
+    .set(read.eq(true))
+    .get_results::<Self>(conn)
+  }
 }
 
 #[cfg(test)]
@@ -103,6 +142,7 @@ mod tests {
       email: None,
       matrix_user_id: None,
       avatar: None,
+      banner: None,
       admin: false,
       banned: false,
       updated: None,
@@ -113,7 +153,7 @@ mod tests {
       lang: "browser".into(),
       show_avatars: true,
       send_notifications_to_email: false,
-      actor_id: "http://fake.com".into(),
+      actor_id: "changeme_6723878".into(),
       bio: None,
       local: true,
       private_key: None,
@@ -130,6 +170,7 @@ mod tests {
       email: None,
       matrix_user_id: None,
       avatar: None,
+      banner: None,
       admin: false,
       banned: false,
       updated: None,
@@ -140,7 +181,7 @@ mod tests {
       lang: "browser".into(),
       show_avatars: true,
       send_notifications_to_email: false,
-      actor_id: "http://fake.com".into(),
+      actor_id: "changeme_287263876".into(),
       bio: None,
       local: true,
       private_key: None,
@@ -180,13 +221,17 @@ mod tests {
     let read_private_message = PrivateMessage::read(&conn, inserted_private_message.id).unwrap();
     let updated_private_message =
       PrivateMessage::update(&conn, inserted_private_message.id, &private_message_form).unwrap();
-    let num_deleted = PrivateMessage::delete(&conn, inserted_private_message.id).unwrap();
+    let deleted_private_message =
+      PrivateMessage::update_deleted(&conn, inserted_private_message.id, true).unwrap();
+    let marked_read_private_message =
+      PrivateMessage::update_read(&conn, inserted_private_message.id, true).unwrap();
     User_::delete(&conn, inserted_creator.id).unwrap();
     User_::delete(&conn, inserted_recipient.id).unwrap();
 
     assert_eq!(expected_private_message, read_private_message);
     assert_eq!(expected_private_message, updated_private_message);
     assert_eq!(expected_private_message, inserted_private_message);
-    assert_eq!(1, num_deleted);
+    assert!(deleted_private_message.deleted);
+    assert!(marked_read_private_message.read);
   }
 }
index dfb11c444c2bf0003b87fa75805c3508606025b5..c9b4249b6e3bfa9271ac70c5b37745619f9a70e6 100644 (file)
@@ -16,10 +16,12 @@ table! {
     ap_id -> Text,
     local -> Bool,
     creator_name -> Varchar,
+    creator_preferred_username -> Nullable<Varchar>,
     creator_avatar -> Nullable<Text>,
     creator_actor_id -> Text,
     creator_local -> Bool,
     recipient_name -> Varchar,
+    recipient_preferred_username -> Nullable<Varchar>,
     recipient_avatar -> Nullable<Text>,
     recipient_actor_id -> Text,
     recipient_local -> Bool,
@@ -42,10 +44,12 @@ pub struct PrivateMessageView {
   pub ap_id: String,
   pub local: bool,
   pub creator_name: String,
+  pub creator_preferred_username: Option<String>,
   pub creator_avatar: Option<String>,
   pub creator_actor_id: String,
   pub creator_local: bool,
   pub recipient_name: String,
+  pub recipient_preferred_username: Option<String>,
   pub recipient_avatar: Option<String>,
   pub recipient_actor_id: String,
   pub recipient_local: bool,
index 9608fb7d423260a360c40cc4d95a1b701b784319..c446edd9f27e72f2add810f181ef57b093a0c36d 100644 (file)
@@ -52,17 +52,20 @@ table! {
         community_actor_id -> Nullable<Varchar>,
         community_local -> Nullable<Bool>,
         community_name -> Nullable<Varchar>,
+        community_icon -> Nullable<Text>,
         banned -> Nullable<Bool>,
         banned_from_community -> Nullable<Bool>,
         creator_actor_id -> Nullable<Varchar>,
         creator_local -> Nullable<Bool>,
         creator_name -> Nullable<Varchar>,
+        creator_preferred_username -> Nullable<Varchar>,
         creator_published -> Nullable<Timestamp>,
         creator_avatar -> Nullable<Text>,
         score -> Nullable<Int8>,
         upvotes -> Nullable<Int8>,
         downvotes -> Nullable<Int8>,
         hot_rank -> Nullable<Int4>,
+        hot_rank_active -> Nullable<Int4>,
     }
 }
 
@@ -104,6 +107,8 @@ table! {
         private_key -> Nullable<Text>,
         public_key -> Nullable<Text>,
         last_refreshed_at -> Timestamp,
+        icon -> Nullable<Text>,
+        banner -> Nullable<Text>,
     }
 }
 
@@ -112,6 +117,8 @@ table! {
         id -> Int4,
         name -> Nullable<Varchar>,
         title -> Nullable<Varchar>,
+        icon -> Nullable<Text>,
+        banner -> Nullable<Text>,
         description -> Nullable<Text>,
         category_id -> Nullable<Int4>,
         creator_id -> Nullable<Int4>,
@@ -126,6 +133,7 @@ table! {
         creator_actor_id -> Nullable<Varchar>,
         creator_local -> Nullable<Bool>,
         creator_name -> Nullable<Varchar>,
+        creator_preferred_username -> Nullable<Varchar>,
         creator_avatar -> Nullable<Text>,
         category_name -> Nullable<Varchar>,
         number_of_subscribers -> Nullable<Int8>,
@@ -319,6 +327,7 @@ table! {
         creator_actor_id -> Nullable<Varchar>,
         creator_local -> Nullable<Bool>,
         creator_name -> Nullable<Varchar>,
+        creator_preferred_username -> Nullable<Varchar>,
         creator_published -> Nullable<Timestamp>,
         creator_avatar -> Nullable<Text>,
         banned -> Nullable<Bool>,
@@ -326,6 +335,7 @@ table! {
         community_actor_id -> Nullable<Varchar>,
         community_local -> Nullable<Bool>,
         community_name -> Nullable<Varchar>,
+        community_icon -> Nullable<Text>,
         community_removed -> Nullable<Bool>,
         community_deleted -> Nullable<Bool>,
         community_nsfw -> Nullable<Bool>,
@@ -334,6 +344,7 @@ table! {
         upvotes -> Nullable<Int8>,
         downvotes -> Nullable<Int8>,
         hot_rank -> Nullable<Int4>,
+        hot_rank_active -> Nullable<Int4>,
         newest_activity_time -> Nullable<Timestamp>,
     }
 }
@@ -392,6 +403,8 @@ table! {
         enable_downvotes -> Bool,
         open_registration -> Bool,
         enable_nsfw -> Bool,
+        icon -> Nullable<Text>,
+        banner -> Nullable<Text>,
     }
 }
 
@@ -421,6 +434,7 @@ table! {
         private_key -> Nullable<Text>,
         public_key -> Nullable<Text>,
         last_refreshed_at -> Timestamp,
+        banner -> Nullable<Text>,
     }
 }
 
@@ -437,7 +451,9 @@ table! {
         id -> Int4,
         actor_id -> Nullable<Varchar>,
         name -> Nullable<Varchar>,
+        preferred_username -> Nullable<Varchar>,
         avatar -> Nullable<Text>,
+        banner -> Nullable<Text>,
         email -> Nullable<Text>,
         matrix_user_id -> Nullable<Text>,
         bio -> Nullable<Text>,
index 066ae0b1a68f152d3f51f82db864ff9072dc02be..36b3e833d988cc672e2cc00432850db4e88b1485 100644 (file)
@@ -1,4 +1,4 @@
-use crate::{schema::site, Crud};
+use crate::{naive_now, schema::site, Crud};
 use diesel::{dsl::*, result::Error, *};
 use serde::{Deserialize, Serialize};
 
@@ -14,6 +14,8 @@ pub struct Site {
   pub enable_downvotes: bool,
   pub open_registration: bool,
   pub enable_nsfw: bool,
+  pub icon: Option<String>,
+  pub banner: Option<String>,
 }
 
 #[derive(Insertable, AsChangeset, Clone, Serialize, Deserialize)]
@@ -26,6 +28,9 @@ pub struct SiteForm {
   pub enable_downvotes: bool,
   pub open_registration: bool,
   pub enable_nsfw: bool,
+  // when you want to null out a column, you have to send Some(None)), since sending None means you just don't want to update that column.
+  pub icon: Option<Option<String>>,
+  pub banner: Option<Option<String>>,
 }
 
 impl Crud<SiteForm> for Site {
@@ -34,11 +39,6 @@ impl Crud<SiteForm> for Site {
     site.first::<Self>(conn)
   }
 
-  fn delete(conn: &PgConnection, site_id: i32) -> Result<usize, Error> {
-    use crate::schema::site::dsl::*;
-    diesel::delete(site.find(site_id)).execute(conn)
-  }
-
   fn create(conn: &PgConnection, new_site: &SiteForm) -> Result<Self, Error> {
     use crate::schema::site::dsl::*;
     insert_into(site).values(new_site).get_result::<Self>(conn)
@@ -51,3 +51,12 @@ impl Crud<SiteForm> for Site {
       .get_result::<Self>(conn)
   }
 }
+
+impl Site {
+  pub fn transfer(conn: &PgConnection, new_creator_id: i32) -> Result<Self, Error> {
+    use crate::schema::site::dsl::*;
+    diesel::update(site.find(1))
+      .set((creator_id.eq(new_creator_id), updated.eq(naive_now())))
+      .get_result::<Self>(conn)
+  }
+}
index bb9b54aa611825eba581ae9f02ae9ea23d636285..75cb29cb7e8414190d5598df5e3521cc9ee9aab1 100644 (file)
@@ -12,7 +12,10 @@ table! {
     enable_downvotes -> Bool,
     open_registration -> Bool,
     enable_nsfw -> Bool,
+    icon -> Nullable<Text>,
+    banner -> Nullable<Text>,
     creator_name -> Varchar,
+    creator_preferred_username -> Nullable<Varchar>,
     creator_avatar -> Nullable<Text>,
     number_of_users -> BigInt,
     number_of_posts -> BigInt,
@@ -35,7 +38,10 @@ pub struct SiteView {
   pub enable_downvotes: bool,
   pub open_registration: bool,
   pub enable_nsfw: bool,
+  pub icon: Option<String>,
+  pub banner: Option<String>,
   pub creator_name: String,
+  pub creator_preferred_username: Option<String>,
   pub creator_avatar: Option<String>,
   pub number_of_users: i64,
   pub number_of_posts: i64,
index 556fc1a75d52b5c6680f87bef52354b8fd751a62..8416d38a1e1024e9aee628ff2929a236b810e0e8 100644 (file)
@@ -1,12 +1,14 @@
 use crate::{
+  is_email_regex,
   naive_now,
   schema::{user_, user_::dsl::*},
   Crud,
 };
 use bcrypt::{hash, DEFAULT_COST};
 use diesel::{dsl::*, result::Error, *};
+use serde::{Deserialize, Serialize};
 
-#[derive(Clone, Queryable, Identifiable, PartialEq, Debug)]
+#[derive(Clone, Queryable, Identifiable, PartialEq, Debug, Serialize, Deserialize)]
 #[table_name = "user_"]
 pub struct User_ {
   pub id: i32,
@@ -33,6 +35,7 @@ pub struct User_ {
   pub private_key: Option<String>,
   pub public_key: Option<String>,
   pub last_refreshed_at: chrono::NaiveDateTime,
+  pub banner: Option<String>,
 }
 
 #[derive(Insertable, AsChangeset, Clone, Debug)]
@@ -43,8 +46,8 @@ pub struct UserForm {
   pub password_encrypted: String,
   pub admin: bool,
   pub banned: bool,
-  pub email: Option<String>,
-  pub avatar: Option<String>,
+  pub email: Option<Option<String>>,
+  pub avatar: Option<Option<String>>,
   pub updated: Option<chrono::NaiveDateTime>,
   pub show_nsfw: bool,
   pub theme: String,
@@ -60,6 +63,7 @@ pub struct UserForm {
   pub private_key: Option<String>,
   pub public_key: Option<String>,
   pub last_refreshed_at: Option<chrono::NaiveDateTime>,
+  pub banner: Option<Option<String>>,
 }
 
 impl Crud<UserForm> for User_ {
@@ -125,11 +129,20 @@ impl User_ {
     use crate::schema::user_::dsl::*;
     user_.filter(actor_id.eq(object_id)).first::<Self>(conn)
   }
-}
 
-impl User_ {
+  pub fn find_by_email_or_username(
+    conn: &PgConnection,
+    username_or_email: &str,
+  ) -> Result<Self, Error> {
+    if is_email_regex(username_or_email) {
+      Self::find_by_email(conn, username_or_email)
+    } else {
+      Self::find_by_username(conn, username_or_email)
+    }
+  }
+
   pub fn find_by_username(conn: &PgConnection, username: &str) -> Result<User_, Error> {
-    user_.filter(name.eq(username)).first::<User_>(conn)
+    user_.filter(name.ilike(username)).first::<User_>(conn)
   }
 
   pub fn find_by_email(conn: &PgConnection, from_email: &str) -> Result<User_, Error> {
@@ -156,6 +169,7 @@ mod tests {
       email: None,
       matrix_user_id: None,
       avatar: None,
+      banner: None,
       admin: false,
       banned: false,
       updated: None,
@@ -166,7 +180,7 @@ mod tests {
       lang: "browser".into(),
       show_avatars: true,
       send_notifications_to_email: false,
-      actor_id: "http://fake.com".into(),
+      actor_id: "changeme_9826382637".into(),
       bio: None,
       local: true,
       private_key: None,
@@ -184,6 +198,7 @@ mod tests {
       email: None,
       matrix_user_id: None,
       avatar: None,
+      banner: None,
       admin: false,
       banned: false,
       published: inserted_user.published,
@@ -195,7 +210,7 @@ mod tests {
       lang: "browser".into(),
       show_avatars: true,
       send_notifications_to_email: false,
-      actor_id: "http://fake.com".into(),
+      actor_id: inserted_user.actor_id.to_owned(),
       bio: None,
       local: true,
       private_key: None,
index 9f23f4410c2434db011ba0e6e1aeb4c8d0f2bfba..4c0fdc5ff85d0fb808d2371a8e1a15bde5749949 100644 (file)
@@ -28,11 +28,6 @@ impl Crud<UserMentionForm> for UserMention {
     user_mention.find(user_mention_id).first::<Self>(conn)
   }
 
-  fn delete(conn: &PgConnection, user_mention_id: i32) -> Result<usize, Error> {
-    use crate::schema::user_mention::dsl::*;
-    diesel::delete(user_mention.find(user_mention_id)).execute(conn)
-  }
-
   fn create(conn: &PgConnection, user_mention_form: &UserMentionForm) -> Result<Self, Error> {
     use crate::schema::user_mention::dsl::*;
     insert_into(user_mention)
@@ -52,6 +47,30 @@ impl Crud<UserMentionForm> for UserMention {
   }
 }
 
+impl UserMention {
+  pub fn update_read(
+    conn: &PgConnection,
+    user_mention_id: i32,
+    new_read: bool,
+  ) -> Result<Self, Error> {
+    use crate::schema::user_mention::dsl::*;
+    diesel::update(user_mention.find(user_mention_id))
+      .set(read.eq(new_read))
+      .get_result::<Self>(conn)
+  }
+
+  pub fn mark_all_as_read(conn: &PgConnection, for_recipient_id: i32) -> Result<Vec<Self>, Error> {
+    use crate::schema::user_mention::dsl::*;
+    diesel::update(
+      user_mention
+        .filter(recipient_id.eq(for_recipient_id))
+        .filter(read.eq(false)),
+    )
+    .set(read.eq(true))
+    .get_results::<Self>(conn)
+  }
+}
+
 #[cfg(test)]
 mod tests {
   use crate::{
@@ -76,6 +95,7 @@ mod tests {
       email: None,
       matrix_user_id: None,
       avatar: None,
+      banner: None,
       admin: false,
       banned: false,
       updated: None,
@@ -86,7 +106,7 @@ mod tests {
       lang: "browser".into(),
       show_avatars: true,
       send_notifications_to_email: false,
-      actor_id: "http://fake.com".into(),
+      actor_id: "changeme_628763".into(),
       bio: None,
       local: true,
       private_key: None,
@@ -103,6 +123,7 @@ mod tests {
       email: None,
       matrix_user_id: None,
       avatar: None,
+      banner: None,
       admin: false,
       banned: false,
       updated: None,
@@ -113,7 +134,7 @@ mod tests {
       lang: "browser".into(),
       show_avatars: true,
       send_notifications_to_email: false,
-      actor_id: "http://fake.com".into(),
+      actor_id: "changeme_927389278".into(),
       bio: None,
       local: true,
       private_key: None,
@@ -133,12 +154,14 @@ mod tests {
       deleted: None,
       updated: None,
       nsfw: false,
-      actor_id: "http://fake.com".into(),
+      actor_id: "changeme_876238".into(),
       local: true,
       private_key: None,
       public_key: None,
       last_refreshed_at: None,
       published: None,
+      icon: None,
+      banner: None,
     };
 
     let inserted_community = Community::create(&conn, &new_community).unwrap();
@@ -201,7 +224,6 @@ mod tests {
     let read_mention = UserMention::read(&conn, inserted_mention.id).unwrap();
     let updated_mention =
       UserMention::update(&conn, inserted_mention.id, &user_mention_form).unwrap();
-    let num_deleted = UserMention::delete(&conn, inserted_mention.id).unwrap();
     Comment::delete(&conn, inserted_comment.id).unwrap();
     Post::delete(&conn, inserted_post.id).unwrap();
     Community::delete(&conn, inserted_community.id).unwrap();
@@ -211,6 +233,5 @@ mod tests {
     assert_eq!(expected_mention, read_mention);
     assert_eq!(expected_mention, inserted_mention);
     assert_eq!(expected_mention, updated_mention);
-    assert_eq!(1, num_deleted);
   }
 }
index 359f166d67acee569563e70b8036c8cb0a95bffd..f74adc31b74cf43041dcb4c0e035e30754a4413e 100644 (file)
@@ -23,14 +23,17 @@ table! {
     community_actor_id -> Text,
     community_local -> Bool,
     community_name -> Varchar,
+    community_icon -> Nullable<Text>,
     banned -> Bool,
     banned_from_community -> Bool,
     creator_name -> Varchar,
+    creator_preferred_username -> Nullable<Varchar>,
     creator_avatar -> Nullable<Text>,
     score -> BigInt,
     upvotes -> BigInt,
     downvotes -> BigInt,
     hot_rank -> Int4,
+    hot_rank_active -> Int4,
     user_id -> Nullable<Int4>,
     my_vote -> Nullable<Int4>,
     saved -> Nullable<Bool>,
@@ -60,14 +63,17 @@ table! {
     community_actor_id -> Text,
     community_local -> Bool,
     community_name -> Varchar,
+    community_icon -> Nullable<Text>,
     banned -> Bool,
     banned_from_community -> Bool,
     creator_name -> Varchar,
+    creator_preferred_username -> Nullable<Varchar>,
     creator_avatar -> Nullable<Text>,
     score -> BigInt,
     upvotes -> BigInt,
     downvotes -> BigInt,
     hot_rank -> Int4,
+    hot_rank_active -> Int4,
     user_id -> Nullable<Int4>,
     my_vote -> Nullable<Int4>,
     saved -> Nullable<Bool>,
@@ -100,14 +106,17 @@ pub struct UserMentionView {
   pub community_actor_id: String,
   pub community_local: bool,
   pub community_name: String,
+  pub community_icon: Option<String>,
   pub banned: bool,
   pub banned_from_community: bool,
   pub creator_name: String,
+  pub creator_preferred_username: Option<String>,
   pub creator_avatar: Option<String>,
   pub score: i64,
   pub upvotes: i64,
   pub downvotes: i64,
   pub hot_rank: i32,
+  pub hot_rank_active: i32,
   pub user_id: Option<i32>,
   pub my_vote: Option<i32>,
   pub saved: Option<bool>,
@@ -180,6 +189,9 @@ impl<'a> UserMentionQueryBuilder<'a> {
       SortType::Hot => query
         .order_by(hot_rank.desc())
         .then_order_by(published.desc()),
+      SortType::Active => query
+        .order_by(hot_rank_active.desc())
+        .then_order_by(published.desc()),
       SortType::New => query.order_by(published.desc()),
       SortType::TopAll => query.order_by(score.desc()),
       SortType::TopYear => query
index f2ac47422492ecc9ad0a2bf7d918a333d2ea855c..08f4c79cfa6acfe17c2f064622c1911c60c0516c 100644 (file)
@@ -8,7 +8,9 @@ table! {
     id -> Int4,
     actor_id -> Text,
     name -> Varchar,
+    preferred_username -> Nullable<Varchar>,
     avatar -> Nullable<Text>,
+    banner -> Nullable<Text>,
     email -> Nullable<Text>,
     matrix_user_id -> Nullable<Text>,
     bio -> Nullable<Text>,
@@ -30,7 +32,9 @@ table! {
     id -> Int4,
     actor_id -> Text,
     name -> Varchar,
+    preferred_username -> Nullable<Varchar>,
     avatar -> Nullable<Text>,
+    banner -> Nullable<Text>,
     email -> Nullable<Text>,
     matrix_user_id -> Nullable<Text>,
     bio -> Nullable<Text>,
@@ -55,15 +59,17 @@ pub struct UserView {
   pub id: i32,
   pub actor_id: String,
   pub name: String,
+  pub preferred_username: Option<String>,
   pub avatar: Option<String>,
-  pub email: Option<String>,
+  pub banner: Option<String>,
+  pub email: Option<String>, // TODO this shouldn't be in this view
   pub matrix_user_id: Option<String>,
   pub bio: Option<String>,
   pub local: bool,
   pub admin: bool,
   pub banned: bool,
-  pub show_avatars: bool,
-  pub send_notifications_to_email: bool,
+  pub show_avatars: bool, // TODO this is a setting, probably doesn't need to be here
+  pub send_notifications_to_email: bool, // TODO also never used
   pub published: chrono::NaiveDateTime,
   pub number_of_posts: i64,
   pub post_score: i64,
@@ -126,6 +132,9 @@ impl<'a> UserQueryBuilder<'a> {
       SortType::Hot => query
         .order_by(comment_score.desc())
         .then_order_by(published.desc()),
+      SortType::Active => query
+        .order_by(comment_score.desc())
+        .then_order_by(published.desc()),
       SortType::New => query.order_by(published.desc()),
       SortType::TopAll => query.order_by(comment_score.desc()),
       SortType::TopYear => query
@@ -150,14 +159,32 @@ impl<'a> UserQueryBuilder<'a> {
 }
 
 impl UserView {
-  pub fn read(conn: &PgConnection, from_user_id: i32) -> Result<Self, Error> {
-    use super::user_view::user_fast::dsl::*;
-    user_fast.find(from_user_id).first::<Self>(conn)
-  }
-
   pub fn admins(conn: &PgConnection) -> Result<Vec<Self>, Error> {
     use super::user_view::user_fast::dsl::*;
+    use diesel::sql_types::{Nullable, Text};
     user_fast
+      // The select is necessary here to not get back emails
+      .select((
+        id,
+        actor_id,
+        name,
+        preferred_username,
+        avatar,
+        banner,
+        "".into_sql::<Nullable<Text>>(),
+        matrix_user_id,
+        bio,
+        local,
+        admin,
+        banned,
+        show_avatars,
+        send_notifications_to_email,
+        published,
+        number_of_posts,
+        post_score,
+        number_of_comments,
+        comment_score,
+      ))
       .filter(admin.eq(true))
       .order_by(published)
       .load::<Self>(conn)
@@ -165,6 +192,59 @@ impl UserView {
 
   pub fn banned(conn: &PgConnection) -> Result<Vec<Self>, Error> {
     use super::user_view::user_fast::dsl::*;
-    user_fast.filter(banned.eq(true)).load::<Self>(conn)
+    use diesel::sql_types::{Nullable, Text};
+    user_fast
+      .select((
+        id,
+        actor_id,
+        name,
+        preferred_username,
+        avatar,
+        banner,
+        "".into_sql::<Nullable<Text>>(),
+        matrix_user_id,
+        bio,
+        local,
+        admin,
+        banned,
+        show_avatars,
+        send_notifications_to_email,
+        published,
+        number_of_posts,
+        post_score,
+        number_of_comments,
+        comment_score,
+      ))
+      .filter(banned.eq(true))
+      .load::<Self>(conn)
+  }
+
+  pub fn get_user_secure(conn: &PgConnection, user_id: i32) -> Result<Self, Error> {
+    use super::user_view::user_fast::dsl::*;
+    use diesel::sql_types::{Nullable, Text};
+    user_fast
+      .select((
+        id,
+        actor_id,
+        name,
+        preferred_username,
+        avatar,
+        banner,
+        "".into_sql::<Nullable<Text>>(),
+        matrix_user_id,
+        bio,
+        local,
+        admin,
+        banned,
+        show_avatars,
+        send_notifications_to_email,
+        published,
+        number_of_posts,
+        post_score,
+        number_of_comments,
+        comment_score,
+      ))
+      .find(user_id)
+      .first::<Self>(conn)
   }
 }
index fed22f585f8602fb58f50f29a6ec1c2cbff2faa3..9685c0edaafef7b0017fab6ed4a196b706ad5618 100644 (file)
@@ -19,4 +19,4 @@ serde_json = { version = "1.0.52", features = ["preserve_order"]}
 comrak = "0.7"
 lazy_static = "1.3.0"
 openssl = "0.10"
-url = { version = "2.1.1", features = ["serde"] }
\ No newline at end of file
+url = { version = "2.1.1", features = ["serde"] }
index d88335e2da5cf260b27fcdf91b769067ebfb9749..41cdcec8e3b842706b3a4f72caac355561d28e29 100644 (file)
@@ -12,7 +12,7 @@ pub extern crate url;
 pub mod settings;
 
 use crate::settings::Settings;
-use chrono::{DateTime, FixedOffset, Local, NaiveDateTime, Utc};
+use chrono::{DateTime, FixedOffset, Local, NaiveDateTime};
 use itertools::Itertools;
 use lettre::{
   smtp::{
@@ -31,8 +31,16 @@ use regex::{Regex, RegexBuilder};
 use std::io::{Error, ErrorKind};
 use url::Url;
 
-pub fn to_datetime_utc(ndt: NaiveDateTime) -> DateTime<Utc> {
-  DateTime::<Utc>::from_utc(ndt, Utc)
+#[macro_export]
+macro_rules! location_info {
+  () => {
+    format!(
+      "None value at {}:{}, column {}",
+      file!(),
+      line!(),
+      column!()
+    )
+  };
 }
 
 pub fn naive_from_unix(time: i64) -> NaiveDateTime {
@@ -44,10 +52,6 @@ pub fn convert_datetime(datetime: NaiveDateTime) -> DateTime<FixedOffset> {
   DateTime::<FixedOffset>::from_utc(datetime, *now.offset())
 }
 
-pub fn is_email_regex(test: &str) -> bool {
-  EMAIL_REGEX.is_match(test)
-}
-
 pub fn remove_slurs(test: &str) -> String {
   SLUR_REGEX.replace_all(test, "*removed*").to_string()
 }
@@ -154,6 +158,13 @@ pub fn is_valid_username(name: &str) -> bool {
   VALID_USERNAME_REGEX.is_match(name)
 }
 
+// Can't do a regex here, reverse lookarounds not supported
+pub fn is_valid_preferred_username(preferred_username: &str) -> bool {
+  !preferred_username.starts_with('@')
+    && preferred_username.len() >= 3
+    && preferred_username.len() <= 20
+}
+
 pub fn is_valid_community_name(name: &str) -> bool {
   VALID_COMMUNITY_NAME_REGEX.is_match(name)
 }
@@ -165,9 +176,9 @@ pub fn is_valid_post_title(title: &str) -> bool {
 #[cfg(test)]
 mod tests {
   use crate::{
-    is_email_regex,
     is_valid_community_name,
     is_valid_post_title,
+    is_valid_preferred_username,
     is_valid_username,
     remove_slurs,
     scrape_text_for_mentions,
@@ -185,12 +196,6 @@ mod tests {
     assert_eq!(mentions[1].domain, "lemmy-alpha:8540".to_string());
   }
 
-  #[test]
-  fn test_email() {
-    assert!(is_email_regex("gush@gmail.com"));
-    assert!(!is_email_regex("nada_neutho"));
-  }
-
   #[test]
   fn test_valid_register_username() {
     assert!(is_valid_username("Hello_98"));
@@ -200,6 +205,12 @@ mod tests {
     assert!(!is_valid_username(""));
   }
 
+  #[test]
+  fn test_valid_preferred_username() {
+    assert!(is_valid_preferred_username("hello @there"));
+    assert!(!is_valid_preferred_username("@hello there"));
+  }
+
   #[test]
   fn test_valid_community_name() {
     assert!(is_valid_community_name("example"));
index 097063b6a55bcdc168737b5f94237cdd053a7525..1d82b231c4fd39960b99f2064cef1b08e649cdec 100644 (file)
@@ -8,15 +8,17 @@ static CONFIG_FILE: &str = "config/config.hjson";
 #[derive(Debug, Deserialize, Clone)]
 pub struct Settings {
   pub setup: Option<Setup>,
-  pub database: Database,
+  pub database: DatabaseConfig,
   pub hostname: String,
   pub bind: IpAddr,
   pub port: u16,
   pub jwt_secret: String,
   pub front_end_dir: String,
+  pub pictrs_url: String,
   pub rate_limit: RateLimitConfig,
   pub email: Option<EmailConfig>,
-  pub federation: Federation,
+  pub federation: FederationConfig,
+  pub captcha: CaptchaConfig,
 }
 
 #[derive(Debug, Deserialize, Clone)]
@@ -35,6 +37,8 @@ pub struct RateLimitConfig {
   pub post_per_second: i32,
   pub register: i32,
   pub register_per_second: i32,
+  pub image: i32,
+  pub image_per_second: i32,
 }
 
 #[derive(Debug, Deserialize, Clone)]
@@ -47,7 +51,13 @@ pub struct EmailConfig {
 }
 
 #[derive(Debug, Deserialize, Clone)]
-pub struct Database {
+pub struct CaptchaConfig {
+  pub enabled: bool,
+  pub difficulty: String, // easy, medium, or hard
+}
+
+#[derive(Debug, Deserialize, Clone)]
+pub struct DatabaseConfig {
   pub user: String,
   pub password: String,
   pub host: String,
@@ -57,10 +67,11 @@ pub struct Database {
 }
 
 #[derive(Debug, Deserialize, Clone)]
-pub struct Federation {
+pub struct FederationConfig {
   pub enabled: bool,
   pub tls_enabled: bool,
   pub allowed_instances: String,
+  pub blocked_instances: String,
 }
 
 lazy_static! {
@@ -81,9 +92,9 @@ impl Settings {
   fn init() -> Result<Self, ConfigError> {
     let mut s = Config::new();
 
-    s.merge(File::with_name(CONFIG_FILE_DEFAULTS))?;
+    s.merge(File::with_name(&Self::get_config_defaults_location()))?;
 
-    s.merge(File::with_name(&Self::get_config_location()).required(false))?;
+    s.merge(File::with_name(CONFIG_FILE).required(false))?;
 
     // Add in settings from the environment (with a prefix of LEMMY)
     // Eg.. `LEMMY_DEBUG=1 ./target/app` would set the `debug` key
@@ -111,20 +122,44 @@ impl Settings {
     )
   }
 
-  pub fn api_endpoint(&self) -> String {
-    format!("{}/api/v1", self.hostname)
+  pub fn get_config_defaults_location() -> String {
+    env::var("LEMMY_CONFIG_LOCATION").unwrap_or_else(|_| CONFIG_FILE_DEFAULTS.to_string())
   }
 
-  pub fn get_config_location() -> String {
-    env::var("LEMMY_CONFIG_LOCATION").unwrap_or_else(|_| CONFIG_FILE.to_string())
+  pub fn read_config_file() -> Result<String, Error> {
+    fs::read_to_string(CONFIG_FILE)
   }
 
-  pub fn read_config_file() -> Result<String, Error> {
-    fs::read_to_string(Self::get_config_location())
+  pub fn get_allowed_instances(&self) -> Vec<String> {
+    let mut allowed_instances: Vec<String> = self
+      .federation
+      .allowed_instances
+      .split(',')
+      .map(|d| d.to_string())
+      .collect();
+
+    // The defaults.hjson config always returns a [""]
+    allowed_instances.retain(|d| !d.eq(""));
+
+    allowed_instances
+  }
+
+  pub fn get_blocked_instances(&self) -> Vec<String> {
+    let mut blocked_instances: Vec<String> = self
+      .federation
+      .blocked_instances
+      .split(',')
+      .map(|d| d.to_string())
+      .collect();
+
+    // The defaults.hjson config always returns a [""]
+    blocked_instances.retain(|d| !d.eq(""));
+
+    blocked_instances
   }
 
   pub fn save_config_file(data: &str) -> Result<String, Error> {
-    fs::write(Self::get_config_location(), data)?;
+    fs::write(CONFIG_FILE, data)?;
 
     // Reload the new settings
     // From https://stackoverflow.com/questions/29654927/how-do-i-assign-a-string-to-a-mutable-static-variable/47181804#47181804
diff --git a/server/migrations/2020-07-18-234519_add_unique_community_user_actor_ids/down.sql b/server/migrations/2020-07-18-234519_add_unique_community_user_actor_ids/down.sql
new file mode 100644 (file)
index 0000000..7a4f2e2
--- /dev/null
@@ -0,0 +1,20 @@
+
+alter table community alter column actor_id set not null;
+alter table community alter column actor_id set default 'http://fake.com';
+alter table user_ alter column actor_id set not null;
+alter table user_ alter column actor_id set default 'http://fake.com';
+
+drop function generate_unique_changeme;
+
+update community
+set actor_id = 'http://fake.com'
+where actor_id like 'changeme_%';
+
+update user_
+set actor_id = 'http://fake.com'
+where actor_id like 'changeme_%';
+
+drop index idx_user_lower_actor_id;
+create unique index idx_user_name_lower_actor_id on user_ (lower(name), lower(actor_id));
+
+drop index idx_community_lower_actor_id;
diff --git a/server/migrations/2020-07-18-234519_add_unique_community_user_actor_ids/up.sql b/server/migrations/2020-07-18-234519_add_unique_community_user_actor_ids/up.sql
new file mode 100644 (file)
index 0000000..e32ed5e
--- /dev/null
@@ -0,0 +1,50 @@
+-- Following this issue : https://github.com/LemmyNet/lemmy/issues/957
+
+-- Creating a unique changeme actor_id
+create or replace function generate_unique_changeme() 
+returns text language sql 
+as $$
+  select 'changeme_' || string_agg (substr('abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz0123456789', ceil (random() * 62)::integer, 1), '')
+  from generate_series(1, 20)
+$$;
+
+-- Need to delete the possible community and user dupes for ones that don't start with the fake one
+-- A few test inserts, to make sure this removes later dupes
+-- insert into community (name, title, category_id, creator_id) values ('testcom', 'another testcom', 1, 2);
+delete from community a using (
+  select min(id) as id, actor_id
+    from community 
+    group by actor_id having count(*) > 1
+) b
+where a.actor_id = b.actor_id 
+and a.id <> b.id;
+
+delete from user_ a using (
+  select min(id) as id, actor_id
+    from user_ 
+    group by actor_id having count(*) > 1
+) b
+where a.actor_id = b.actor_id 
+and a.id <> b.id;
+
+-- Replacing the current default on the columns, to the unique one
+update community 
+set actor_id = generate_unique_changeme()
+where actor_id = 'http://fake.com';
+
+update user_ 
+set actor_id = generate_unique_changeme()
+where actor_id = 'http://fake.com';
+
+-- Add the unique indexes
+alter table community alter column actor_id set not null;
+alter table community alter column actor_id set default generate_unique_changeme();
+
+alter table user_ alter column actor_id set not null;
+alter table user_ alter column actor_id set default generate_unique_changeme();
+
+-- Add lowercase uniqueness too
+drop index idx_user_name_lower_actor_id;
+create unique index idx_user_lower_actor_id on user_ (lower(actor_id));
+
+create unique index idx_community_lower_actor_id on community (lower(actor_id));
diff --git a/server/migrations/2020-08-03-000110_add_preferred_usernames_banners_and_icons/down.sql b/server/migrations/2020-08-03-000110_add_preferred_usernames_banners_and_icons/down.sql
new file mode 100644 (file)
index 0000000..8ac1a99
--- /dev/null
@@ -0,0 +1,704 @@
+-- Drops first
+drop view site_view;
+drop table user_fast;
+drop view user_view;
+drop view post_fast_view;
+drop table post_aggregates_fast;
+drop view post_view;
+drop view post_aggregates_view;
+drop view community_moderator_view;
+drop view community_follower_view;
+drop view community_user_ban_view;
+drop view community_view;
+drop view community_aggregates_view;
+drop view community_fast_view;
+drop table community_aggregates_fast;
+drop view private_message_view;
+drop view user_mention_view;
+drop view reply_fast_view;
+drop view comment_fast_view;
+drop view comment_view;
+drop view user_mention_fast_view;
+drop table comment_aggregates_fast;
+drop view comment_aggregates_view;
+
+alter table site 
+  drop column icon,
+  drop column banner;
+
+alter table community 
+  drop column icon,
+  drop column banner;
+
+alter table user_ drop column banner;
+
+-- Site
+create view site_view as 
+select *,
+(select name from user_ u where s.creator_id = u.id) as creator_name,
+(select avatar from user_ u where s.creator_id = u.id) as creator_avatar,
+(select count(*) from user_) as number_of_users,
+(select count(*) from post) as number_of_posts,
+(select count(*) from comment) as number_of_comments,
+(select count(*) from community) as number_of_communities
+from site s;
+
+-- User
+create view user_view as
+select 
+       u.id,
+  u.actor_id,
+       u.name,
+       u.avatar,
+       u.email,
+       u.matrix_user_id,
+  u.bio,
+  u.local,
+       u.admin,
+       u.banned,
+       u.show_avatars,
+       u.send_notifications_to_email,
+       u.published,
+       coalesce(pd.posts, 0) as number_of_posts,
+       coalesce(pd.score, 0) as post_score,
+       coalesce(cd.comments, 0) as number_of_comments,
+       coalesce(cd.score, 0) as comment_score
+from user_ u
+left join (
+    select
+        p.creator_id as creator_id,
+        count(distinct p.id) as posts,
+        sum(pl.score) as score
+    from post p
+    join post_like pl on p.id = pl.post_id
+    group by p.creator_id
+) pd on u.id = pd.creator_id
+left join (
+    select
+        c.creator_id,
+        count(distinct c.id) as comments,
+        sum(cl.score) as score
+    from comment c
+    join comment_like cl on c.id = cl.comment_id
+    group by c.creator_id
+) cd on u.id = cd.creator_id;
+
+create table user_fast as select * from user_view;
+alter table user_fast add primary key (id);
+
+-- Post fast
+
+create view post_aggregates_view as
+select
+       p.*,
+       -- creator details
+       u.actor_id as creator_actor_id,
+       u."local" as creator_local,
+       u."name" as creator_name,
+  u.published as creator_published,
+       u.avatar as creator_avatar,
+  u.banned as banned,
+  cb.id::bool as banned_from_community,
+       -- community details
+       c.actor_id as community_actor_id,
+       c."local" as community_local,
+       c."name" as community_name,
+       c.removed as community_removed,
+       c.deleted as community_deleted,
+       c.nsfw as community_nsfw,
+       -- post score data/comment count
+       coalesce(ct.comments, 0) as number_of_comments,
+       coalesce(pl.score, 0) as score,
+       coalesce(pl.upvotes, 0) as upvotes,
+       coalesce(pl.downvotes, 0) as downvotes,
+       hot_rank(
+               coalesce(pl.score , 0), (
+                       case
+                               when (p.published < ('now'::timestamp - '1 month'::interval))
+                               then p.published
+                               else greatest(ct.recent_comment_time, p.published)
+                       end
+               )
+       ) as hot_rank,
+       (
+               case
+                       when (p.published < ('now'::timestamp - '1 month'::interval))
+                       then p.published
+                       else greatest(ct.recent_comment_time, p.published)
+               end
+       ) as newest_activity_time
+from post p
+left join user_ u on p.creator_id = u.id
+left join community_user_ban cb on p.creator_id = cb.user_id and p.community_id = cb.community_id
+left join community c on p.community_id = c.id
+left join (
+       select
+               post_id,
+               count(*) as comments,
+               max(published) as recent_comment_time
+       from comment
+       group by post_id
+) ct on ct.post_id = p.id
+left join (
+       select
+               post_id,
+               sum(score) as score,
+               sum(score) filter (where score = 1) as upvotes,
+               -sum(score) filter (where score = -1) as downvotes
+       from post_like
+       group by post_id
+) pl on pl.post_id = p.id
+order by p.id;
+
+create view post_view as
+select
+       pav.*,
+       us.id as user_id,
+       us.user_vote as my_vote,
+       us.is_subbed::bool as subscribed,
+       us.is_read::bool as read,
+       us.is_saved::bool as saved
+from post_aggregates_view pav
+cross join lateral (
+       select
+               u.id,
+               coalesce(cf.community_id, 0) as is_subbed,
+               coalesce(pr.post_id, 0) as is_read,
+               coalesce(ps.post_id, 0) as is_saved,
+               coalesce(pl.score, 0) as user_vote
+       from user_ u
+       left join community_user_ban cb on u.id = cb.user_id and cb.community_id = pav.community_id
+       left join community_follower cf on u.id = cf.user_id and cf.community_id = pav.community_id
+       left join post_read pr on u.id = pr.user_id and pr.post_id = pav.id
+       left join post_saved ps on u.id = ps.user_id and ps.post_id = pav.id
+       left join post_like pl on u.id = pl.user_id and pav.id = pl.post_id
+) as us
+
+union all
+
+select 
+pav.*,
+null as user_id,
+null as my_vote,
+null as subscribed,
+null as read,
+null as saved
+from post_aggregates_view pav;
+
+create table post_aggregates_fast as select * from post_aggregates_view;
+alter table post_aggregates_fast add primary key (id);
+
+create view post_fast_view as 
+select
+       pav.*,
+       us.id as user_id,
+       us.user_vote as my_vote,
+       us.is_subbed::bool as subscribed,
+       us.is_read::bool as read,
+       us.is_saved::bool as saved
+from post_aggregates_fast pav
+cross join lateral (
+       select
+               u.id,
+               coalesce(cf.community_id, 0) as is_subbed,
+               coalesce(pr.post_id, 0) as is_read,
+               coalesce(ps.post_id, 0) as is_saved,
+               coalesce(pl.score, 0) as user_vote
+       from user_ u
+       left join community_user_ban cb on u.id = cb.user_id and cb.community_id = pav.community_id
+       left join community_follower cf on u.id = cf.user_id and cf.community_id = pav.community_id
+       left join post_read pr on u.id = pr.user_id and pr.post_id = pav.id
+       left join post_saved ps on u.id = ps.user_id and ps.post_id = pav.id
+       left join post_like pl on u.id = pl.user_id and pav.id = pl.post_id
+) as us
+
+union all
+
+select 
+pav.*,
+null as user_id,
+null as my_vote,
+null as subscribed,
+null as read,
+null as saved
+from post_aggregates_fast pav;
+
+-- Community
+create view community_aggregates_view as
+select 
+    c.id,
+    c.name,
+    c.title,
+    c.description,
+    c.category_id,
+    c.creator_id,
+    c.removed,
+    c.published,
+    c.updated,
+    c.deleted,
+    c.nsfw,
+    c.actor_id,
+    c.local,
+    c.last_refreshed_at,
+    u.actor_id as creator_actor_id,
+    u.local as creator_local,
+    u.name as creator_name,
+    u.avatar as creator_avatar,
+    cat.name as category_name,
+    coalesce(cf.subs, 0) as number_of_subscribers,
+    coalesce(cd.posts, 0) as number_of_posts,
+    coalesce(cd.comments, 0) as number_of_comments,
+    hot_rank(cf.subs, c.published) as hot_rank
+from community c
+left join user_ u on c.creator_id = u.id
+left join category cat on c.category_id = cat.id
+left join (
+    select
+        p.community_id,
+        count(distinct p.id) as posts,
+        count(distinct ct.id) as comments
+    from post p
+    join comment ct on p.id = ct.post_id
+    group by p.community_id
+) cd on cd.community_id = c.id
+left join (
+    select
+        community_id,
+        count(*) as subs 
+    from community_follower
+    group by community_id 
+) cf on cf.community_id = c.id;
+
+create view community_view as
+select
+    cv.*,
+    us.user as user_id,
+    us.is_subbed::bool as subscribed
+from community_aggregates_view cv
+cross join lateral (
+       select
+               u.id as user,
+               coalesce(cf.community_id, 0) as is_subbed
+       from user_ u
+       left join community_follower cf on u.id = cf.user_id and cf.community_id = cv.id
+) as us
+
+union all
+
+select 
+    cv.*,
+    null as user_id,
+    null as subscribed
+from community_aggregates_view cv;
+
+create view community_moderator_view as
+select
+    cm.*,
+    u.actor_id as user_actor_id,
+    u.local as user_local,
+    u.name as user_name,
+    u.avatar as avatar,
+    c.actor_id as community_actor_id,
+    c.local as community_local,
+    c.name as community_name
+from community_moderator cm
+left join user_ u on cm.user_id = u.id
+left join community c on cm.community_id = c.id;
+
+create view community_follower_view as
+select
+    cf.*,
+    u.actor_id as user_actor_id,
+    u.local as user_local,
+    u.name as user_name,
+    u.avatar as avatar,
+    c.actor_id as community_actor_id,
+    c.local as community_local,
+    c.name as community_name
+from community_follower cf
+left join user_ u on cf.user_id = u.id
+left join community c on cf.community_id = c.id;
+
+create view community_user_ban_view as
+select
+    cb.*,
+    u.actor_id as user_actor_id,
+    u.local as user_local,
+    u.name as user_name,
+    u.avatar as avatar,
+    c.actor_id as community_actor_id,
+    c.local as community_local,
+    c.name as community_name
+from community_user_ban cb
+left join user_ u on cb.user_id = u.id
+left join community c on cb.community_id = c.id;
+
+-- The community fast table
+
+create table community_aggregates_fast as select * from community_aggregates_view;
+alter table community_aggregates_fast add primary key (id);
+
+create view community_fast_view as
+select
+ac.*,
+u.id as user_id,
+(select cf.id::boolean from community_follower cf where u.id = cf.user_id and ac.id = cf.community_id) as subscribed
+from user_ u
+cross join (
+  select
+  ca.*
+  from community_aggregates_fast ca
+) ac
+
+union all
+
+select 
+caf.*,
+null as user_id,
+null as subscribed
+from community_aggregates_fast caf;
+
+
+-- Private message
+create view private_message_view as 
+select        
+pm.*,
+u.name as creator_name,
+u.avatar as creator_avatar,
+u.actor_id as creator_actor_id,
+u.local as creator_local,
+u2.name as recipient_name,
+u2.avatar as recipient_avatar,
+u2.actor_id as recipient_actor_id,
+u2.local as recipient_local
+from private_message pm
+inner join user_ u on u.id = pm.creator_id
+inner join user_ u2 on u2.id = pm.recipient_id;
+
+
+-- Comments, mentions, replies
+
+create view comment_aggregates_view as
+select
+       ct.*,
+       -- post details
+       p."name" as post_name,
+       p.community_id,
+       -- community details
+       c.actor_id as community_actor_id,
+       c."local" as community_local,
+       c."name" as community_name,
+       -- creator details
+       u.banned as banned,
+  coalesce(cb.id, 0)::bool as banned_from_community,
+       u.actor_id as creator_actor_id,
+       u.local as creator_local,
+       u.name as creator_name,
+  u.published as creator_published,
+       u.avatar as creator_avatar,
+       -- score details
+       coalesce(cl.total, 0) as score,
+       coalesce(cl.up, 0) as upvotes,
+       coalesce(cl.down, 0) as downvotes,
+       hot_rank(coalesce(cl.total, 0), ct.published) as hot_rank
+from comment ct
+left join post p on ct.post_id = p.id
+left join community c on p.community_id = c.id
+left join user_ u on ct.creator_id = u.id
+left join community_user_ban cb on ct.creator_id = cb.user_id and p.id = ct.post_id and p.community_id = cb.community_id
+left join (
+       select
+               l.comment_id as id,
+               sum(l.score) as total,
+               count(case when l.score = 1 then 1 else null end) as up,
+               count(case when l.score = -1 then 1 else null end) as down
+       from comment_like l
+       group by comment_id
+) as cl on cl.id = ct.id;
+
+create or replace view comment_view as (
+select
+       cav.*,
+  us.user_id as user_id,
+  us.my_vote as my_vote,
+  us.is_subbed::bool as subscribed,
+  us.is_saved::bool as saved
+from comment_aggregates_view cav
+cross join lateral (
+       select
+               u.id as user_id,
+               coalesce(cl.score, 0) as my_vote,
+    coalesce(cf.id, 0) as is_subbed,
+    coalesce(cs.id, 0) as is_saved
+       from user_ u
+       left join comment_like cl on u.id = cl.user_id and cav.id = cl.comment_id
+       left join comment_saved cs on u.id = cs.user_id and cs.comment_id = cav.id
+       left join community_follower cf on u.id = cf.user_id and cav.community_id = cf.community_id
+) as us
+
+union all
+
+select
+    cav.*,
+    null as user_id,
+    null as my_vote,
+    null as subscribed,
+    null as saved
+from comment_aggregates_view cav
+);
+
+create table comment_aggregates_fast as select * from comment_aggregates_view;
+alter table comment_aggregates_fast add primary key (id);
+
+create view comment_fast_view as
+select
+       cav.*,
+  us.user_id as user_id,
+  us.my_vote as my_vote,
+  us.is_subbed::bool as subscribed,
+  us.is_saved::bool as saved
+from comment_aggregates_fast cav
+cross join lateral (
+       select
+               u.id as user_id,
+               coalesce(cl.score, 0) as my_vote,
+    coalesce(cf.id, 0) as is_subbed,
+    coalesce(cs.id, 0) as is_saved
+       from user_ u
+       left join comment_like cl on u.id = cl.user_id and cav.id = cl.comment_id
+       left join comment_saved cs on u.id = cs.user_id and cs.comment_id = cav.id
+       left join community_follower cf on u.id = cf.user_id and cav.community_id = cf.community_id
+) as us
+
+union all
+
+select
+    cav.*,
+    null as user_id,
+    null as my_vote,
+    null as subscribed,
+    null as saved
+from comment_aggregates_fast cav;
+
+create view user_mention_view as
+select
+    c.id,
+    um.id as user_mention_id,
+    c.creator_id,
+    c.creator_actor_id,
+    c.creator_local,
+    c.post_id,
+    c.post_name,
+    c.parent_id,
+    c.content,
+    c.removed,
+    um.read,
+    c.published,
+    c.updated,
+    c.deleted,
+    c.community_id,
+    c.community_actor_id,
+    c.community_local,
+    c.community_name,
+    c.banned,
+    c.banned_from_community,
+    c.creator_name,
+    c.creator_avatar,
+    c.score,
+    c.upvotes,
+    c.downvotes,
+    c.hot_rank,
+    c.user_id,
+    c.my_vote,
+    c.saved,
+    um.recipient_id,
+    (select actor_id from user_ u where u.id = um.recipient_id) as recipient_actor_id,
+    (select local from user_ u where u.id = um.recipient_id) as recipient_local
+from user_mention um, comment_view c
+where um.comment_id = c.id;
+
+create view user_mention_fast_view as
+select
+    ac.id,
+    um.id as user_mention_id,
+    ac.creator_id,
+    ac.creator_actor_id,
+    ac.creator_local,
+    ac.post_id,
+    ac.post_name,
+    ac.parent_id,
+    ac.content,
+    ac.removed,
+    um.read,
+    ac.published,
+    ac.updated,
+    ac.deleted,
+    ac.community_id,
+    ac.community_actor_id,
+    ac.community_local,
+    ac.community_name,
+    ac.banned,
+    ac.banned_from_community,
+    ac.creator_name,
+    ac.creator_avatar,
+    ac.score,
+    ac.upvotes,
+    ac.downvotes,
+    ac.hot_rank,
+    u.id as user_id,
+    coalesce(cl.score, 0) as my_vote,
+    (select cs.id::bool from comment_saved cs where u.id = cs.user_id and cs.comment_id = ac.id) as saved,
+    um.recipient_id,
+    (select actor_id from user_ u where u.id = um.recipient_id) as recipient_actor_id,
+    (select local from user_ u where u.id = um.recipient_id) as recipient_local
+from user_ u
+cross join (
+  select
+  ca.*
+  from comment_aggregates_fast ca
+) ac
+left join comment_like cl on u.id = cl.user_id and ac.id = cl.comment_id
+left join user_mention um on um.comment_id = ac.id
+
+union all
+
+select
+    ac.id,
+    um.id as user_mention_id,
+    ac.creator_id,
+    ac.creator_actor_id,
+    ac.creator_local,
+    ac.post_id,
+    ac.post_name,
+    ac.parent_id,
+    ac.content,
+    ac.removed,
+    um.read,
+    ac.published,
+    ac.updated,
+    ac.deleted,
+    ac.community_id,
+    ac.community_actor_id,
+    ac.community_local,
+    ac.community_name,
+    ac.banned,
+    ac.banned_from_community,
+    ac.creator_name,
+    ac.creator_avatar,
+    ac.score,
+    ac.upvotes,
+    ac.downvotes,
+    ac.hot_rank,
+    null as user_id,
+    null as my_vote,
+    null as saved,
+    um.recipient_id,
+    (select actor_id from user_ u where u.id = um.recipient_id) as recipient_actor_id,
+    (select local from user_ u where u.id = um.recipient_id) as recipient_local
+from comment_aggregates_fast ac
+left join user_mention um on um.comment_id = ac.id
+;
+
+-- Do the reply_view referencing the comment_fast_view
+create view reply_fast_view as
+with closereply as (
+    select
+    c2.id,
+    c2.creator_id as sender_id,
+    c.creator_id as recipient_id
+    from comment c
+    inner join comment c2 on c.id = c2.parent_id
+    where c2.creator_id != c.creator_id
+    -- Do union where post is null
+    union
+    select
+    c.id,
+    c.creator_id as sender_id,
+    p.creator_id as recipient_id
+    from comment c, post p
+    where c.post_id = p.id and c.parent_id is null and c.creator_id != p.creator_id
+)
+select cv.*,
+closereply.recipient_id
+from comment_fast_view cv, closereply
+where closereply.id = cv.id
+;
+
+-- redoing the triggers
+create or replace function refresh_post()
+returns trigger language plpgsql
+as $$
+begin
+  IF (TG_OP = 'DELETE') THEN
+    delete from post_aggregates_fast where id = OLD.id;
+
+    -- Update community number of posts
+    update community_aggregates_fast set number_of_posts = number_of_posts - 1 where id = OLD.community_id;
+  ELSIF (TG_OP = 'UPDATE') THEN
+    delete from post_aggregates_fast where id = OLD.id;
+    insert into post_aggregates_fast select * from post_aggregates_view where id = NEW.id;
+  ELSIF (TG_OP = 'INSERT') THEN
+    insert into post_aggregates_fast select * from post_aggregates_view where id = NEW.id;
+
+    -- Update that users number of posts, post score
+    delete from user_fast where id = NEW.creator_id;
+    insert into user_fast select * from user_view where id = NEW.creator_id;
+  
+    -- Update community number of posts
+    update community_aggregates_fast set number_of_posts = number_of_posts + 1 where id = NEW.community_id;
+
+    -- Update the hot rank on the post table
+    -- TODO this might not correctly update it, using a 1 week interval
+    update post_aggregates_fast as paf
+    set hot_rank = pav.hot_rank 
+    from post_aggregates_view as pav
+    where paf.id = pav.id  and (pav.published > ('now'::timestamp - '1 week'::interval));
+  END IF;
+
+  return null;
+end $$;
+
+create or replace function refresh_comment()
+returns trigger language plpgsql
+as $$
+begin
+  IF (TG_OP = 'DELETE') THEN
+    delete from comment_aggregates_fast where id = OLD.id;
+
+    -- Update community number of comments
+    update community_aggregates_fast as caf
+    set number_of_comments = number_of_comments - 1
+    from post as p
+    where caf.id = p.community_id and p.id = OLD.post_id;
+
+  ELSIF (TG_OP = 'UPDATE') THEN
+    delete from comment_aggregates_fast where id = OLD.id;
+    insert into comment_aggregates_fast select * from comment_aggregates_view where id = NEW.id;
+  ELSIF (TG_OP = 'INSERT') THEN
+    insert into comment_aggregates_fast select * from comment_aggregates_view where id = NEW.id;
+
+    -- Update user view due to comment count
+    update user_fast 
+    set number_of_comments = number_of_comments + 1
+    where id = NEW.creator_id;
+    
+    -- Update post view due to comment count, new comment activity time, but only on new posts
+    -- TODO this could be done more efficiently
+    delete from post_aggregates_fast where id = NEW.post_id;
+    insert into post_aggregates_fast select * from post_aggregates_view where id = NEW.post_id;
+
+    -- Force the hot rank as zero on week-older posts
+    update post_aggregates_fast as paf
+    set hot_rank = 0
+    where paf.id = NEW.post_id and (paf.published < ('now'::timestamp - '1 week'::interval));
+
+    -- Update community number of comments
+    update community_aggregates_fast as caf
+    set number_of_comments = number_of_comments + 1 
+    from post as p
+    where caf.id = p.community_id and p.id = NEW.post_id;
+
+  END IF;
+
+  return null;
+end $$;
diff --git a/server/migrations/2020-08-03-000110_add_preferred_usernames_banners_and_icons/up.sql b/server/migrations/2020-08-03-000110_add_preferred_usernames_banners_and_icons/up.sql
new file mode 100644 (file)
index 0000000..97f35fb
--- /dev/null
@@ -0,0 +1,748 @@
+-- This adds the following columns, as well as updates the views:
+--  Site icon
+--  Site banner
+--  Community icon
+--  Community Banner
+--  User Banner (User avatar is already there)
+--  User preferred name (already in table, needs to be added to view)
+
+-- It also adds hot_rank_active to post_view
+
+alter table site 
+  add column icon text,
+  add column banner text;
+
+alter table community 
+  add column icon text,
+  add column banner text;
+
+alter table user_ add column banner text;
+
+drop view site_view;
+create view site_view as 
+select s.*,
+u.name as creator_name,
+u.preferred_username as creator_preferred_username, 
+u.avatar as creator_avatar,
+(select count(*) from user_) as number_of_users,
+(select count(*) from post) as number_of_posts,
+(select count(*) from comment) as number_of_comments,
+(select count(*) from community) as number_of_communities
+from site s
+left join user_ u on s.creator_id = u.id;
+
+-- User
+drop table user_fast;
+drop view user_view;
+create view user_view as
+select 
+       u.id,
+  u.actor_id,
+       u.name,
+  u.preferred_username,
+       u.avatar,
+  u.banner,
+       u.email,
+       u.matrix_user_id,
+  u.bio,
+  u.local,
+       u.admin,
+       u.banned,
+       u.show_avatars,
+       u.send_notifications_to_email,
+       u.published,
+       coalesce(pd.posts, 0) as number_of_posts,
+       coalesce(pd.score, 0) as post_score,
+       coalesce(cd.comments, 0) as number_of_comments,
+       coalesce(cd.score, 0) as comment_score
+from user_ u
+left join (
+    select
+        p.creator_id as creator_id,
+        count(distinct p.id) as posts,
+        sum(pl.score) as score
+    from post p
+    join post_like pl on p.id = pl.post_id
+    group by p.creator_id
+) pd on u.id = pd.creator_id
+left join (
+    select
+        c.creator_id,
+        count(distinct c.id) as comments,
+        sum(cl.score) as score
+    from comment c
+    join comment_like cl on c.id = cl.comment_id
+    group by c.creator_id
+) cd on u.id = cd.creator_id;
+
+create table user_fast as select * from user_view;
+alter table user_fast add primary key (id);
+
+-- private message
+drop view private_message_view;
+create view private_message_view as 
+select        
+pm.*,
+u.name as creator_name,
+u.preferred_username as creator_preferred_username,
+u.avatar as creator_avatar,
+u.actor_id as creator_actor_id,
+u.local as creator_local,
+u2.name as recipient_name,
+u2.preferred_username as recipient_preferred_username,
+u2.avatar as recipient_avatar,
+u2.actor_id as recipient_actor_id,
+u2.local as recipient_local
+from private_message pm
+inner join user_ u on u.id = pm.creator_id
+inner join user_ u2 on u2.id = pm.recipient_id;
+
+-- Post fast
+drop view post_fast_view;
+drop table post_aggregates_fast;
+drop view post_view;
+drop view post_aggregates_view;
+
+create view post_aggregates_view as
+select
+       p.*,
+       -- creator details
+       u.actor_id as creator_actor_id,
+       u."local" as creator_local,
+       u."name" as creator_name,
+  u."preferred_username" as creator_preferred_username,
+  u.published as creator_published,
+       u.avatar as creator_avatar,
+  u.banned as banned,
+  cb.id::bool as banned_from_community,
+       -- community details
+       c.actor_id as community_actor_id,
+       c."local" as community_local,
+       c."name" as community_name,
+  c.icon as community_icon,
+       c.removed as community_removed,
+       c.deleted as community_deleted,
+       c.nsfw as community_nsfw,
+       -- post score data/comment count
+       coalesce(ct.comments, 0) as number_of_comments,
+       coalesce(pl.score, 0) as score,
+       coalesce(pl.upvotes, 0) as upvotes,
+       coalesce(pl.downvotes, 0) as downvotes,
+       hot_rank(coalesce(pl.score, 1), p.published) as hot_rank,
+  hot_rank(coalesce(pl.score, 1), greatest(ct.recent_comment_time, p.published)) as hot_rank_active,
+       greatest(ct.recent_comment_time, p.published) as newest_activity_time
+from post p
+left join user_ u on p.creator_id = u.id
+left join community_user_ban cb on p.creator_id = cb.user_id and p.community_id = cb.community_id
+left join community c on p.community_id = c.id
+left join (
+       select
+               post_id,
+               count(*) as comments,
+               max(published) as recent_comment_time
+       from comment
+       group by post_id
+) ct on ct.post_id = p.id
+left join (
+       select
+               post_id,
+               sum(score) as score,
+               sum(score) filter (where score = 1) as upvotes,
+               -sum(score) filter (where score = -1) as downvotes
+       from post_like
+       group by post_id
+) pl on pl.post_id = p.id
+order by p.id;
+
+create view post_view as
+select
+       pav.*,
+       us.id as user_id,
+       us.user_vote as my_vote,
+       us.is_subbed::bool as subscribed,
+       us.is_read::bool as read,
+       us.is_saved::bool as saved
+from post_aggregates_view pav
+cross join lateral (
+       select
+               u.id,
+               coalesce(cf.community_id, 0) as is_subbed,
+               coalesce(pr.post_id, 0) as is_read,
+               coalesce(ps.post_id, 0) as is_saved,
+               coalesce(pl.score, 0) as user_vote
+       from user_ u
+       left join community_user_ban cb on u.id = cb.user_id and cb.community_id = pav.community_id
+       left join community_follower cf on u.id = cf.user_id and cf.community_id = pav.community_id
+       left join post_read pr on u.id = pr.user_id and pr.post_id = pav.id
+       left join post_saved ps on u.id = ps.user_id and ps.post_id = pav.id
+       left join post_like pl on u.id = pl.user_id and pav.id = pl.post_id
+) as us
+
+union all
+
+select 
+pav.*,
+null as user_id,
+null as my_vote,
+null as subscribed,
+null as read,
+null as saved
+from post_aggregates_view pav;
+
+create table post_aggregates_fast as select * from post_aggregates_view;
+alter table post_aggregates_fast add primary key (id);
+
+-- For the hot rank resorting
+create index idx_post_aggregates_fast_hot_rank_published on post_aggregates_fast (hot_rank desc, published desc);
+create index idx_post_aggregates_fast_hot_rank_active_published on post_aggregates_fast (hot_rank_active desc, published desc);
+
+create view post_fast_view as 
+select
+       pav.*,
+       us.id as user_id,
+       us.user_vote as my_vote,
+       us.is_subbed::bool as subscribed,
+       us.is_read::bool as read,
+       us.is_saved::bool as saved
+from post_aggregates_fast pav
+cross join lateral (
+       select
+               u.id,
+               coalesce(cf.community_id, 0) as is_subbed,
+               coalesce(pr.post_id, 0) as is_read,
+               coalesce(ps.post_id, 0) as is_saved,
+               coalesce(pl.score, 0) as user_vote
+       from user_ u
+       left join community_user_ban cb on u.id = cb.user_id and cb.community_id = pav.community_id
+       left join community_follower cf on u.id = cf.user_id and cf.community_id = pav.community_id
+       left join post_read pr on u.id = pr.user_id and pr.post_id = pav.id
+       left join post_saved ps on u.id = ps.user_id and ps.post_id = pav.id
+       left join post_like pl on u.id = pl.user_id and pav.id = pl.post_id
+) as us
+
+union all
+
+select 
+pav.*,
+null as user_id,
+null as my_vote,
+null as subscribed,
+null as read,
+null as saved
+from post_aggregates_fast pav;
+
+-- Community
+drop view community_moderator_view;
+drop view community_follower_view;
+drop view community_user_ban_view;
+drop view community_view;
+drop view community_aggregates_view;
+drop view community_fast_view;
+drop table community_aggregates_fast;
+
+create view community_aggregates_view as
+select 
+    c.id,
+    c.name,
+    c.title,
+    c.icon,
+    c.banner,
+    c.description,
+    c.category_id,
+    c.creator_id,
+    c.removed,
+    c.published,
+    c.updated,
+    c.deleted,
+    c.nsfw,
+    c.actor_id,
+    c.local,
+    c.last_refreshed_at,
+    u.actor_id as creator_actor_id,
+    u.local as creator_local,
+    u.name as creator_name,
+    u.preferred_username as creator_preferred_username,
+    u.avatar as creator_avatar,
+    cat.name as category_name,
+    coalesce(cf.subs, 0) as number_of_subscribers,
+    coalesce(cd.posts, 0) as number_of_posts,
+    coalesce(cd.comments, 0) as number_of_comments,
+    hot_rank(cf.subs, c.published) as hot_rank
+from community c
+left join user_ u on c.creator_id = u.id
+left join category cat on c.category_id = cat.id
+left join (
+    select
+        p.community_id,
+        count(distinct p.id) as posts,
+        count(distinct ct.id) as comments
+    from post p
+    join comment ct on p.id = ct.post_id
+    group by p.community_id
+) cd on cd.community_id = c.id
+left join (
+    select
+        community_id,
+        count(*) as subs 
+    from community_follower
+    group by community_id 
+) cf on cf.community_id = c.id;
+
+create view community_view as
+select
+    cv.*,
+    us.user as user_id,
+    us.is_subbed::bool as subscribed
+from community_aggregates_view cv
+cross join lateral (
+       select
+               u.id as user,
+               coalesce(cf.community_id, 0) as is_subbed
+       from user_ u
+       left join community_follower cf on u.id = cf.user_id and cf.community_id = cv.id
+) as us
+
+union all
+
+select 
+    cv.*,
+    null as user_id,
+    null as subscribed
+from community_aggregates_view cv;
+
+create view community_moderator_view as
+select
+    cm.*,
+    u.actor_id as user_actor_id,
+    u.local as user_local,
+    u.name as user_name,
+    u.preferred_username as user_preferred_username,
+    u.avatar as avatar,
+    c.actor_id as community_actor_id,
+    c.local as community_local,
+    c.name as community_name,
+    c.icon as community_icon
+from community_moderator cm
+left join user_ u on cm.user_id = u.id
+left join community c on cm.community_id = c.id;
+
+create view community_follower_view as
+select
+    cf.*,
+    u.actor_id as user_actor_id,
+    u.local as user_local,
+    u.name as user_name,
+    u.preferred_username as user_preferred_username,
+    u.avatar as avatar,
+    c.actor_id as community_actor_id,
+    c.local as community_local,
+    c.name as community_name,
+    c.icon as community_icon
+from community_follower cf
+left join user_ u on cf.user_id = u.id
+left join community c on cf.community_id = c.id;
+
+create view community_user_ban_view as
+select
+    cb.*,
+    u.actor_id as user_actor_id,
+    u.local as user_local,
+    u.name as user_name,
+    u.preferred_username as user_preferred_username,
+    u.avatar as avatar,
+    c.actor_id as community_actor_id,
+    c.local as community_local,
+    c.name as community_name,
+    c.icon as community_icon
+from community_user_ban cb
+left join user_ u on cb.user_id = u.id
+left join community c on cb.community_id = c.id;
+
+-- The community fast table
+
+create table community_aggregates_fast as select * from community_aggregates_view;
+alter table community_aggregates_fast add primary key (id);
+
+create view community_fast_view as
+select
+ac.*,
+u.id as user_id,
+(select cf.id::boolean from community_follower cf where u.id = cf.user_id and ac.id = cf.community_id) as subscribed
+from user_ u
+cross join (
+  select
+  ca.*
+  from community_aggregates_fast ca
+) ac
+
+union all
+
+select 
+caf.*,
+null as user_id,
+null as subscribed
+from community_aggregates_fast caf;
+
+-- Comments, mentions, replies
+drop view user_mention_view;
+drop view reply_fast_view;
+drop view comment_fast_view;
+drop view comment_view;
+drop view user_mention_fast_view;
+drop table comment_aggregates_fast;
+drop view comment_aggregates_view;
+
+create view comment_aggregates_view as
+select
+       ct.*,
+       -- post details
+       p."name" as post_name,
+       p.community_id,
+       -- community details
+       c.actor_id as community_actor_id,
+       c."local" as community_local,
+       c."name" as community_name,
+  c.icon as community_icon,
+       -- creator details
+       u.banned as banned,
+  coalesce(cb.id, 0)::bool as banned_from_community,
+       u.actor_id as creator_actor_id,
+       u.local as creator_local,
+       u.name as creator_name,
+  u.preferred_username as creator_preferred_username,
+  u.published as creator_published,
+       u.avatar as creator_avatar,
+       -- score details
+       coalesce(cl.total, 0) as score,
+       coalesce(cl.up, 0) as upvotes,
+       coalesce(cl.down, 0) as downvotes,
+       hot_rank(coalesce(cl.total, 1), p.published) as hot_rank,
+       hot_rank(coalesce(cl.total, 1), ct.published) as hot_rank_active
+from comment ct
+left join post p on ct.post_id = p.id
+left join community c on p.community_id = c.id
+left join user_ u on ct.creator_id = u.id
+left join community_user_ban cb on ct.creator_id = cb.user_id and p.id = ct.post_id and p.community_id = cb.community_id
+left join (
+       select
+               l.comment_id as id,
+               sum(l.score) as total,
+               count(case when l.score = 1 then 1 else null end) as up,
+               count(case when l.score = -1 then 1 else null end) as down
+       from comment_like l
+       group by comment_id
+) as cl on cl.id = ct.id;
+
+create or replace view comment_view as (
+select
+       cav.*,
+  us.user_id as user_id,
+  us.my_vote as my_vote,
+  us.is_subbed::bool as subscribed,
+  us.is_saved::bool as saved
+from comment_aggregates_view cav
+cross join lateral (
+       select
+               u.id as user_id,
+               coalesce(cl.score, 0) as my_vote,
+    coalesce(cf.id, 0) as is_subbed,
+    coalesce(cs.id, 0) as is_saved
+       from user_ u
+       left join comment_like cl on u.id = cl.user_id and cav.id = cl.comment_id
+       left join comment_saved cs on u.id = cs.user_id and cs.comment_id = cav.id
+       left join community_follower cf on u.id = cf.user_id and cav.community_id = cf.community_id
+) as us
+
+union all
+
+select
+    cav.*,
+    null as user_id,
+    null as my_vote,
+    null as subscribed,
+    null as saved
+from comment_aggregates_view cav
+);
+
+create table comment_aggregates_fast as select * from comment_aggregates_view;
+alter table comment_aggregates_fast add primary key (id);
+
+create view comment_fast_view as
+select
+       cav.*,
+  us.user_id as user_id,
+  us.my_vote as my_vote,
+  us.is_subbed::bool as subscribed,
+  us.is_saved::bool as saved
+from comment_aggregates_fast cav
+cross join lateral (
+       select
+               u.id as user_id,
+               coalesce(cl.score, 0) as my_vote,
+    coalesce(cf.id, 0) as is_subbed,
+    coalesce(cs.id, 0) as is_saved
+       from user_ u
+       left join comment_like cl on u.id = cl.user_id and cav.id = cl.comment_id
+       left join comment_saved cs on u.id = cs.user_id and cs.comment_id = cav.id
+       left join community_follower cf on u.id = cf.user_id and cav.community_id = cf.community_id
+) as us
+
+union all
+
+select
+    cav.*,
+    null as user_id,
+    null as my_vote,
+    null as subscribed,
+    null as saved
+from comment_aggregates_fast cav;
+
+create view user_mention_view as
+select
+    c.id,
+    um.id as user_mention_id,
+    c.creator_id,
+    c.creator_actor_id,
+    c.creator_local,
+    c.post_id,
+    c.post_name,
+    c.parent_id,
+    c.content,
+    c.removed,
+    um.read,
+    c.published,
+    c.updated,
+    c.deleted,
+    c.community_id,
+    c.community_actor_id,
+    c.community_local,
+    c.community_name,
+    c.community_icon,
+    c.banned,
+    c.banned_from_community,
+    c.creator_name,
+    c.creator_preferred_username,
+    c.creator_avatar,
+    c.score,
+    c.upvotes,
+    c.downvotes,
+    c.hot_rank,
+    c.hot_rank_active,
+    c.user_id,
+    c.my_vote,
+    c.saved,
+    um.recipient_id,
+    (select actor_id from user_ u where u.id = um.recipient_id) as recipient_actor_id,
+    (select local from user_ u where u.id = um.recipient_id) as recipient_local
+from user_mention um, comment_view c
+where um.comment_id = c.id;
+
+create view user_mention_fast_view as
+select
+    ac.id,
+    um.id as user_mention_id,
+    ac.creator_id,
+    ac.creator_actor_id,
+    ac.creator_local,
+    ac.post_id,
+    ac.post_name,
+    ac.parent_id,
+    ac.content,
+    ac.removed,
+    um.read,
+    ac.published,
+    ac.updated,
+    ac.deleted,
+    ac.community_id,
+    ac.community_actor_id,
+    ac.community_local,
+    ac.community_name,
+    ac.community_icon,
+    ac.banned,
+    ac.banned_from_community,
+    ac.creator_name,
+    ac.creator_preferred_username,
+    ac.creator_avatar,
+    ac.score,
+    ac.upvotes,
+    ac.downvotes,
+    ac.hot_rank,
+    ac.hot_rank_active,
+    u.id as user_id,
+    coalesce(cl.score, 0) as my_vote,
+    (select cs.id::bool from comment_saved cs where u.id = cs.user_id and cs.comment_id = ac.id) as saved,
+    um.recipient_id,
+    (select actor_id from user_ u where u.id = um.recipient_id) as recipient_actor_id,
+    (select local from user_ u where u.id = um.recipient_id) as recipient_local
+from user_ u
+cross join (
+  select
+  ca.*
+  from comment_aggregates_fast ca
+) ac
+left join comment_like cl on u.id = cl.user_id and ac.id = cl.comment_id
+left join user_mention um on um.comment_id = ac.id
+
+union all
+
+select
+    ac.id,
+    um.id as user_mention_id,
+    ac.creator_id,
+    ac.creator_actor_id,
+    ac.creator_local,
+    ac.post_id,
+    ac.post_name,
+    ac.parent_id,
+    ac.content,
+    ac.removed,
+    um.read,
+    ac.published,
+    ac.updated,
+    ac.deleted,
+    ac.community_id,
+    ac.community_actor_id,
+    ac.community_local,
+    ac.community_name,
+    ac.community_icon,
+    ac.banned,
+    ac.banned_from_community,
+    ac.creator_name,
+    ac.creator_preferred_username,
+    ac.creator_avatar,
+    ac.score,
+    ac.upvotes,
+    ac.downvotes,
+    ac.hot_rank,
+    ac.hot_rank_active,
+    null as user_id,
+    null as my_vote,
+    null as saved,
+    um.recipient_id,
+    (select actor_id from user_ u where u.id = um.recipient_id) as recipient_actor_id,
+    (select local from user_ u where u.id = um.recipient_id) as recipient_local
+from comment_aggregates_fast ac
+left join user_mention um on um.comment_id = ac.id
+;
+
+-- Do the reply_view referencing the comment_fast_view
+create view reply_fast_view as
+with closereply as (
+    select
+    c2.id,
+    c2.creator_id as sender_id,
+    c.creator_id as recipient_id
+    from comment c
+    inner join comment c2 on c.id = c2.parent_id
+    where c2.creator_id != c.creator_id
+    -- Do union where post is null
+    union
+    select
+    c.id,
+    c.creator_id as sender_id,
+    p.creator_id as recipient_id
+    from comment c, post p
+    where c.post_id = p.id and c.parent_id is null and c.creator_id != p.creator_id
+)
+select cv.*,
+closereply.recipient_id
+from comment_fast_view cv, closereply
+where closereply.id = cv.id
+;
+
+-- Adding hot rank active to the triggers
+create or replace function refresh_post()
+returns trigger language plpgsql
+as $$
+begin
+  IF (TG_OP = 'DELETE') THEN
+    delete from post_aggregates_fast where id = OLD.id;
+
+    -- Update community number of posts
+    update community_aggregates_fast set number_of_posts = number_of_posts - 1 where id = OLD.community_id;
+  ELSIF (TG_OP = 'UPDATE') THEN
+    delete from post_aggregates_fast where id = OLD.id;
+    insert into post_aggregates_fast select * from post_aggregates_view where id = NEW.id;
+  ELSIF (TG_OP = 'INSERT') THEN
+    insert into post_aggregates_fast select * from post_aggregates_view where id = NEW.id;
+
+    -- Update that users number of posts, post score
+    delete from user_fast where id = NEW.creator_id;
+    insert into user_fast select * from user_view where id = NEW.creator_id;
+  
+    -- Update community number of posts
+    update community_aggregates_fast set number_of_posts = number_of_posts + 1 where id = NEW.community_id;
+
+    -- Update the hot rank on the post table
+    -- TODO this might not correctly update it, using a 1 week interval
+    update post_aggregates_fast as paf
+    set 
+      hot_rank = pav.hot_rank,
+      hot_rank_active = pav.hot_rank_active
+    from post_aggregates_view as pav
+    where paf.id = pav.id  and (pav.published > ('now'::timestamp - '1 week'::interval));
+  END IF;
+
+  return null;
+end $$;
+
+create or replace function refresh_comment()
+returns trigger language plpgsql
+as $$
+begin
+  IF (TG_OP = 'DELETE') THEN
+    delete from comment_aggregates_fast where id = OLD.id;
+
+    -- Update community number of comments
+    update community_aggregates_fast as caf
+    set number_of_comments = number_of_comments - 1
+    from post as p
+    where caf.id = p.community_id and p.id = OLD.post_id;
+
+  ELSIF (TG_OP = 'UPDATE') THEN
+    delete from comment_aggregates_fast where id = OLD.id;
+    insert into comment_aggregates_fast select * from comment_aggregates_view where id = NEW.id;
+  ELSIF (TG_OP = 'INSERT') THEN
+    insert into comment_aggregates_fast select * from comment_aggregates_view where id = NEW.id;
+
+    -- Update user view due to comment count
+    update user_fast 
+    set number_of_comments = number_of_comments + 1
+    where id = NEW.creator_id;
+    
+    -- Update post view due to comment count, new comment activity time, but only on new posts
+    -- TODO this could be done more efficiently
+    delete from post_aggregates_fast where id = NEW.post_id;
+    insert into post_aggregates_fast select * from post_aggregates_view where id = NEW.post_id;
+
+    -- Update the comment hot_ranks as of last week
+    update comment_aggregates_fast as caf
+    set 
+      hot_rank = cav.hot_rank,
+      hot_rank_active = cav.hot_rank_active
+    from comment_aggregates_view as cav
+    where caf.id = cav.id and (cav.published > ('now'::timestamp - '1 week'::interval));
+
+    -- Update the post ranks
+    update post_aggregates_fast as paf
+    set 
+      hot_rank = pav.hot_rank,
+      hot_rank_active = pav.hot_rank_active
+    from post_aggregates_view as pav
+    where paf.id = pav.id  and (pav.published > ('now'::timestamp - '1 week'::interval));
+
+    -- Force the hot rank active as zero on 2 day-older posts (necro-bump)
+    update post_aggregates_fast as paf
+    set hot_rank_active = 0
+    where paf.id = NEW.post_id and (paf.published < ('now'::timestamp - '2 days'::interval));
+
+    -- Update community number of comments
+    update community_aggregates_fast as caf
+    set number_of_comments = number_of_comments + 1 
+    from post as p
+    where caf.id = p.community_id and p.id = NEW.post_id;
+
+  END IF;
+
+  return null;
+end $$;
diff --git a/server/migrations/2020-08-06-205355_update_community_post_count/down.sql b/server/migrations/2020-08-06-205355_update_community_post_count/down.sql
new file mode 100644 (file)
index 0000000..53b016c
--- /dev/null
@@ -0,0 +1,100 @@
+-- Drop first
+drop view community_view;
+drop view community_aggregates_view;
+drop view community_fast_view;
+drop table community_aggregates_fast;
+
+create view community_aggregates_view as
+select
+    c.id,
+    c.name,
+    c.title,
+    c.icon,
+    c.banner,
+    c.description,
+    c.category_id,
+    c.creator_id,
+    c.removed,
+    c.published,
+    c.updated,
+    c.deleted,
+    c.nsfw,
+    c.actor_id,
+    c.local,
+    c.last_refreshed_at,
+    u.actor_id as creator_actor_id,
+    u.local as creator_local,
+    u.name as creator_name,
+    u.preferred_username as creator_preferred_username,
+    u.avatar as creator_avatar,
+    cat.name as category_name,
+    coalesce(cf.subs, 0) as number_of_subscribers,
+    coalesce(cd.posts, 0) as number_of_posts,
+    coalesce(cd.comments, 0) as number_of_comments,
+    hot_rank(cf.subs, c.published) as hot_rank
+from community c
+left join user_ u on c.creator_id = u.id
+left join category cat on c.category_id = cat.id
+left join (
+    select
+        p.community_id,
+        count(distinct p.id) as posts,
+        count(distinct ct.id) as comments
+    from post p
+    join comment ct on p.id = ct.post_id
+    group by p.community_id
+) cd on cd.community_id = c.id
+left join (
+    select
+        community_id,
+        count(*) as subs
+    from community_follower
+    group by community_id
+) cf on cf.community_id = c.id;
+
+create view community_view as
+select
+    cv.*,
+    us.user as user_id,
+    us.is_subbed::bool as subscribed
+from community_aggregates_view cv
+cross join lateral (
+       select
+               u.id as user,
+               coalesce(cf.community_id, 0) as is_subbed
+       from user_ u
+       left join community_follower cf on u.id = cf.user_id and cf.community_id = cv.id
+) as us
+
+union all
+
+select
+    cv.*,
+    null as user_id,
+    null as subscribed
+from community_aggregates_view cv;
+
+-- The community fast table
+
+create table community_aggregates_fast as select * from community_aggregates_view;
+alter table community_aggregates_fast add primary key (id);
+
+create view community_fast_view as
+select
+ac.*,
+u.id as user_id,
+(select cf.id::boolean from community_follower cf where u.id = cf.user_id and ac.id = cf.community_id) as subscribed
+from user_ u
+cross join (
+  select
+  ca.*
+  from community_aggregates_fast ca
+) ac
+
+union all
+
+select
+caf.*,
+null as user_id,
+null as subscribed
+from community_aggregates_fast caf;
\ No newline at end of file
diff --git a/server/migrations/2020-08-06-205355_update_community_post_count/up.sql b/server/migrations/2020-08-06-205355_update_community_post_count/up.sql
new file mode 100644 (file)
index 0000000..de5d447
--- /dev/null
@@ -0,0 +1,100 @@
+-- Drop first
+drop view community_view;
+drop view community_aggregates_view;
+drop view community_fast_view;
+drop table community_aggregates_fast;
+
+create view community_aggregates_view as
+select
+    c.id,
+    c.name,
+    c.title,
+    c.icon,
+    c.banner,
+    c.description,
+    c.category_id,
+    c.creator_id,
+    c.removed,
+    c.published,
+    c.updated,
+    c.deleted,
+    c.nsfw,
+    c.actor_id,
+    c.local,
+    c.last_refreshed_at,
+    u.actor_id as creator_actor_id,
+    u.local as creator_local,
+    u.name as creator_name,
+    u.preferred_username as creator_preferred_username,
+    u.avatar as creator_avatar,
+    cat.name as category_name,
+    coalesce(cf.subs, 0) as number_of_subscribers,
+    coalesce(cd.posts, 0) as number_of_posts,
+    coalesce(cd.comments, 0) as number_of_comments,
+    hot_rank(cf.subs, c.published) as hot_rank
+from community c
+left join user_ u on c.creator_id = u.id
+left join category cat on c.category_id = cat.id
+left join (
+    select
+        p.community_id,
+        count(distinct p.id) as posts,
+        count(distinct ct.id) as comments
+    from post p
+    left join comment ct on p.id = ct.post_id
+    group by p.community_id
+) cd on cd.community_id = c.id
+left join (
+    select
+        community_id,
+        count(*) as subs
+    from community_follower
+    group by community_id
+) cf on cf.community_id = c.id;
+
+create view community_view as
+select
+    cv.*,
+    us.user as user_id,
+    us.is_subbed::bool as subscribed
+from community_aggregates_view cv
+cross join lateral (
+       select
+               u.id as user,
+               coalesce(cf.community_id, 0) as is_subbed
+       from user_ u
+       left join community_follower cf on u.id = cf.user_id and cf.community_id = cv.id
+) as us
+
+union all
+
+select
+    cv.*,
+    null as user_id,
+    null as subscribed
+from community_aggregates_view cv;
+
+-- The community fast table
+
+create table community_aggregates_fast as select * from community_aggregates_view;
+alter table community_aggregates_fast add primary key (id);
+
+create view community_fast_view as
+select
+ac.*,
+u.id as user_id,
+(select cf.id::boolean from community_follower cf where u.id = cf.user_id and ac.id = cf.community_id) as subscribed
+from user_ u
+cross join (
+  select
+  ca.*
+  from community_aggregates_fast ca
+) ac
+
+union all
+
+select
+caf.*,
+null as user_id,
+null as subscribed
+from community_aggregates_fast caf;
\ No newline at end of file
index eec9d1a71b4ab5920f609b28c3f73992bc8ce234..f475f1dfe763d4a8d8aa4bf5cdbeae78308c7118 100644 (file)
@@ -1,7 +1,6 @@
-use diesel::{result::Error, PgConnection};
 use jsonwebtoken::{decode, encode, DecodingKey, EncodingKey, Header, TokenData, Validation};
-use lemmy_db::{user::User_, Crud};
-use lemmy_utils::{is_email_regex, settings::Settings};
+use lemmy_db::user::User_;
+use lemmy_utils::settings::Settings;
 use serde::{Deserialize, Serialize};
 
 type Jwt = String;
@@ -9,15 +8,7 @@ type Jwt = String;
 #[derive(Debug, Serialize, Deserialize)]
 pub struct Claims {
   pub id: i32,
-  pub username: String,
   pub iss: String,
-  pub show_nsfw: bool,
-  pub theme: String,
-  pub default_sort_type: i16,
-  pub default_listing_type: i16,
-  pub lang: String,
-  pub avatar: Option<String>,
-  pub show_avatars: bool,
 }
 
 impl Claims {
@@ -33,41 +24,15 @@ impl Claims {
     )
   }
 
-  pub fn jwt(user: User_, hostname: String) -> Jwt {
+  pub fn jwt(user: User_, hostname: String) -> Result<Jwt, jsonwebtoken::errors::Error> {
     let my_claims = Claims {
       id: user.id,
-      username: user.name.to_owned(),
       iss: hostname,
-      show_nsfw: user.show_nsfw,
-      theme: user.theme.to_owned(),
-      default_sort_type: user.default_sort_type,
-      default_listing_type: user.default_listing_type,
-      lang: user.lang.to_owned(),
-      avatar: user.avatar.to_owned(),
-      show_avatars: user.show_avatars.to_owned(),
     };
     encode(
       &Header::default(),
       &my_claims,
       &EncodingKey::from_secret(Settings::get().jwt_secret.as_ref()),
     )
-    .unwrap()
-  }
-
-  // TODO: move these into user?
-  pub fn find_by_email_or_username(
-    conn: &PgConnection,
-    username_or_email: &str,
-  ) -> Result<User_, Error> {
-    if is_email_regex(username_or_email) {
-      User_::find_by_email(conn, username_or_email)
-    } else {
-      User_::find_by_username(conn, username_or_email)
-    }
-  }
-
-  pub fn find_by_jwt(conn: &PgConnection, jwt: &str) -> Result<User_, Error> {
-    let claims: Claims = Claims::decode(&jwt).expect("Invalid token").claims;
-    User_::read(&conn, claims.id)
   }
 }
index f8bdf5d5b17f3f708de2b72a05373ea5cdf8036b..3384993f887dd4115ab6a2c19ab4ce6fc185e6d8 100644 (file)
@@ -1,26 +1,33 @@
 use crate::{
-  api::{claims::Claims, APIError, Oper, Perform},
+  api::{
+    check_community_ban,
+    get_post,
+    get_user_from_jwt,
+    get_user_from_jwt_opt,
+    is_mod_or_admin,
+    APIError,
+    Perform,
+  },
   apub::{ApubLikeableType, ApubObjectType},
   blocking,
   websocket::{
     server::{JoinCommunityRoom, SendComment},
     UserOperation,
-    WebsocketInfo,
   },
+  ConnectionId,
   DbPool,
+  LemmyContext,
   LemmyError,
 };
+use actix_web::web::Data;
 use lemmy_db::{
   comment::*,
   comment_view::*,
-  community_view::*,
   moderator::*,
-  naive_now,
   post::*,
   site_view::*,
   user::*,
   user_mention::*,
-  user_view::*,
   Crud,
   Likeable,
   ListingType,
@@ -44,22 +51,38 @@ use std::str::FromStr;
 pub struct CreateComment {
   content: String,
   parent_id: Option<i32>,
-  edit_id: Option<i32>, // TODO this isn't used
   pub post_id: i32,
+  form_id: Option<String>,
   auth: String,
 }
 
 #[derive(Serialize, Deserialize)]
 pub struct EditComment {
   content: String,
-  parent_id: Option<i32>, // TODO why are the parent_id, creator_id, post_id, etc fields required? They aren't going to change
   edit_id: i32,
-  creator_id: i32,
-  pub post_id: i32,
-  removed: Option<bool>,
-  deleted: Option<bool>,
+  form_id: Option<String>,
+  auth: String,
+}
+
+#[derive(Serialize, Deserialize)]
+pub struct DeleteComment {
+  edit_id: i32,
+  deleted: bool,
+  auth: String,
+}
+
+#[derive(Serialize, Deserialize)]
+pub struct RemoveComment {
+  edit_id: i32,
+  removed: bool,
   reason: Option<String>,
-  read: Option<bool>,
+  auth: String,
+}
+
+#[derive(Serialize, Deserialize)]
+pub struct MarkCommentAsRead {
+  edit_id: i32,
+  read: bool,
   auth: String,
 }
 
@@ -74,12 +97,12 @@ pub struct SaveComment {
 pub struct CommentResponse {
   pub comment: CommentView,
   pub recipient_ids: Vec<i32>,
+  pub form_id: Option<String>,
 }
 
 #[derive(Serialize, Deserialize)]
 pub struct CreateCommentLike {
   comment_id: i32,
-  pub post_id: i32,
   score: i16,
   auth: String,
 }
@@ -100,22 +123,16 @@ pub struct GetCommentsResponse {
 }
 
 #[async_trait::async_trait(?Send)]
-impl Perform for Oper<CreateComment> {
+impl Perform for CreateComment {
   type Response = CommentResponse;
 
   async fn perform(
     &self,
-    pool: &DbPool,
-    websocket_info: Option<WebsocketInfo>,
+    context: &Data<LemmyContext>,
+    websocket_id: Option<ConnectionId>,
   ) -> Result<CommentResponse, LemmyError> {
-    let data: &CreateComment = &self.data;
-
-    let claims = match Claims::decode(&data.auth) {
-      Ok(claims) => claims.claims,
-      Err(_e) => return Err(APIError::err("not_logged_in").into()),
-    };
-
-    let user_id = claims.id;
+    let data: &CreateComment = &self;
+    let user = get_user_from_jwt(&data.auth, context.pool()).await?;
 
     let content_slurs_removed = remove_slurs(&data.content.to_owned());
 
@@ -123,7 +140,7 @@ impl Perform for Oper<CreateComment> {
       content: content_slurs_removed,
       parent_id: data.parent_id.to_owned(),
       post_id: data.post_id,
-      creator_id: user_id,
+      creator_id: user.id,
       removed: None,
       deleted: None,
       read: None,
@@ -135,30 +152,29 @@ impl Perform for Oper<CreateComment> {
 
     // Check for a community ban
     let post_id = data.post_id;
-    let post = blocking(pool, move |conn| Post::read(conn, post_id)).await??;
+    let post = get_post(post_id, context.pool()).await?;
 
-    let community_id = post.community_id;
-    let is_banned =
-      move |conn: &'_ _| CommunityUserBanView::get(conn, user_id, community_id).is_ok();
-    if blocking(pool, is_banned).await? {
-      return Err(APIError::err("community_ban").into());
-    }
+    check_community_ban(user.id, post.community_id, context.pool()).await?;
 
-    // Check for a site ban
-    let user = blocking(pool, move |conn| User_::read(&conn, user_id)).await??;
-    if user.banned {
-      return Err(APIError::err("site_ban").into());
+    // Check if post is locked, no new comments
+    if post.locked {
+      return Err(APIError::err("locked").into());
     }
 
+    // Create the comment
     let comment_form2 = comment_form.clone();
-    let inserted_comment =
-      match blocking(pool, move |conn| Comment::create(&conn, &comment_form2)).await? {
-        Ok(comment) => comment,
-        Err(_e) => return Err(APIError::err("couldnt_create_comment").into()),
-      };
+    let inserted_comment = match blocking(context.pool(), move |conn| {
+      Comment::create(&conn, &comment_form2)
+    })
+    .await?
+    {
+      Ok(comment) => comment,
+      Err(_e) => return Err(APIError::err("couldnt_create_comment").into()),
+    };
 
+    // Necessary to update the ap_id
     let inserted_comment_id = inserted_comment.id;
-    let updated_comment: Comment = match blocking(pool, move |conn| {
+    let updated_comment: Comment = match blocking(context.pool(), move |conn| {
       let apub_id =
         make_apub_endpoint(EndpointType::Comment, &inserted_comment_id.to_string()).to_string();
       Comment::update_ap_id(&conn, inserted_comment_id, apub_id)
@@ -169,31 +185,37 @@ impl Perform for Oper<CreateComment> {
       Err(_e) => return Err(APIError::err("couldnt_create_comment").into()),
     };
 
-    updated_comment
-      .send_create(&user, &self.client, pool)
-      .await?;
+    updated_comment.send_create(&user, context).await?;
 
     // Scan the comment for user mentions, add those rows
     let mentions = scrape_text_for_mentions(&comment_form.content);
-    let recipient_ids =
-      send_local_notifs(mentions, updated_comment.clone(), user.clone(), post, pool).await?;
+    let recipient_ids = send_local_notifs(
+      mentions,
+      updated_comment.clone(),
+      &user,
+      post,
+      context.pool(),
+      true,
+    )
+    .await?;
 
     // You like your own comment by default
     let like_form = CommentLikeForm {
       comment_id: inserted_comment.id,
       post_id: data.post_id,
-      user_id,
+      user_id: user.id,
       score: 1,
     };
 
     let like = move |conn: &'_ _| CommentLike::like(&conn, &like_form);
-    if blocking(pool, like).await?.is_err() {
+    if blocking(context.pool(), like).await?.is_err() {
       return Err(APIError::err("couldnt_like_comment").into());
     }
 
-    updated_comment.send_like(&user, &self.client, pool).await?;
+    updated_comment.send_like(&user, context).await?;
 
-    let comment_view = blocking(pool, move |conn| {
+    let user_id = user.id;
+    let comment_view = blocking(context.pool(), move |conn| {
       CommentView::read(&conn, inserted_comment.id, Some(user_id))
     })
     .await??;
@@ -201,158 +223,218 @@ impl Perform for Oper<CreateComment> {
     let mut res = CommentResponse {
       comment: comment_view,
       recipient_ids,
+      form_id: data.form_id.to_owned(),
     };
 
-    if let Some(ws) = websocket_info {
-      ws.chatserver.do_send(SendComment {
-        op: UserOperation::CreateComment,
-        comment: res.clone(),
-        my_id: ws.id,
-      });
+    context.chat_server().do_send(SendComment {
+      op: UserOperation::CreateComment,
+      comment: res.clone(),
+      websocket_id,
+    });
 
-      // strip out the recipient_ids, so that
-      // users don't get double notifs
-      res.recipient_ids = Vec::new();
-    }
+    // strip out the recipient_ids, so that
+    // users don't get double notifs
+    res.recipient_ids = Vec::new();
 
     Ok(res)
   }
 }
 
 #[async_trait::async_trait(?Send)]
-impl Perform for Oper<EditComment> {
+impl Perform for EditComment {
   type Response = CommentResponse;
 
   async fn perform(
     &self,
-    pool: &DbPool,
-    websocket_info: Option<WebsocketInfo>,
+    context: &Data<LemmyContext>,
+    websocket_id: Option<ConnectionId>,
   ) -> Result<CommentResponse, LemmyError> {
-    let data: &EditComment = &self.data;
+    let data: &EditComment = &self;
+    let user = get_user_from_jwt(&data.auth, context.pool()).await?;
+
+    let edit_id = data.edit_id;
+    let orig_comment = blocking(context.pool(), move |conn| {
+      CommentView::read(&conn, edit_id, None)
+    })
+    .await??;
+
+    check_community_ban(user.id, orig_comment.community_id, context.pool()).await?;
+
+    // Verify that only the creator can edit
+    if user.id != orig_comment.creator_id {
+      return Err(APIError::err("no_comment_edit_allowed").into());
+    }
 
-    let claims = match Claims::decode(&data.auth) {
-      Ok(claims) => claims.claims,
-      Err(_e) => return Err(APIError::err("not_logged_in").into()),
+    // Do the update
+    let content_slurs_removed = remove_slurs(&data.content.to_owned());
+    let edit_id = data.edit_id;
+    let updated_comment = match blocking(context.pool(), move |conn| {
+      Comment::update_content(conn, edit_id, &content_slurs_removed)
+    })
+    .await?
+    {
+      Ok(comment) => comment,
+      Err(_e) => return Err(APIError::err("couldnt_update_comment").into()),
     };
 
-    let user_id = claims.id;
+    // Send the apub update
+    updated_comment.send_update(&user, context).await?;
+
+    // Do the mentions / recipients
+    let post_id = orig_comment.post_id;
+    let post = get_post(post_id, context.pool()).await?;
+
+    let updated_comment_content = updated_comment.content.to_owned();
+    let mentions = scrape_text_for_mentions(&updated_comment_content);
+    let recipient_ids = send_local_notifs(
+      mentions,
+      updated_comment,
+      &user,
+      post,
+      context.pool(),
+      false,
+    )
+    .await?;
 
-    let user = blocking(pool, move |conn| User_::read(&conn, user_id)).await??;
+    let edit_id = data.edit_id;
+    let user_id = user.id;
+    let comment_view = blocking(context.pool(), move |conn| {
+      CommentView::read(conn, edit_id, Some(user_id))
+    })
+    .await??;
+
+    let mut res = CommentResponse {
+      comment: comment_view,
+      recipient_ids,
+      form_id: data.form_id.to_owned(),
+    };
+
+    context.chat_server().do_send(SendComment {
+      op: UserOperation::EditComment,
+      comment: res.clone(),
+      websocket_id,
+    });
+
+    // strip out the recipient_ids, so that
+    // users don't get double notifs
+    res.recipient_ids = Vec::new();
+
+    Ok(res)
+  }
+}
+
+#[async_trait::async_trait(?Send)]
+impl Perform for DeleteComment {
+  type Response = CommentResponse;
+
+  async fn perform(
+    &self,
+    context: &Data<LemmyContext>,
+    websocket_id: Option<ConnectionId>,
+  ) -> Result<CommentResponse, LemmyError> {
+    let data: &DeleteComment = &self;
+    let user = get_user_from_jwt(&data.auth, context.pool()).await?;
 
     let edit_id = data.edit_id;
-    let orig_comment =
-      blocking(pool, move |conn| CommentView::read(&conn, edit_id, None)).await??;
-
-    let mut editors: Vec<i32> = vec![orig_comment.creator_id];
-    let mut moderators: Vec<i32> = vec![];
-
-    let community_id = orig_comment.community_id;
-    moderators.append(
-      &mut blocking(pool, move |conn| {
-        CommunityModeratorView::for_community(&conn, community_id)
-          .map(|v| v.into_iter().map(|m| m.user_id).collect())
-      })
-      .await??,
-    );
-    moderators.append(
-      &mut blocking(pool, move |conn| {
-        UserView::admins(conn).map(|v| v.into_iter().map(|a| a.id).collect())
-      })
-      .await??,
-    );
-
-    editors.extend(&moderators);
-    // You are allowed to mark the comment as read even if you're banned.
-    if data.read.is_none() {
-      // Verify its the creator or a mod, or an admin
-
-      if !editors.contains(&user_id) {
-        return Err(APIError::err("no_comment_edit_allowed").into());
-      }
+    let orig_comment = blocking(context.pool(), move |conn| {
+      CommentView::read(&conn, edit_id, None)
+    })
+    .await??;
 
-      // Check for a community ban
-      let community_id = orig_comment.community_id;
-      let is_banned =
-        move |conn: &'_ _| CommunityUserBanView::get(conn, user_id, community_id).is_ok();
-      if blocking(pool, is_banned).await? {
-        return Err(APIError::err("community_ban").into());
-      }
+    check_community_ban(user.id, orig_comment.community_id, context.pool()).await?;
 
-      // Check for a site ban
-      if user.banned {
-        return Err(APIError::err("site_ban").into());
-      }
-    } else {
-      // check that user can mark as read
-      let parent_id = orig_comment.parent_id;
-      match parent_id {
-        Some(pid) => {
-          let parent_comment =
-            blocking(pool, move |conn| CommentView::read(&conn, pid, None)).await??;
-          if user_id != parent_comment.creator_id {
-            return Err(APIError::err("no_comment_edit_allowed").into());
-          }
-        }
-        None => {
-          let parent_post_id = orig_comment.post_id;
-          let parent_post = blocking(pool, move |conn| Post::read(conn, parent_post_id)).await??;
-          if user_id != parent_post.creator_id {
-            return Err(APIError::err("no_comment_edit_allowed").into());
-          }
-        }
-      }
+    // Verify that only the creator can delete
+    if user.id != orig_comment.creator_id {
+      return Err(APIError::err("no_comment_edit_allowed").into());
     }
 
-    let content_slurs_removed = remove_slurs(&data.content.to_owned());
+    // Do the delete
+    let deleted = data.deleted;
+    let updated_comment = match blocking(context.pool(), move |conn| {
+      Comment::update_deleted(conn, edit_id, deleted)
+    })
+    .await?
+    {
+      Ok(comment) => comment,
+      Err(_e) => return Err(APIError::err("couldnt_update_comment").into()),
+    };
+
+    // Send the apub message
+    if deleted {
+      updated_comment.send_delete(&user, context).await?;
+    } else {
+      updated_comment.send_undo_delete(&user, context).await?;
+    }
 
+    // Refetch it
     let edit_id = data.edit_id;
-    let read_comment = blocking(pool, move |conn| Comment::read(conn, edit_id)).await??;
-
-    let comment_form = {
-      if data.read.is_none() {
-        // the ban etc checks should been made and have passed
-        // the comment can be properly edited
-        let post_removed = if moderators.contains(&user_id) {
-          data.removed
-        } else {
-          Some(read_comment.removed)
-        };
-
-        CommentForm {
-          content: content_slurs_removed,
-          parent_id: read_comment.parent_id,
-          post_id: read_comment.post_id,
-          creator_id: read_comment.creator_id,
-          removed: post_removed.to_owned(),
-          deleted: data.deleted.to_owned(),
-          read: Some(read_comment.read),
-          published: None,
-          updated: Some(naive_now()),
-          ap_id: read_comment.ap_id,
-          local: read_comment.local,
-        }
-      } else {
-        // the only field that can be updated it the read field
-        CommentForm {
-          content: read_comment.content,
-          parent_id: read_comment.parent_id,
-          post_id: read_comment.post_id,
-          creator_id: read_comment.creator_id,
-          removed: Some(read_comment.removed).to_owned(),
-          deleted: Some(read_comment.deleted).to_owned(),
-          read: data.read.to_owned(),
-          published: None,
-          updated: orig_comment.updated,
-          ap_id: read_comment.ap_id,
-          local: read_comment.local,
-        }
-      }
+    let user_id = user.id;
+    let comment_view = blocking(context.pool(), move |conn| {
+      CommentView::read(conn, edit_id, Some(user_id))
+    })
+    .await??;
+
+    // Build the recipients
+    let post_id = comment_view.post_id;
+    let post = get_post(post_id, context.pool()).await?;
+    let mentions = vec![];
+    let recipient_ids = send_local_notifs(
+      mentions,
+      updated_comment,
+      &user,
+      post,
+      context.pool(),
+      false,
+    )
+    .await?;
+
+    let mut res = CommentResponse {
+      comment: comment_view,
+      recipient_ids,
+      form_id: None,
     };
 
+    context.chat_server().do_send(SendComment {
+      op: UserOperation::DeleteComment,
+      comment: res.clone(),
+      websocket_id,
+    });
+
+    // strip out the recipient_ids, so that
+    // users don't get double notifs
+    res.recipient_ids = Vec::new();
+
+    Ok(res)
+  }
+}
+
+#[async_trait::async_trait(?Send)]
+impl Perform for RemoveComment {
+  type Response = CommentResponse;
+
+  async fn perform(
+    &self,
+    context: &Data<LemmyContext>,
+    websocket_id: Option<ConnectionId>,
+  ) -> Result<CommentResponse, LemmyError> {
+    let data: &RemoveComment = &self;
+    let user = get_user_from_jwt(&data.auth, context.pool()).await?;
+
     let edit_id = data.edit_id;
-    let comment_form2 = comment_form.clone();
-    let updated_comment = match blocking(pool, move |conn| {
-      Comment::update(conn, edit_id, &comment_form2)
+    let orig_comment = blocking(context.pool(), move |conn| {
+      CommentView::read(&conn, edit_id, None)
+    })
+    .await??;
+
+    check_community_ban(user.id, orig_comment.community_id, context.pool()).await?;
+
+    // Verify that only a mod or admin can remove
+    is_mod_or_admin(context.pool(), user.id, orig_comment.community_id).await?;
+
+    // Do the remove
+    let removed = data.removed;
+    let updated_comment = match blocking(context.pool(), move |conn| {
+      Comment::update_removed(conn, edit_id, removed)
     })
     .await?
     {
@@ -360,119 +442,171 @@ impl Perform for Oper<EditComment> {
       Err(_e) => return Err(APIError::err("couldnt_update_comment").into()),
     };
 
-    if data.read.is_none() {
-      if let Some(deleted) = data.deleted.to_owned() {
-        if deleted {
-          updated_comment
-            .send_delete(&user, &self.client, pool)
-            .await?;
-        } else {
-          updated_comment
-            .send_undo_delete(&user, &self.client, pool)
-            .await?;
-        }
-      } else if let Some(removed) = data.removed.to_owned() {
-        if moderators.contains(&user_id) {
-          if removed {
-            updated_comment
-              .send_remove(&user, &self.client, pool)
-              .await?;
-          } else {
-            updated_comment
-              .send_undo_remove(&user, &self.client, pool)
-              .await?;
-          }
-        }
-      } else {
-        updated_comment
-          .send_update(&user, &self.client, pool)
-          .await?;
-      }
+    // Mod tables
+    let form = ModRemoveCommentForm {
+      mod_user_id: user.id,
+      comment_id: data.edit_id,
+      removed: Some(removed),
+      reason: data.reason.to_owned(),
+    };
+    blocking(context.pool(), move |conn| {
+      ModRemoveComment::create(conn, &form)
+    })
+    .await??;
 
-      // Mod tables
-      if moderators.contains(&user_id) {
-        if let Some(removed) = data.removed.to_owned() {
-          let form = ModRemoveCommentForm {
-            mod_user_id: user_id,
-            comment_id: data.edit_id,
-            removed: Some(removed),
-            reason: data.reason.to_owned(),
-          };
-          blocking(pool, move |conn| ModRemoveComment::create(conn, &form)).await??;
-        }
-      }
+    // Send the apub message
+    if removed {
+      updated_comment.send_remove(&user, context).await?;
+    } else {
+      updated_comment.send_undo_remove(&user, context).await?;
     }
 
-    let post_id = data.post_id;
-    let post = blocking(pool, move |conn| Post::read(conn, post_id)).await??;
-
-    let mentions = scrape_text_for_mentions(&comment_form.content);
-    let recipient_ids = send_local_notifs(mentions, updated_comment, user, post, pool).await?;
-
+    // Refetch it
     let edit_id = data.edit_id;
-    let comment_view = blocking(pool, move |conn| {
+    let user_id = user.id;
+    let comment_view = blocking(context.pool(), move |conn| {
       CommentView::read(conn, edit_id, Some(user_id))
     })
     .await??;
 
+    // Build the recipients
+    let post_id = comment_view.post_id;
+    let post = get_post(post_id, context.pool()).await?;
+    let mentions = vec![];
+    let recipient_ids = send_local_notifs(
+      mentions,
+      updated_comment,
+      &user,
+      post,
+      context.pool(),
+      false,
+    )
+    .await?;
+
     let mut res = CommentResponse {
       comment: comment_view,
       recipient_ids,
+      form_id: None,
     };
 
-    if let Some(ws) = websocket_info {
-      ws.chatserver.do_send(SendComment {
-        op: UserOperation::EditComment,
-        comment: res.clone(),
-        my_id: ws.id,
-      });
+    context.chat_server().do_send(SendComment {
+      op: UserOperation::RemoveComment,
+      comment: res.clone(),
+      websocket_id,
+    });
 
-      // strip out the recipient_ids, so that
-      // users don't get double notifs
-      res.recipient_ids = Vec::new();
-    }
+    // strip out the recipient_ids, so that
+    // users don't get double notifs
+    res.recipient_ids = Vec::new();
 
     Ok(res)
   }
 }
 
 #[async_trait::async_trait(?Send)]
-impl Perform for Oper<SaveComment> {
+impl Perform for MarkCommentAsRead {
   type Response = CommentResponse;
 
   async fn perform(
     &self,
-    pool: &DbPool,
-    _websocket_info: Option<WebsocketInfo>,
+    context: &Data<LemmyContext>,
+    _websocket_id: Option<ConnectionId>,
   ) -> Result<CommentResponse, LemmyError> {
-    let data: &SaveComment = &self.data;
+    let data: &MarkCommentAsRead = &self;
+    let user = get_user_from_jwt(&data.auth, context.pool()).await?;
+
+    let edit_id = data.edit_id;
+    let orig_comment = blocking(context.pool(), move |conn| {
+      CommentView::read(&conn, edit_id, None)
+    })
+    .await??;
+
+    check_community_ban(user.id, orig_comment.community_id, context.pool()).await?;
+
+    // Verify that only the recipient can mark as read
+    // Needs to fetch the parent comment / post to get the recipient
+    let parent_id = orig_comment.parent_id;
+    match parent_id {
+      Some(pid) => {
+        let parent_comment = blocking(context.pool(), move |conn| {
+          CommentView::read(&conn, pid, None)
+        })
+        .await??;
+        if user.id != parent_comment.creator_id {
+          return Err(APIError::err("no_comment_edit_allowed").into());
+        }
+      }
+      None => {
+        let parent_post_id = orig_comment.post_id;
+        let parent_post =
+          blocking(context.pool(), move |conn| Post::read(conn, parent_post_id)).await??;
+        if user.id != parent_post.creator_id {
+          return Err(APIError::err("no_comment_edit_allowed").into());
+        }
+      }
+    }
+
+    // Do the mark as read
+    let read = data.read;
+    match blocking(context.pool(), move |conn| {
+      Comment::update_read(conn, edit_id, read)
+    })
+    .await?
+    {
+      Ok(comment) => comment,
+      Err(_e) => return Err(APIError::err("couldnt_update_comment").into()),
+    };
+
+    // Refetch it
+    let edit_id = data.edit_id;
+    let user_id = user.id;
+    let comment_view = blocking(context.pool(), move |conn| {
+      CommentView::read(conn, edit_id, Some(user_id))
+    })
+    .await??;
 
-    let claims = match Claims::decode(&data.auth) {
-      Ok(claims) => claims.claims,
-      Err(_e) => return Err(APIError::err("not_logged_in").into()),
+    let res = CommentResponse {
+      comment: comment_view,
+      recipient_ids: Vec::new(),
+      form_id: None,
     };
 
-    let user_id = claims.id;
+    Ok(res)
+  }
+}
+
+#[async_trait::async_trait(?Send)]
+impl Perform for SaveComment {
+  type Response = CommentResponse;
+
+  async fn perform(
+    &self,
+    context: &Data<LemmyContext>,
+    _websocket_id: Option<ConnectionId>,
+  ) -> Result<CommentResponse, LemmyError> {
+    let data: &SaveComment = &self;
+    let user = get_user_from_jwt(&data.auth, context.pool()).await?;
 
     let comment_saved_form = CommentSavedForm {
       comment_id: data.comment_id,
-      user_id,
+      user_id: user.id,
     };
 
     if data.save {
       let save_comment = move |conn: &'_ _| CommentSaved::save(conn, &comment_saved_form);
-      if blocking(pool, save_comment).await?.is_err() {
+      if blocking(context.pool(), save_comment).await?.is_err() {
         return Err(APIError::err("couldnt_save_comment").into());
       }
     } else {
       let unsave_comment = move |conn: &'_ _| CommentSaved::unsave(conn, &comment_saved_form);
-      if blocking(pool, unsave_comment).await?.is_err() {
+      if blocking(context.pool(), unsave_comment).await?.is_err() {
         return Err(APIError::err("couldnt_save_comment").into());
       }
     }
 
     let comment_id = data.comment_id;
-    let comment_view = blocking(pool, move |conn| {
+    let user_id = user.id;
+    let comment_view = blocking(context.pool(), move |conn| {
       CommentView::read(conn, comment_id, Some(user_id))
     })
     .await??;
@@ -480,63 +614,53 @@ impl Perform for Oper<SaveComment> {
     Ok(CommentResponse {
       comment: comment_view,
       recipient_ids: Vec::new(),
+      form_id: None,
     })
   }
 }
 
 #[async_trait::async_trait(?Send)]
-impl Perform for Oper<CreateCommentLike> {
+impl Perform for CreateCommentLike {
   type Response = CommentResponse;
 
   async fn perform(
     &self,
-    pool: &DbPool,
-    websocket_info: Option<WebsocketInfo>,
+    context: &Data<LemmyContext>,
+    websocket_id: Option<ConnectionId>,
   ) -> Result<CommentResponse, LemmyError> {
-    let data: &CreateCommentLike = &self.data;
-
-    let claims = match Claims::decode(&data.auth) {
-      Ok(claims) => claims.claims,
-      Err(_e) => return Err(APIError::err("not_logged_in").into()),
-    };
-
-    let user_id = claims.id;
+    let data: &CreateCommentLike = &self;
+    let user = get_user_from_jwt(&data.auth, context.pool()).await?;
 
     let mut recipient_ids = Vec::new();
 
     // Don't do a downvote if site has downvotes disabled
     if data.score == -1 {
-      let site = blocking(pool, move |conn| SiteView::read(conn)).await??;
+      let site = blocking(context.pool(), move |conn| SiteView::read(conn)).await??;
       if !site.enable_downvotes {
         return Err(APIError::err("downvotes_disabled").into());
       }
     }
 
-    // Check for a community ban
-    let post_id = data.post_id;
-    let post = blocking(pool, move |conn| Post::read(conn, post_id)).await??;
-    let community_id = post.community_id;
-    let is_banned =
-      move |conn: &'_ _| CommunityUserBanView::get(conn, user_id, community_id).is_ok();
-    if blocking(pool, is_banned).await? {
-      return Err(APIError::err("community_ban").into());
-    }
+    let comment_id = data.comment_id;
+    let orig_comment = blocking(context.pool(), move |conn| {
+      CommentView::read(&conn, comment_id, None)
+    })
+    .await??;
 
-    // Check for a site ban
-    let user = blocking(pool, move |conn| User_::read(conn, user_id)).await??;
-    if user.banned {
-      return Err(APIError::err("site_ban").into());
-    }
+    let post_id = orig_comment.post_id;
+    let post = get_post(post_id, context.pool()).await?;
+    check_community_ban(user.id, post.community_id, context.pool()).await?;
 
     let comment_id = data.comment_id;
-    let comment = blocking(pool, move |conn| Comment::read(conn, comment_id)).await??;
+    let comment = blocking(context.pool(), move |conn| Comment::read(conn, comment_id)).await??;
 
     // Add to recipient ids
     match comment.parent_id {
       Some(parent_id) => {
-        let parent_comment = blocking(pool, move |conn| Comment::read(conn, parent_id)).await??;
-        if parent_comment.creator_id != user_id {
-          let parent_user = blocking(pool, move |conn| {
+        let parent_comment =
+          blocking(context.pool(), move |conn| Comment::read(conn, parent_id)).await??;
+        if parent_comment.creator_id != user.id {
+          let parent_user = blocking(context.pool(), move |conn| {
             User_::read(conn, parent_comment.creator_id)
           })
           .await??;
@@ -550,36 +674,40 @@ impl Perform for Oper<CreateCommentLike> {
 
     let like_form = CommentLikeForm {
       comment_id: data.comment_id,
-      post_id: data.post_id,
-      user_id,
+      post_id,
+      user_id: user.id,
       score: data.score,
     };
 
     // Remove any likes first
-    let like_form2 = like_form.clone();
-    blocking(pool, move |conn| CommentLike::remove(conn, &like_form2)).await??;
+    let user_id = user.id;
+    blocking(context.pool(), move |conn| {
+      CommentLike::remove(conn, user_id, comment_id)
+    })
+    .await??;
 
     // Only add the like if the score isnt 0
     let do_add = like_form.score != 0 && (like_form.score == 1 || like_form.score == -1);
     if do_add {
       let like_form2 = like_form.clone();
       let like = move |conn: &'_ _| CommentLike::like(conn, &like_form2);
-      if blocking(pool, like).await?.is_err() {
+      if blocking(context.pool(), like).await?.is_err() {
         return Err(APIError::err("couldnt_like_comment").into());
       }
 
       if like_form.score == 1 {
-        comment.send_like(&user, &self.client, pool).await?;
+        comment.send_like(&user, context).await?;
       } else if like_form.score == -1 {
-        comment.send_dislike(&user, &self.client, pool).await?;
+        comment.send_dislike(&user, context).await?;
       }
     } else {
-      comment.send_undo_like(&user, &self.client, pool).await?;
+      comment.send_undo_like(&user, context).await?;
     }
 
     // Have to refetch the comment to get the current state
     let comment_id = data.comment_id;
-    let liked_comment = blocking(pool, move |conn| {
+    let user_id = user.id;
+    let liked_comment = blocking(context.pool(), move |conn| {
       CommentView::read(conn, comment_id, Some(user_id))
     })
     .await??;
@@ -587,47 +715,35 @@ impl Perform for Oper<CreateCommentLike> {
     let mut res = CommentResponse {
       comment: liked_comment,
       recipient_ids,
+      form_id: None,
     };
 
-    if let Some(ws) = websocket_info {
-      ws.chatserver.do_send(SendComment {
-        op: UserOperation::CreateCommentLike,
-        comment: res.clone(),
-        my_id: ws.id,
-      });
+    context.chat_server().do_send(SendComment {
+      op: UserOperation::CreateCommentLike,
+      comment: res.clone(),
+      websocket_id,
+    });
 
-      // strip out the recipient_ids, so that
-      // users don't get double notifs
-      res.recipient_ids = Vec::new();
-    }
+    // strip out the recipient_ids, so that
+    // users don't get double notifs
+    res.recipient_ids = Vec::new();
 
     Ok(res)
   }
 }
 
 #[async_trait::async_trait(?Send)]
-impl Perform for Oper<GetComments> {
+impl Perform for GetComments {
   type Response = GetCommentsResponse;
 
   async fn perform(
     &self,
-    pool: &DbPool,
-    websocket_info: Option<WebsocketInfo>,
+    context: &Data<LemmyContext>,
+    websocket_id: Option<ConnectionId>,
   ) -> Result<GetCommentsResponse, LemmyError> {
-    let data: &GetComments = &self.data;
-
-    let user_claims: Option<Claims> = match &data.auth {
-      Some(auth) => match Claims::decode(&auth) {
-        Ok(claims) => Some(claims.claims),
-        Err(_e) => None,
-      },
-      None => None,
-    };
-
-    let user_id = match &user_claims {
-      Some(claims) => Some(claims.id),
-      None => None,
-    };
+    let data: &GetComments = &self;
+    let user = get_user_from_jwt_opt(&data.auth, context.pool()).await?;
+    let user_id = user.map(|u| u.id);
 
     let type_ = ListingType::from_str(&data.type_)?;
     let sort = SortType::from_str(&data.sort)?;
@@ -635,7 +751,7 @@ impl Perform for Oper<GetComments> {
     let community_id = data.community_id;
     let page = data.page;
     let limit = data.limit;
-    let comments = blocking(pool, move |conn| {
+    let comments = blocking(context.pool(), move |conn| {
       CommentQueryBuilder::create(conn)
         .listing_type(type_)
         .sort(&sort)
@@ -651,17 +767,15 @@ impl Perform for Oper<GetComments> {
       Err(_) => return Err(APIError::err("couldnt_get_comments").into()),
     };
 
-    if let Some(ws) = websocket_info {
+    if let Some(id) = websocket_id {
       // You don't need to join the specific community room, bc this is already handled by
       // GetCommunity
       if data.community_id.is_none() {
-        if let Some(id) = ws.id {
-          // 0 is the "all" community
-          ws.chatserver.do_send(JoinCommunityRoom {
-            community_id: 0,
-            id,
-          });
-        }
+        // 0 is the "all" community
+        context.chat_server().do_send(JoinCommunityRoom {
+          community_id: 0,
+          id,
+        });
       }
     }
 
@@ -672,12 +786,14 @@ impl Perform for Oper<GetComments> {
 pub async fn send_local_notifs(
   mentions: Vec<MentionData>,
   comment: Comment,
-  user: User_,
+  user: &User_,
   post: Post,
   pool: &DbPool,
+  do_send_email: bool,
 ) -> Result<Vec<i32>, LemmyError> {
+  let user2 = user.clone();
   let ids = blocking(pool, move |conn| {
-    do_send_local_notifs(conn, &mentions, &comment, &user, &post)
+    do_send_local_notifs(conn, &mentions, &comment, &user2, &post, do_send_email)
   })
   .await?;
 
@@ -690,6 +806,7 @@ fn do_send_local_notifs(
   comment: &Comment,
   user: &User_,
   post: &Post,
+  do_send_email: bool,
 ) -> Vec<i32> {
   let mut recipient_ids = Vec::new();
   let hostname = &format!("https://{}", Settings::get().hostname);
@@ -720,7 +837,7 @@ fn do_send_local_notifs(
       };
 
       // Send an email to those users that have notifications on
-      if mention_user.send_notifications_to_email {
+      if do_send_email && mention_user.send_notifications_to_email {
         if let Some(mention_email) = mention_user.email {
           let subject = &format!("{} - Mentioned by {}", Settings::get().hostname, user.name,);
           let html = &format!(
@@ -744,7 +861,7 @@ fn do_send_local_notifs(
           if let Ok(parent_user) = User_::read(&conn, parent_comment.creator_id) {
             recipient_ids.push(parent_user.id);
 
-            if parent_user.send_notifications_to_email {
+            if do_send_email && parent_user.send_notifications_to_email {
               if let Some(comment_reply_email) = parent_user.email {
                 let subject = &format!("{} - Reply from {}", Settings::get().hostname, user.name,);
                 let html = &format!(
@@ -767,7 +884,7 @@ fn do_send_local_notifs(
         if let Ok(parent_user) = User_::read(&conn, post.creator_id) {
           recipient_ids.push(parent_user.id);
 
-          if parent_user.send_notifications_to_email {
+          if do_send_email && parent_user.send_notifications_to_email {
             if let Some(post_reply_email) = parent_user.email {
               let subject = &format!("{} - Reply from {}", Settings::get().hostname, user.name,);
               let html = &format!(
index e5063e0ff0af6093704387674a6d36f235ee6891..7b63c672691e7d07653aec321345e6a99446b7f8 100644 (file)
@@ -1,23 +1,33 @@
 use super::*;
 use crate::{
-  api::{claims::Claims, APIError, Oper, Perform},
+  api::{is_admin, is_mod_or_admin, APIError, Perform},
   apub::ActorType,
   blocking,
   websocket::{
-    server::{JoinCommunityRoom, SendCommunityRoomMessage},
+    server::{GetCommunityUsersOnline, JoinCommunityRoom, SendCommunityRoomMessage},
     UserOperation,
-    WebsocketInfo,
   },
-  DbPool,
+  ConnectionId,
+};
+use anyhow::Context;
+use lemmy_db::{
+  comment::Comment,
+  comment_view::CommentQueryBuilder,
+  diesel_option_overwrite,
+  naive_now,
+  post::Post,
+  Bannable,
+  Crud,
+  Followable,
+  Joinable,
+  SortType,
 };
-use lemmy_db::{naive_now, Bannable, Crud, Followable, Joinable, SortType};
 use lemmy_utils::{
   generate_actor_keypair,
   is_valid_community_name,
+  location_info,
   make_apub_endpoint,
   naive_from_unix,
-  slur_check,
-  slurs_vec_to_str,
   EndpointType,
 };
 use serde::{Deserialize, Serialize};
@@ -34,7 +44,6 @@ pub struct GetCommunity {
 pub struct GetCommunityResponse {
   pub community: CommunityView,
   pub moderators: Vec<CommunityModeratorView>,
-  pub admins: Vec<UserView>,
   pub online: usize,
 }
 
@@ -43,6 +52,8 @@ pub struct CreateCommunity {
   name: String,
   title: String,
   description: Option<String>,
+  icon: Option<String>,
+  banner: Option<String>,
   category_id: i32,
   nsfw: bool,
   auth: String,
@@ -71,6 +82,7 @@ pub struct BanFromCommunity {
   pub community_id: i32,
   user_id: i32,
   ban: bool,
+  remove_data: Option<bool>,
   reason: Option<String>,
   expires: Option<i64>,
   auth: String,
@@ -98,13 +110,26 @@ pub struct AddModToCommunityResponse {
 #[derive(Serialize, Deserialize)]
 pub struct EditCommunity {
   pub edit_id: i32,
-  name: String,
   title: String,
   description: Option<String>,
+  icon: Option<String>,
+  banner: Option<String>,
   category_id: i32,
-  removed: Option<bool>,
-  deleted: Option<bool>,
   nsfw: bool,
+  auth: String,
+}
+
+#[derive(Serialize, Deserialize)]
+pub struct DeleteCommunity {
+  pub edit_id: i32,
+  deleted: bool,
+  auth: String,
+}
+
+#[derive(Serialize, Deserialize)]
+pub struct RemoveCommunity {
+  pub edit_id: i32,
+  removed: bool,
   reason: Option<String>,
   expires: Option<i64>,
   auth: String,
@@ -135,38 +160,33 @@ pub struct TransferCommunity {
 }
 
 #[async_trait::async_trait(?Send)]
-impl Perform for Oper<GetCommunity> {
+impl Perform for GetCommunity {
   type Response = GetCommunityResponse;
 
   async fn perform(
     &self,
-    pool: &DbPool,
-    websocket_info: Option<WebsocketInfo>,
+    context: &Data<LemmyContext>,
+    websocket_id: Option<ConnectionId>,
   ) -> Result<GetCommunityResponse, LemmyError> {
-    let data: &GetCommunity = &self.data;
-
-    let user_id: Option<i32> = match &data.auth {
-      Some(auth) => match Claims::decode(&auth) {
-        Ok(claims) => {
-          let user_id = claims.claims.id;
-          Some(user_id)
-        }
-        Err(_e) => None,
-      },
-      None => None,
-    };
+    let data: &GetCommunity = &self;
+    let user = get_user_from_jwt_opt(&data.auth, context.pool()).await?;
+    let user_id = user.map(|u| u.id);
 
     let name = data.name.to_owned().unwrap_or_else(|| "main".to_string());
     let community = match data.id {
-      Some(id) => blocking(pool, move |conn| Community::read(conn, id)).await??,
-      None => match blocking(pool, move |conn| Community::read_from_name(conn, &name)).await? {
+      Some(id) => blocking(context.pool(), move |conn| Community::read(conn, id)).await??,
+      None => match blocking(context.pool(), move |conn| {
+        Community::read_from_name(conn, &name)
+      })
+      .await?
+      {
         Ok(community) => community,
         Err(_e) => return Err(APIError::err("couldnt_find_community").into()),
       },
     };
 
     let community_id = community.id;
-    let community_view = match blocking(pool, move |conn| {
+    let community_view = match blocking(context.pool(), move |conn| {
       CommunityView::read(conn, community_id, user_id)
     })
     .await?
@@ -176,7 +196,7 @@ impl Perform for Oper<GetCommunity> {
     };
 
     let community_id = community.id;
-    let moderators: Vec<CommunityModeratorView> = match blocking(pool, move |conn| {
+    let moderators: Vec<CommunityModeratorView> = match blocking(context.pool(), move |conn| {
       CommunityModeratorView::for_community(conn, community_id)
     })
     .await?
@@ -185,35 +205,21 @@ impl Perform for Oper<GetCommunity> {
       Err(_e) => return Err(APIError::err("couldnt_find_community").into()),
     };
 
-    let site = blocking(pool, move |conn| Site::read(conn, 1)).await??;
-    let site_creator_id = site.creator_id;
-    let mut admins = blocking(pool, move |conn| UserView::admins(conn)).await??;
-    let creator_index = admins.iter().position(|r| r.id == site_creator_id).unwrap();
-    let creator_user = admins.remove(creator_index);
-    admins.insert(0, creator_user);
-
-    let online = if let Some(ws) = websocket_info {
-      if let Some(id) = ws.id {
-        ws.chatserver.do_send(JoinCommunityRoom {
-          community_id: community.id,
-          id,
-        });
-      }
+    if let Some(id) = websocket_id {
+      context
+        .chat_server()
+        .do_send(JoinCommunityRoom { community_id, id });
+    }
 
-      // TODO
-      1
-    // let fut = async {
-    //   ws.chatserver.send(GetCommunityUsersOnline {community_id}).await.unwrap()
-    // };
-    // Runtime::new().unwrap().block_on(fut)
-    } else {
-      0
-    };
+    let online = context
+      .chat_server()
+      .send(GetCommunityUsersOnline { community_id })
+      .await
+      .unwrap_or(1);
 
     let res = GetCommunityResponse {
       community: community_view,
       moderators,
-      admins,
       online,
     };
 
@@ -223,45 +229,34 @@ impl Perform for Oper<GetCommunity> {
 }
 
 #[async_trait::async_trait(?Send)]
-impl Perform for Oper<CreateCommunity> {
+impl Perform for CreateCommunity {
   type Response = CommunityResponse;
 
   async fn perform(
     &self,
-    pool: &DbPool,
-    _websocket_info: Option<WebsocketInfo>,
+    context: &Data<LemmyContext>,
+    _websocket_id: Option<ConnectionId>,
   ) -> Result<CommunityResponse, LemmyError> {
-    let data: &CreateCommunity = &self.data;
-
-    let claims = match Claims::decode(&data.auth) {
-      Ok(claims) => claims.claims,
-      Err(_e) => return Err(APIError::err("not_logged_in").into()),
-    };
-
-    if let Err(slurs) = slur_check(&data.name) {
-      return Err(APIError::err(&slurs_vec_to_str(slurs)).into());
-    }
+    let data: &CreateCommunity = &self;
+    let user = get_user_from_jwt(&data.auth, context.pool()).await?;
 
-    if let Err(slurs) = slur_check(&data.title) {
-      return Err(APIError::err(&slurs_vec_to_str(slurs)).into());
-    }
-
-    if let Some(description) = &data.description {
-      if let Err(slurs) = slur_check(description) {
-        return Err(APIError::err(&slurs_vec_to_str(slurs)).into());
-      }
-    }
+    check_slurs(&data.name)?;
+    check_slurs(&data.title)?;
+    check_slurs_opt(&data.description)?;
 
     if !is_valid_community_name(&data.name) {
       return Err(APIError::err("invalid_community_name").into());
     }
 
-    let user_id = claims.id;
-
-    // Check for a site ban
-    let user_view = blocking(pool, move |conn| UserView::read(conn, user_id)).await??;
-    if user_view.banned {
-      return Err(APIError::err("site_ban").into());
+    // Double check for duplicate community actor_ids
+    let actor_id = make_apub_endpoint(EndpointType::Community, &data.name).to_string();
+    let actor_id_cloned = actor_id.to_owned();
+    let community_dupe = blocking(context.pool(), move |conn| {
+      Community::read_from_actor_id(conn, &actor_id_cloned)
+    })
+    .await?;
+    if community_dupe.is_ok() {
+      return Err(APIError::err("community_already_exists").into());
     }
 
     // When you create a community, make sure the user becomes a moderator and a follower
@@ -271,13 +266,15 @@ impl Perform for Oper<CreateCommunity> {
       name: data.name.to_owned(),
       title: data.title.to_owned(),
       description: data.description.to_owned(),
+      icon: Some(data.icon.to_owned()),
+      banner: Some(data.banner.to_owned()),
       category_id: data.category_id,
-      creator_id: user_id,
+      creator_id: user.id,
       removed: None,
       deleted: None,
       nsfw: data.nsfw,
       updated: None,
-      actor_id: make_apub_endpoint(EndpointType::Community, &data.name).to_string(),
+      actor_id,
       local: true,
       private_key: Some(keypair.private_key),
       public_key: Some(keypair.public_key),
@@ -285,33 +282,37 @@ impl Perform for Oper<CreateCommunity> {
       published: None,
     };
 
-    let inserted_community =
-      match blocking(pool, move |conn| Community::create(conn, &community_form)).await? {
-        Ok(community) => community,
-        Err(_e) => return Err(APIError::err("community_already_exists").into()),
-      };
+    let inserted_community = match blocking(context.pool(), move |conn| {
+      Community::create(conn, &community_form)
+    })
+    .await?
+    {
+      Ok(community) => community,
+      Err(_e) => return Err(APIError::err("community_already_exists").into()),
+    };
 
     let community_moderator_form = CommunityModeratorForm {
       community_id: inserted_community.id,
-      user_id,
+      user_id: user.id,
     };
 
     let join = move |conn: &'_ _| CommunityModerator::join(conn, &community_moderator_form);
-    if blocking(pool, join).await?.is_err() {
+    if blocking(context.pool(), join).await?.is_err() {
       return Err(APIError::err("community_moderator_already_exists").into());
     }
 
     let community_follower_form = CommunityFollowerForm {
       community_id: inserted_community.id,
-      user_id,
+      user_id: user.id,
     };
 
     let follow = move |conn: &'_ _| CommunityFollower::follow(conn, &community_follower_form);
-    if blocking(pool, follow).await?.is_err() {
+    if blocking(context.pool(), follow).await?.is_err() {
       return Err(APIError::err("community_follower_already_exists").into());
     }
 
-    let community_view = blocking(pool, move |conn| {
+    let user_id = user.id;
+    let community_view = blocking(context.pool(), move |conn| {
       CommunityView::read(conn, inserted_community.id, Some(user_id))
     })
     .await??;
@@ -323,78 +324,48 @@ impl Perform for Oper<CreateCommunity> {
 }
 
 #[async_trait::async_trait(?Send)]
-impl Perform for Oper<EditCommunity> {
+impl Perform for EditCommunity {
   type Response = CommunityResponse;
 
   async fn perform(
     &self,
-    pool: &DbPool,
-    websocket_info: Option<WebsocketInfo>,
+    context: &Data<LemmyContext>,
+    websocket_id: Option<ConnectionId>,
   ) -> Result<CommunityResponse, LemmyError> {
-    let data: &EditCommunity = &self.data;
-
-    if let Err(slurs) = slur_check(&data.name) {
-      return Err(APIError::err(&slurs_vec_to_str(slurs)).into());
-    }
-
-    if let Err(slurs) = slur_check(&data.title) {
-      return Err(APIError::err(&slurs_vec_to_str(slurs)).into());
-    }
-
-    if let Some(description) = &data.description {
-      if let Err(slurs) = slur_check(description) {
-        return Err(APIError::err(&slurs_vec_to_str(slurs)).into());
-      }
-    }
-
-    let claims = match Claims::decode(&data.auth) {
-      Ok(claims) => claims.claims,
-      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 data: &EditCommunity = &self;
+    let user = get_user_from_jwt(&data.auth, context.pool()).await?;
 
-    // Check for a site ban
-    let user = blocking(pool, move |conn| User_::read(conn, user_id)).await??;
-    if user.banned {
-      return Err(APIError::err("site_ban").into());
-    }
+    check_slurs(&data.title)?;
+    check_slurs_opt(&data.description)?;
 
-    // Verify its a mod
+    // Verify its a mod (only mods can edit it)
     let edit_id = data.edit_id;
-    let mut editors: Vec<i32> = Vec::new();
-    editors.append(
-      &mut blocking(pool, move |conn| {
-        CommunityModeratorView::for_community(conn, edit_id)
-          .map(|v| v.into_iter().map(|m| m.user_id).collect())
-      })
-      .await??,
-    );
-    editors.append(
-      &mut blocking(pool, move |conn| {
-        UserView::admins(conn).map(|v| v.into_iter().map(|a| a.id).collect())
-      })
-      .await??,
-    );
-    if !editors.contains(&user_id) {
-      return Err(APIError::err("no_community_edit_allowed").into());
+    let mods: Vec<i32> = blocking(context.pool(), move |conn| {
+      CommunityModeratorView::for_community(conn, edit_id)
+        .map(|v| v.into_iter().map(|m| m.user_id).collect())
+    })
+    .await??;
+    if !mods.contains(&user.id) {
+      return Err(APIError::err("not_a_moderator").into());
     }
 
     let edit_id = data.edit_id;
-    let read_community = blocking(pool, move |conn| Community::read(conn, edit_id)).await??;
+    let read_community =
+      blocking(context.pool(), move |conn| Community::read(conn, edit_id)).await??;
+
+    let icon = diesel_option_overwrite(&data.icon);
+    let banner = diesel_option_overwrite(&data.banner);
 
     let community_form = CommunityForm {
-      name: data.name.to_owned(),
+      name: read_community.name,
       title: data.title.to_owned(),
       description: data.description.to_owned(),
+      icon,
+      banner,
       category_id: data.category_id.to_owned(),
       creator_id: read_community.creator_id,
-      removed: data.removed.to_owned(),
-      deleted: data.deleted.to_owned(),
+      removed: Some(read_community.removed),
+      deleted: Some(read_community.deleted),
       nsfw: data.nsfw,
       updated: Some(naive_now()),
       actor_id: read_community.actor_id,
@@ -406,7 +377,7 @@ impl Perform for Oper<EditCommunity> {
     };
 
     let edit_id = data.edit_id;
-    let updated_community = match blocking(pool, move |conn| {
+    match blocking(context.pool(), move |conn| {
       Community::update(conn, edit_id, &community_form)
     })
     .await?
@@ -415,46 +386,68 @@ impl Perform for Oper<EditCommunity> {
       Err(_e) => return Err(APIError::err("couldnt_update_community").into()),
     };
 
-    // Mod tables
-    if let Some(removed) = data.removed.to_owned() {
-      let expires = match data.expires {
-        Some(time) => Some(naive_from_unix(time)),
-        None => None,
-      };
-      let form = ModRemoveCommunityForm {
-        mod_user_id: user_id,
-        community_id: data.edit_id,
-        removed: Some(removed),
-        reason: data.reason.to_owned(),
-        expires,
-      };
-      blocking(pool, move |conn| ModRemoveCommunity::create(conn, &form)).await??;
+    // TODO there needs to be some kind of an apub update
+    // process for communities and users
+
+    let edit_id = data.edit_id;
+    let user_id = user.id;
+    let community_view = blocking(context.pool(), move |conn| {
+      CommunityView::read(conn, edit_id, Some(user_id))
+    })
+    .await??;
+
+    let res = CommunityResponse {
+      community: community_view,
+    };
+
+    send_community_websocket(&res, context, websocket_id, UserOperation::EditCommunity);
+
+    Ok(res)
+  }
+}
+
+#[async_trait::async_trait(?Send)]
+impl Perform for DeleteCommunity {
+  type Response = CommunityResponse;
+
+  async fn perform(
+    &self,
+    context: &Data<LemmyContext>,
+    websocket_id: Option<ConnectionId>,
+  ) -> Result<CommunityResponse, LemmyError> {
+    let data: &DeleteCommunity = &self;
+    let user = get_user_from_jwt(&data.auth, context.pool()).await?;
+
+    // Verify its the creator (only a creator can delete the community)
+    let edit_id = data.edit_id;
+    let read_community =
+      blocking(context.pool(), move |conn| Community::read(conn, edit_id)).await??;
+    if read_community.creator_id != user.id {
+      return Err(APIError::err("no_community_edit_allowed").into());
     }
 
-    if let Some(deleted) = data.deleted.to_owned() {
-      if deleted {
-        updated_community
-          .send_delete(&user, &self.client, pool)
-          .await?;
-      } else {
-        updated_community
-          .send_undo_delete(&user, &self.client, pool)
-          .await?;
-      }
-    } else if let Some(removed) = data.removed.to_owned() {
-      if removed {
-        updated_community
-          .send_remove(&user, &self.client, pool)
-          .await?;
-      } else {
-        updated_community
-          .send_undo_remove(&user, &self.client, pool)
-          .await?;
-      }
+    // Do the delete
+    let edit_id = data.edit_id;
+    let deleted = data.deleted;
+    let updated_community = match blocking(context.pool(), move |conn| {
+      Community::update_deleted(conn, edit_id, deleted)
+    })
+    .await?
+    {
+      Ok(community) => community,
+      Err(_e) => return Err(APIError::err("couldnt_update_community").into()),
+    };
+
+    // Send apub messages
+    if deleted {
+      updated_community.send_delete(&user, context).await?;
+    } else {
+      updated_community.send_undo_delete(&user, context).await?;
     }
 
     let edit_id = data.edit_id;
-    let community_view = blocking(pool, move |conn| {
+    let user_id = user.id;
+    let community_view = blocking(context.pool(), move |conn| {
       CommunityView::read(conn, edit_id, Some(user_id))
     })
     .await??;
@@ -463,50 +456,99 @@ impl Perform for Oper<EditCommunity> {
       community: community_view,
     };
 
-    if let Some(ws) = websocket_info {
-      // Strip out the user id and subscribed when sending to others
-      let mut res_sent = res.clone();
-      res_sent.community.user_id = None;
-      res_sent.community.subscribed = None;
-
-      ws.chatserver.do_send(SendCommunityRoomMessage {
-        op: UserOperation::EditCommunity,
-        response: res_sent,
-        community_id: data.edit_id,
-        my_id: ws.id,
-      });
-    }
+    send_community_websocket(&res, context, websocket_id, UserOperation::DeleteCommunity);
 
     Ok(res)
   }
 }
 
 #[async_trait::async_trait(?Send)]
-impl Perform for Oper<ListCommunities> {
-  type Response = ListCommunitiesResponse;
+impl Perform for RemoveCommunity {
+  type Response = CommunityResponse;
 
   async fn perform(
     &self,
-    pool: &DbPool,
-    _websocket_info: Option<WebsocketInfo>,
-  ) -> Result<ListCommunitiesResponse, LemmyError> {
-    let data: &ListCommunities = &self.data;
+    context: &Data<LemmyContext>,
+    websocket_id: Option<ConnectionId>,
+  ) -> Result<CommunityResponse, LemmyError> {
+    let data: &RemoveCommunity = &self;
+    let user = get_user_from_jwt(&data.auth, context.pool()).await?;
 
-    let user_claims: Option<Claims> = match &data.auth {
-      Some(auth) => match Claims::decode(&auth) {
-        Ok(claims) => Some(claims.claims),
-        Err(_e) => None,
-      },
+    // Verify its an admin (only an admin can remove a community)
+    is_admin(context.pool(), user.id).await?;
+
+    // Do the remove
+    let edit_id = data.edit_id;
+    let removed = data.removed;
+    let updated_community = match blocking(context.pool(), move |conn| {
+      Community::update_removed(conn, edit_id, removed)
+    })
+    .await?
+    {
+      Ok(community) => community,
+      Err(_e) => return Err(APIError::err("couldnt_update_community").into()),
+    };
+
+    // Mod tables
+    let expires = match data.expires {
+      Some(time) => Some(naive_from_unix(time)),
       None => None,
     };
+    let form = ModRemoveCommunityForm {
+      mod_user_id: user.id,
+      community_id: data.edit_id,
+      removed: Some(removed),
+      reason: data.reason.to_owned(),
+      expires,
+    };
+    blocking(context.pool(), move |conn| {
+      ModRemoveCommunity::create(conn, &form)
+    })
+    .await??;
+
+    // Apub messages
+    if removed {
+      updated_community.send_remove(&user, context).await?;
+    } else {
+      updated_community.send_undo_remove(&user, context).await?;
+    }
+
+    let edit_id = data.edit_id;
+    let user_id = user.id;
+    let community_view = blocking(context.pool(), move |conn| {
+      CommunityView::read(conn, edit_id, Some(user_id))
+    })
+    .await??;
+
+    let res = CommunityResponse {
+      community: community_view,
+    };
 
-    let user_id = match &user_claims {
-      Some(claims) => Some(claims.id),
+    send_community_websocket(&res, context, websocket_id, UserOperation::RemoveCommunity);
+
+    Ok(res)
+  }
+}
+
+#[async_trait::async_trait(?Send)]
+impl Perform for ListCommunities {
+  type Response = ListCommunitiesResponse;
+
+  async fn perform(
+    &self,
+    context: &Data<LemmyContext>,
+    _websocket_id: Option<ConnectionId>,
+  ) -> Result<ListCommunitiesResponse, LemmyError> {
+    let data: &ListCommunities = &self;
+    let user = get_user_from_jwt_opt(&data.auth, context.pool()).await?;
+
+    let user_id = match &user {
+      Some(user) => Some(user.id),
       None => None,
     };
 
-    let show_nsfw = match &user_claims {
-      Some(claims) => claims.show_nsfw,
+    let show_nsfw = match &user {
+      Some(user) => user.show_nsfw,
       None => false,
     };
 
@@ -514,7 +556,7 @@ impl Perform for Oper<ListCommunities> {
 
     let page = data.page;
     let limit = data.limit;
-    let communities = blocking(pool, move |conn| {
+    let communities = blocking(context.pool(), move |conn| {
       CommunityQueryBuilder::create(conn)
         .sort(&sort)
         .for_user(user_id)
@@ -531,67 +573,56 @@ impl Perform for Oper<ListCommunities> {
 }
 
 #[async_trait::async_trait(?Send)]
-impl Perform for Oper<FollowCommunity> {
+impl Perform for FollowCommunity {
   type Response = CommunityResponse;
 
   async fn perform(
     &self,
-    pool: &DbPool,
-    _websocket_info: Option<WebsocketInfo>,
+    context: &Data<LemmyContext>,
+    _websocket_id: Option<ConnectionId>,
   ) -> Result<CommunityResponse, LemmyError> {
-    let data: &FollowCommunity = &self.data;
-
-    let claims = match Claims::decode(&data.auth) {
-      Ok(claims) => claims.claims,
-      Err(_e) => return Err(APIError::err("not_logged_in").into()),
-    };
-
-    let user_id = claims.id;
+    let data: &FollowCommunity = &self;
+    let user = get_user_from_jwt(&data.auth, context.pool()).await?;
 
     let community_id = data.community_id;
-    let community = blocking(pool, move |conn| Community::read(conn, community_id)).await??;
+    let community = blocking(context.pool(), move |conn| {
+      Community::read(conn, community_id)
+    })
+    .await??;
     let community_follower_form = CommunityFollowerForm {
       community_id: data.community_id,
-      user_id,
+      user_id: user.id,
     };
 
     if community.local {
       if data.follow {
         let follow = move |conn: &'_ _| CommunityFollower::follow(conn, &community_follower_form);
-        if blocking(pool, follow).await?.is_err() {
+        if blocking(context.pool(), follow).await?.is_err() {
           return Err(APIError::err("community_follower_already_exists").into());
         }
       } else {
         let unfollow =
           move |conn: &'_ _| CommunityFollower::unfollow(conn, &community_follower_form);
-        if blocking(pool, unfollow).await?.is_err() {
+        if blocking(context.pool(), unfollow).await?.is_err() {
           return Err(APIError::err("community_follower_already_exists").into());
         }
       }
+    } else if data.follow {
+      // Dont actually add to the community followers here, because you need
+      // to wait for the accept
+      user.send_follow(&community.actor_id()?, context).await?;
     } else {
-      let user = blocking(pool, move |conn| User_::read(conn, user_id)).await??;
-
-      if data.follow {
-        // Dont actually add to the community followers here, because you need
-        // to wait for the accept
-        user
-          .send_follow(&community.actor_id, &self.client, pool)
-          .await?;
-      } else {
-        user
-          .send_unfollow(&community.actor_id, &self.client, pool)
-          .await?;
-        let unfollow =
-          move |conn: &'_ _| CommunityFollower::unfollow(conn, &community_follower_form);
-        if blocking(pool, unfollow).await?.is_err() {
-          return Err(APIError::err("community_follower_already_exists").into());
-        }
+      user.send_unfollow(&community.actor_id()?, context).await?;
+      let unfollow = move |conn: &'_ _| CommunityFollower::unfollow(conn, &community_follower_form);
+      if blocking(context.pool(), unfollow).await?.is_err() {
+        return Err(APIError::err("community_follower_already_exists").into());
       }
-      // TODO: this needs to return a "pending" state, until Accept is received from the remote server
     }
+    // TODO: this needs to return a "pending" state, until Accept is received from the remote server
 
     let community_id = data.community_id;
-    let community_view = blocking(pool, move |conn| {
+    let user_id = user.id;
+    let community_view = blocking(context.pool(), move |conn| {
       CommunityView::read(conn, community_id, Some(user_id))
     })
     .await??;
@@ -603,24 +634,19 @@ impl Perform for Oper<FollowCommunity> {
 }
 
 #[async_trait::async_trait(?Send)]
-impl Perform for Oper<GetFollowedCommunities> {
+impl Perform for GetFollowedCommunities {
   type Response = GetFollowedCommunitiesResponse;
 
   async fn perform(
     &self,
-    pool: &DbPool,
-    _websocket_info: Option<WebsocketInfo>,
+    context: &Data<LemmyContext>,
+    _websocket_id: Option<ConnectionId>,
   ) -> Result<GetFollowedCommunitiesResponse, LemmyError> {
-    let data: &GetFollowedCommunities = &self.data;
+    let data: &GetFollowedCommunities = &self;
+    let user = get_user_from_jwt(&data.auth, context.pool()).await?;
 
-    let claims = match Claims::decode(&data.auth) {
-      Ok(claims) => claims.claims,
-      Err(_e) => return Err(APIError::err("not_logged_in").into()),
-    };
-
-    let user_id = claims.id;
-
-    let communities = match blocking(pool, move |conn| {
+    let user_id = user.id;
+    let communities = match blocking(context.pool(), move |conn| {
       CommunityFollowerView::for_user(conn, user_id)
     })
     .await?
@@ -635,44 +661,22 @@ impl Perform for Oper<GetFollowedCommunities> {
 }
 
 #[async_trait::async_trait(?Send)]
-impl Perform for Oper<BanFromCommunity> {
+impl Perform for BanFromCommunity {
   type Response = BanFromCommunityResponse;
 
   async fn perform(
     &self,
-    pool: &DbPool,
-    websocket_info: Option<WebsocketInfo>,
+    context: &Data<LemmyContext>,
+    websocket_id: Option<ConnectionId>,
   ) -> Result<BanFromCommunityResponse, LemmyError> {
-    let data: &BanFromCommunity = &self.data;
-
-    let claims = match Claims::decode(&data.auth) {
-      Ok(claims) => claims.claims,
-      Err(_e) => return Err(APIError::err("not_logged_in").into()),
-    };
-
-    let user_id = claims.id;
-
-    let mut community_moderators: Vec<i32> = vec![];
+    let data: &BanFromCommunity = &self;
+    let user = get_user_from_jwt(&data.auth, context.pool()).await?;
 
     let community_id = data.community_id;
+    let banned_user_id = data.user_id;
 
-    community_moderators.append(
-      &mut blocking(pool, move |conn| {
-        CommunityModeratorView::for_community(&conn, community_id)
-          .map(|v| v.into_iter().map(|m| m.user_id).collect())
-      })
-      .await??,
-    );
-    community_moderators.append(
-      &mut blocking(pool, move |conn| {
-        UserView::admins(conn).map(|v| v.into_iter().map(|a| a.id).collect())
-      })
-      .await??,
-    );
-
-    if !community_moderators.contains(&user_id) {
-      return Err(APIError::err("couldnt_update_community").into());
-    }
+    // Verify that only mods or admins can ban
+    is_mod_or_admin(context.pool(), user.id, community_id).await?;
 
     let community_user_ban_form = CommunityUserBanForm {
       community_id: data.community_id,
@@ -681,214 +685,210 @@ impl Perform for Oper<BanFromCommunity> {
 
     if data.ban {
       let ban = move |conn: &'_ _| CommunityUserBan::ban(conn, &community_user_ban_form);
-      if blocking(pool, ban).await?.is_err() {
+      if blocking(context.pool(), ban).await?.is_err() {
         return Err(APIError::err("community_user_already_banned").into());
       }
     } else {
       let unban = move |conn: &'_ _| CommunityUserBan::unban(conn, &community_user_ban_form);
-      if blocking(pool, unban).await?.is_err() {
+      if blocking(context.pool(), unban).await?.is_err() {
         return Err(APIError::err("community_user_already_banned").into());
       }
     }
 
+    // Remove/Restore their data if that's desired
+    if let Some(remove_data) = data.remove_data {
+      // Posts
+      blocking(context.pool(), move |conn: &'_ _| {
+        Post::update_removed_for_creator(conn, banned_user_id, Some(community_id), remove_data)
+      })
+      .await??;
+
+      // Comments
+      // Diesel doesn't allow updates with joins, so this has to be a loop
+      let comments = blocking(context.pool(), move |conn| {
+        CommentQueryBuilder::create(conn)
+          .for_creator_id(banned_user_id)
+          .for_community_id(community_id)
+          .limit(std::i64::MAX)
+          .list()
+      })
+      .await??;
+
+      for comment in &comments {
+        let comment_id = comment.id;
+        blocking(context.pool(), move |conn: &'_ _| {
+          Comment::update_removed(conn, comment_id, remove_data)
+        })
+        .await??;
+      }
+    }
+
     // Mod tables
+    // TODO eventually do correct expires
     let expires = match data.expires {
       Some(time) => Some(naive_from_unix(time)),
       None => None,
     };
 
     let form = ModBanFromCommunityForm {
-      mod_user_id: user_id,
+      mod_user_id: user.id,
       other_user_id: data.user_id,
       community_id: data.community_id,
       reason: data.reason.to_owned(),
       banned: Some(data.ban),
       expires,
     };
-    blocking(pool, move |conn| ModBanFromCommunity::create(conn, &form)).await??;
+    blocking(context.pool(), move |conn| {
+      ModBanFromCommunity::create(conn, &form)
+    })
+    .await??;
 
     let user_id = data.user_id;
-    let user_view = blocking(pool, move |conn| UserView::read(conn, user_id)).await??;
+    let user_view = blocking(context.pool(), move |conn| {
+      UserView::get_user_secure(conn, user_id)
+    })
+    .await??;
 
     let res = BanFromCommunityResponse {
       user: user_view,
       banned: data.ban,
     };
 
-    if let Some(ws) = websocket_info {
-      ws.chatserver.do_send(SendCommunityRoomMessage {
-        op: UserOperation::BanFromCommunity,
-        response: res.clone(),
-        community_id: data.community_id,
-        my_id: ws.id,
-      });
-    }
+    context.chat_server().do_send(SendCommunityRoomMessage {
+      op: UserOperation::BanFromCommunity,
+      response: res.clone(),
+      community_id,
+      websocket_id,
+    });
 
     Ok(res)
   }
 }
 
 #[async_trait::async_trait(?Send)]
-impl Perform for Oper<AddModToCommunity> {
+impl Perform for AddModToCommunity {
   type Response = AddModToCommunityResponse;
 
   async fn perform(
     &self,
-    pool: &DbPool,
-    websocket_info: Option<WebsocketInfo>,
+    context: &Data<LemmyContext>,
+    websocket_id: Option<ConnectionId>,
   ) -> Result<AddModToCommunityResponse, LemmyError> {
-    let data: &AddModToCommunity = &self.data;
-
-    let claims = match Claims::decode(&data.auth) {
-      Ok(claims) => claims.claims,
-      Err(_e) => return Err(APIError::err("not_logged_in").into()),
-    };
-
-    let user_id = claims.id;
+    let data: &AddModToCommunity = &self;
+    let user = get_user_from_jwt(&data.auth, context.pool()).await?;
 
     let community_moderator_form = CommunityModeratorForm {
       community_id: data.community_id,
       user_id: data.user_id,
     };
 
-    let mut community_moderators: Vec<i32> = vec![];
-
     let community_id = data.community_id;
 
-    community_moderators.append(
-      &mut blocking(pool, move |conn| {
-        CommunityModeratorView::for_community(&conn, community_id)
-          .map(|v| v.into_iter().map(|m| m.user_id).collect())
-      })
-      .await??,
-    );
-    community_moderators.append(
-      &mut blocking(pool, move |conn| {
-        UserView::admins(conn).map(|v| v.into_iter().map(|a| a.id).collect())
-      })
-      .await??,
-    );
-
-    if !community_moderators.contains(&user_id) {
-      return Err(APIError::err("couldnt_update_community").into());
-    }
+    // Verify that only mods or admins can add mod
+    is_mod_or_admin(context.pool(), user.id, community_id).await?;
 
     if data.added {
       let join = move |conn: &'_ _| CommunityModerator::join(conn, &community_moderator_form);
-      if blocking(pool, join).await?.is_err() {
+      if blocking(context.pool(), join).await?.is_err() {
         return Err(APIError::err("community_moderator_already_exists").into());
       }
     } else {
       let leave = move |conn: &'_ _| CommunityModerator::leave(conn, &community_moderator_form);
-      if blocking(pool, leave).await?.is_err() {
+      if blocking(context.pool(), leave).await?.is_err() {
         return Err(APIError::err("community_moderator_already_exists").into());
       }
     }
 
     // Mod tables
     let form = ModAddCommunityForm {
-      mod_user_id: user_id,
+      mod_user_id: user.id,
       other_user_id: data.user_id,
       community_id: data.community_id,
       removed: Some(!data.added),
     };
-    blocking(pool, move |conn| ModAddCommunity::create(conn, &form)).await??;
+    blocking(context.pool(), move |conn| {
+      ModAddCommunity::create(conn, &form)
+    })
+    .await??;
 
     let community_id = data.community_id;
-    let moderators = blocking(pool, move |conn| {
+    let moderators = blocking(context.pool(), move |conn| {
       CommunityModeratorView::for_community(conn, community_id)
     })
     .await??;
 
     let res = AddModToCommunityResponse { moderators };
 
-    if let Some(ws) = websocket_info {
-      ws.chatserver.do_send(SendCommunityRoomMessage {
-        op: UserOperation::AddModToCommunity,
-        response: res.clone(),
-        community_id: data.community_id,
-        my_id: ws.id,
-      });
-    }
+    context.chat_server().do_send(SendCommunityRoomMessage {
+      op: UserOperation::AddModToCommunity,
+      response: res.clone(),
+      community_id,
+      websocket_id,
+    });
 
     Ok(res)
   }
 }
 
 #[async_trait::async_trait(?Send)]
-impl Perform for Oper<TransferCommunity> {
+impl Perform for TransferCommunity {
   type Response = GetCommunityResponse;
 
   async fn perform(
     &self,
-    pool: &DbPool,
-    _websocket_info: Option<WebsocketInfo>,
+    context: &Data<LemmyContext>,
+    _websocket_id: Option<ConnectionId>,
   ) -> Result<GetCommunityResponse, LemmyError> {
-    let data: &TransferCommunity = &self.data;
-
-    let claims = match Claims::decode(&data.auth) {
-      Ok(claims) => claims.claims,
-      Err(_e) => return Err(APIError::err("not_logged_in").into()),
-    };
-
-    let user_id = claims.id;
+    let data: &TransferCommunity = &self;
+    let user = get_user_from_jwt(&data.auth, context.pool()).await?;
 
     let community_id = data.community_id;
-    let read_community = blocking(pool, move |conn| Community::read(conn, community_id)).await??;
+    let read_community = blocking(context.pool(), move |conn| {
+      Community::read(conn, community_id)
+    })
+    .await??;
 
-    let site_creator_id =
-      blocking(pool, move |conn| Site::read(conn, 1).map(|s| s.creator_id)).await??;
+    let site_creator_id = blocking(context.pool(), move |conn| {
+      Site::read(conn, 1).map(|s| s.creator_id)
+    })
+    .await??;
 
-    let mut admins = blocking(pool, move |conn| UserView::admins(conn)).await??;
+    let mut admins = blocking(context.pool(), move |conn| UserView::admins(conn)).await??;
 
-    let creator_index = admins.iter().position(|r| r.id == site_creator_id).unwrap();
+    let creator_index = admins
+      .iter()
+      .position(|r| r.id == site_creator_id)
+      .context(location_info!())?;
     let creator_user = admins.remove(creator_index);
     admins.insert(0, creator_user);
 
     // Make sure user is the creator, or an admin
-    if user_id != read_community.creator_id && !admins.iter().map(|a| a.id).any(|x| x == user_id) {
+    if user.id != read_community.creator_id && !admins.iter().map(|a| a.id).any(|x| x == user.id) {
       return Err(APIError::err("not_an_admin").into());
     }
 
-    let community_form = CommunityForm {
-      name: read_community.name,
-      title: read_community.title,
-      description: read_community.description,
-      category_id: read_community.category_id,
-      creator_id: data.user_id, // This makes the new user the community creator
-      removed: None,
-      deleted: None,
-      nsfw: read_community.nsfw,
-      updated: Some(naive_now()),
-      actor_id: read_community.actor_id,
-      local: read_community.local,
-      private_key: read_community.private_key,
-      public_key: read_community.public_key,
-      last_refreshed_at: None,
-      published: None,
-    };
-
     let community_id = data.community_id;
-    let update = move |conn: &'_ _| Community::update(conn, community_id, &community_form);
-    if blocking(pool, update).await?.is_err() {
+    let new_creator = data.user_id;
+    let update = move |conn: &'_ _| Community::update_creator(conn, community_id, new_creator);
+    if blocking(context.pool(), update).await?.is_err() {
       return Err(APIError::err("couldnt_update_community").into());
     };
 
     // You also have to re-do the community_moderator table, reordering it.
     let community_id = data.community_id;
-    let mut community_mods = blocking(pool, move |conn| {
+    let mut community_mods = blocking(context.pool(), move |conn| {
       CommunityModeratorView::for_community(conn, community_id)
     })
     .await??;
     let creator_index = community_mods
       .iter()
       .position(|r| r.user_id == data.user_id)
-      .unwrap();
+      .context(location_info!())?;
     let creator_user = community_mods.remove(creator_index);
     community_mods.insert(0, creator_user);
 
     let community_id = data.community_id;
-    blocking(pool, move |conn| {
+    blocking(context.pool(), move |conn| {
       CommunityModerator::delete_for_community(conn, community_id)
     })
     .await??;
@@ -901,22 +901,26 @@ impl Perform for Oper<TransferCommunity> {
       };
 
       let join = move |conn: &'_ _| CommunityModerator::join(conn, &community_moderator_form);
-      if blocking(pool, join).await?.is_err() {
+      if blocking(context.pool(), join).await?.is_err() {
         return Err(APIError::err("community_moderator_already_exists").into());
       }
     }
 
     // Mod tables
     let form = ModAddCommunityForm {
-      mod_user_id: user_id,
+      mod_user_id: user.id,
       other_user_id: data.user_id,
       community_id: data.community_id,
       removed: Some(false),
     };
-    blocking(pool, move |conn| ModAddCommunity::create(conn, &form)).await??;
+    blocking(context.pool(), move |conn| {
+      ModAddCommunity::create(conn, &form)
+    })
+    .await??;
 
     let community_id = data.community_id;
-    let community_view = match blocking(pool, move |conn| {
+    let user_id = user.id;
+    let community_view = match blocking(context.pool(), move |conn| {
       CommunityView::read(conn, community_id, Some(user_id))
     })
     .await?
@@ -926,7 +930,7 @@ impl Perform for Oper<TransferCommunity> {
     };
 
     let community_id = data.community_id;
-    let moderators = match blocking(pool, move |conn| {
+    let moderators = match blocking(context.pool(), move |conn| {
       CommunityModeratorView::for_community(conn, community_id)
     })
     .await?
@@ -939,8 +943,26 @@ impl Perform for Oper<TransferCommunity> {
     Ok(GetCommunityResponse {
       community: community_view,
       moderators,
-      admins,
       online: 0,
     })
   }
 }
+
+pub fn send_community_websocket(
+  res: &CommunityResponse,
+  context: &Data<LemmyContext>,
+  websocket_id: Option<ConnectionId>,
+  op: UserOperation,
+) {
+  // Strip out the user id and subscribed when sending to others
+  let mut res_sent = res.clone();
+  res_sent.community.user_id = None;
+  res_sent.community.subscribed = None;
+
+  context.chat_server().do_send(SendCommunityRoomMessage {
+    op,
+    response: res_sent,
+    community_id: res.community.id,
+    websocket_id,
+  });
+}
index bb65815ad931f80f64ec02af2df34262f79356bb..5f8706e00dace66a109098ecb58820bcaaef85ef 100644 (file)
@@ -1,6 +1,17 @@
-use crate::{websocket::WebsocketInfo, DbPool, LemmyError};
-use actix_web::client::Client;
-use lemmy_db::{community::*, community_view::*, moderator::*, site::*, user::*, user_view::*};
+use crate::{api::claims::Claims, blocking, ConnectionId, DbPool, LemmyContext, LemmyError};
+use actix_web::web::Data;
+use lemmy_db::{
+  community::*,
+  community_view::*,
+  moderator::*,
+  post::Post,
+  site::*,
+  user::*,
+  user_view::*,
+  Crud,
+};
+use lemmy_utils::{slur_check, slurs_vec_to_str};
+use thiserror::Error;
 
 pub mod claims;
 pub mod comment;
@@ -9,8 +20,8 @@ pub mod post;
 pub mod site;
 pub mod user;
 
-#[derive(Fail, Debug)]
-#[fail(display = "{{\"error\":\"{}\"}}", message)]
+#[derive(Debug, Error)]
+#[error("{{\"error\":\"{message}\"}}")]
 pub struct APIError {
   pub message: String,
 }
@@ -23,24 +34,95 @@ impl APIError {
   }
 }
 
-pub struct Oper<T> {
-  data: T,
-  client: Client,
-}
-
-impl<Data> Oper<Data> {
-  pub fn new(data: Data, client: Client) -> Oper<Data> {
-    Oper { data, client }
-  }
-}
-
 #[async_trait::async_trait(?Send)]
 pub trait Perform {
   type Response: serde::ser::Serialize + Send;
 
   async fn perform(
     &self,
-    pool: &DbPool,
-    websocket_info: Option<WebsocketInfo>,
+    context: &Data<LemmyContext>,
+    websocket_id: Option<ConnectionId>,
   ) -> Result<Self::Response, LemmyError>;
 }
+
+pub(in crate::api) async fn is_mod_or_admin(
+  pool: &DbPool,
+  user_id: i32,
+  community_id: i32,
+) -> Result<(), LemmyError> {
+  let is_mod_or_admin = blocking(pool, move |conn| {
+    Community::is_mod_or_admin(conn, user_id, community_id)
+  })
+  .await?;
+  if !is_mod_or_admin {
+    return Err(APIError::err("not_a_mod_or_admin").into());
+  }
+  Ok(())
+}
+pub async fn is_admin(pool: &DbPool, user_id: i32) -> Result<(), LemmyError> {
+  let user = blocking(pool, move |conn| User_::read(conn, user_id)).await??;
+  if !user.admin {
+    return Err(APIError::err("not_an_admin").into());
+  }
+  Ok(())
+}
+
+pub(in crate::api) async fn get_post(post_id: i32, pool: &DbPool) -> Result<Post, LemmyError> {
+  match blocking(pool, move |conn| Post::read(conn, post_id)).await? {
+    Ok(post) => Ok(post),
+    Err(_e) => Err(APIError::err("couldnt_find_post").into()),
+  }
+}
+
+pub(in crate::api) async fn get_user_from_jwt(
+  jwt: &str,
+  pool: &DbPool,
+) -> Result<User_, LemmyError> {
+  let claims = match Claims::decode(&jwt) {
+    Ok(claims) => claims.claims,
+    Err(_e) => return Err(APIError::err("not_logged_in").into()),
+  };
+  let user_id = claims.id;
+  let user = blocking(pool, move |conn| User_::read(conn, user_id)).await??;
+  // Check for a site ban
+  if user.banned {
+    return Err(APIError::err("site_ban").into());
+  }
+  Ok(user)
+}
+
+pub(in crate::api) async fn get_user_from_jwt_opt(
+  jwt: &Option<String>,
+  pool: &DbPool,
+) -> Result<Option<User_>, LemmyError> {
+  match jwt {
+    Some(jwt) => Ok(Some(get_user_from_jwt(jwt, pool).await?)),
+    None => Ok(None),
+  }
+}
+
+pub(in crate) fn check_slurs(text: &str) -> Result<(), APIError> {
+  if let Err(slurs) = slur_check(text) {
+    Err(APIError::err(&slurs_vec_to_str(slurs)))
+  } else {
+    Ok(())
+  }
+}
+pub(in crate) fn check_slurs_opt(text: &Option<String>) -> Result<(), APIError> {
+  match text {
+    Some(t) => check_slurs(t),
+    None => Ok(()),
+  }
+}
+pub(in crate::api) async fn check_community_ban(
+  user_id: i32,
+  community_id: i32,
+  pool: &DbPool,
+) -> Result<(), LemmyError> {
+  let is_banned = move |conn: &'_ _| CommunityUserBanView::get(conn, user_id, community_id).is_ok();
+  if blocking(pool, is_banned).await? {
+    Err(APIError::err("community_ban").into())
+  } else {
+    Ok(())
+  }
+}
index b9518f0e956f0bad5ba345bf5cb3e0c9c31ddd4d..5cb7e3222192df531285d30f669aa399a46c35c5 100644 (file)
@@ -1,16 +1,26 @@
 use crate::{
-  api::{claims::Claims, APIError, Oper, Perform},
+  api::{
+    check_community_ban,
+    check_slurs,
+    check_slurs_opt,
+    get_user_from_jwt,
+    get_user_from_jwt_opt,
+    is_mod_or_admin,
+    APIError,
+    Perform,
+  },
   apub::{ApubLikeableType, ApubObjectType},
   blocking,
   fetch_iframely_and_pictrs_data,
   websocket::{
-    server::{JoinCommunityRoom, JoinPostRoom, SendPost},
+    server::{GetPostUsersOnline, JoinCommunityRoom, JoinPostRoom, SendPost},
     UserOperation,
-    WebsocketInfo,
   },
-  DbPool,
+  ConnectionId,
+  LemmyContext,
   LemmyError,
 };
+use actix_web::web::Data;
 use lemmy_db::{
   comment_view::*,
   community_view::*,
@@ -18,25 +28,17 @@ use lemmy_db::{
   naive_now,
   post::*,
   post_view::*,
-  site::*,
   site_view::*,
-  user::*,
-  user_view::*,
   Crud,
   Likeable,
   ListingType,
   Saveable,
   SortType,
 };
-use lemmy_utils::{
-  is_valid_post_title,
-  make_apub_endpoint,
-  slur_check,
-  slurs_vec_to_str,
-  EndpointType,
-};
+use lemmy_utils::{is_valid_post_title, make_apub_endpoint, EndpointType};
 use serde::{Deserialize, Serialize};
 use std::str::FromStr;
+use url::Url;
 
 #[derive(Serialize, Deserialize, Debug)]
 pub struct CreatePost {
@@ -65,7 +67,6 @@ pub struct GetPostResponse {
   comments: Vec<CommentView>,
   community: CommunityView,
   moderators: Vec<CommunityModeratorView>,
-  admins: Vec<UserView>,
   pub online: usize,
 }
 
@@ -95,20 +96,42 @@ pub struct CreatePostLike {
 #[derive(Serialize, Deserialize)]
 pub struct EditPost {
   pub edit_id: i32,
-  creator_id: i32,
-  community_id: i32,
   name: String,
   url: Option<String>,
   body: Option<String>,
-  removed: Option<bool>,
-  deleted: Option<bool>,
   nsfw: bool,
-  locked: Option<bool>,
-  stickied: Option<bool>,
+  auth: String,
+}
+
+#[derive(Serialize, Deserialize)]
+pub struct DeletePost {
+  pub edit_id: i32,
+  deleted: bool,
+  auth: String,
+}
+
+#[derive(Serialize, Deserialize)]
+pub struct RemovePost {
+  pub edit_id: i32,
+  removed: bool,
   reason: Option<String>,
   auth: String,
 }
 
+#[derive(Serialize, Deserialize)]
+pub struct LockPost {
+  pub edit_id: i32,
+  locked: bool,
+  auth: String,
+}
+
+#[derive(Serialize, Deserialize)]
+pub struct StickyPost {
+  pub edit_id: i32,
+  stickied: bool,
+  auth: String,
+}
+
 #[derive(Serialize, Deserialize)]
 pub struct SavePost {
   post_id: i32,
@@ -117,61 +140,43 @@ pub struct SavePost {
 }
 
 #[async_trait::async_trait(?Send)]
-impl Perform for Oper<CreatePost> {
+impl Perform for CreatePost {
   type Response = PostResponse;
 
   async fn perform(
     &self,
-    pool: &DbPool,
-    websocket_info: Option<WebsocketInfo>,
+    context: &Data<LemmyContext>,
+    websocket_id: Option<ConnectionId>,
   ) -> Result<PostResponse, LemmyError> {
-    let data: &CreatePost = &self.data;
+    let data: &CreatePost = &self;
+    let user = get_user_from_jwt(&data.auth, context.pool()).await?;
 
-    let claims = match Claims::decode(&data.auth) {
-      Ok(claims) => claims.claims,
-      Err(_e) => return Err(APIError::err("not_logged_in").into()),
-    };
-
-    if let Err(slurs) = slur_check(&data.name) {
-      return Err(APIError::err(&slurs_vec_to_str(slurs)).into());
-    }
-
-    if let Some(body) = &data.body {
-      if let Err(slurs) = slur_check(body) {
-        return Err(APIError::err(&slurs_vec_to_str(slurs)).into());
-      }
-    }
+    check_slurs(&data.name)?;
+    check_slurs_opt(&data.body)?;
 
     if !is_valid_post_title(&data.name) {
       return Err(APIError::err("invalid_post_title").into());
     }
 
-    let user_id = claims.id;
-
-    // Check for a community ban
-    let community_id = data.community_id;
-    let is_banned =
-      move |conn: &'_ _| CommunityUserBanView::get(conn, user_id, community_id).is_ok();
-    if blocking(pool, is_banned).await? {
-      return Err(APIError::err("community_ban").into());
-    }
+    check_community_ban(user.id, data.community_id, context.pool()).await?;
 
-    // Check for a site ban
-    let user = blocking(pool, move |conn| User_::read(conn, user_id)).await??;
-    if user.banned {
-      return Err(APIError::err("site_ban").into());
+    if let Some(url) = data.url.as_ref() {
+      match Url::parse(url) {
+        Ok(_t) => (),
+        Err(_e) => return Err(APIError::err("invalid_url").into()),
+      }
     }
 
     // Fetch Iframely and pictrs cached image
     let (iframely_title, iframely_description, iframely_html, pictrs_thumbnail) =
-      fetch_iframely_and_pictrs_data(&self.client, data.url.to_owned()).await;
+      fetch_iframely_and_pictrs_data(context.client(), data.url.to_owned()).await;
 
     let post_form = PostForm {
       name: data.name.trim().to_owned(),
       url: data.url.to_owned(),
       body: data.body.to_owned(),
       community_id: data.community_id,
-      creator_id: user_id,
+      creator_id: user.id,
       removed: None,
       deleted: None,
       nsfw: data.nsfw,
@@ -187,21 +192,22 @@ impl Perform for Oper<CreatePost> {
       published: None,
     };
 
-    let inserted_post = match blocking(pool, move |conn| Post::create(conn, &post_form)).await? {
-      Ok(post) => post,
-      Err(e) => {
-        let err_type = if e.to_string() == "value too long for type character varying(200)" {
-          "post_title_too_long"
-        } else {
-          "couldnt_create_post"
-        };
-
-        return Err(APIError::err(err_type).into());
-      }
-    };
+    let inserted_post =
+      match blocking(context.pool(), move |conn| Post::create(conn, &post_form)).await? {
+        Ok(post) => post,
+        Err(e) => {
+          let err_type = if e.to_string() == "value too long for type character varying(200)" {
+            "post_title_too_long"
+          } else {
+            "couldnt_create_post"
+          };
+
+          return Err(APIError::err(err_type).into());
+        }
+      };
 
     let inserted_post_id = inserted_post.id;
-    let updated_post = match blocking(pool, move |conn| {
+    let updated_post = match blocking(context.pool(), move |conn| {
       let apub_id =
         make_apub_endpoint(EndpointType::Post, &inserted_post_id.to_string()).to_string();
       Post::update_ap_id(conn, inserted_post_id, apub_id)
@@ -212,26 +218,26 @@ impl Perform for Oper<CreatePost> {
       Err(_e) => return Err(APIError::err("couldnt_create_post").into()),
     };
 
-    updated_post.send_create(&user, &self.client, pool).await?;
+    updated_post.send_create(&user, context).await?;
 
     // They like their own post by default
     let like_form = PostLikeForm {
       post_id: inserted_post.id,
-      user_id,
+      user_id: user.id,
       score: 1,
     };
 
     let like = move |conn: &'_ _| PostLike::like(conn, &like_form);
-    if blocking(pool, like).await?.is_err() {
+    if blocking(context.pool(), like).await?.is_err() {
       return Err(APIError::err("couldnt_like_post").into());
     }
 
-    updated_post.send_like(&user, &self.client, pool).await?;
+    updated_post.send_like(&user, context).await?;
 
     // Refetch the view
     let inserted_post_id = inserted_post.id;
-    let post_view = match blocking(pool, move |conn| {
-      PostView::read(conn, inserted_post_id, Some(user_id))
+    let post_view = match blocking(context.pool(), move |conn| {
+      PostView::read(conn, inserted_post_id, Some(user.id))
     })
     .await?
     {
@@ -241,48 +247,41 @@ impl Perform for Oper<CreatePost> {
 
     let res = PostResponse { post: post_view };
 
-    if let Some(ws) = websocket_info {
-      ws.chatserver.do_send(SendPost {
-        op: UserOperation::CreatePost,
-        post: res.clone(),
-        my_id: ws.id,
-      });
-    }
+    context.chat_server().do_send(SendPost {
+      op: UserOperation::CreatePost,
+      post: res.clone(),
+      websocket_id,
+    });
 
     Ok(res)
   }
 }
 
 #[async_trait::async_trait(?Send)]
-impl Perform for Oper<GetPost> {
+impl Perform for GetPost {
   type Response = GetPostResponse;
 
   async fn perform(
     &self,
-    pool: &DbPool,
-    websocket_info: Option<WebsocketInfo>,
+    context: &Data<LemmyContext>,
+    websocket_id: Option<ConnectionId>,
   ) -> Result<GetPostResponse, LemmyError> {
-    let data: &GetPost = &self.data;
-
-    let user_id: Option<i32> = match &data.auth {
-      Some(auth) => match Claims::decode(&auth) {
-        Ok(claims) => {
-          let user_id = claims.claims.id;
-          Some(user_id)
-        }
-        Err(_e) => None,
-      },
-      None => None,
-    };
+    let data: &GetPost = &self;
+    let user = get_user_from_jwt_opt(&data.auth, context.pool()).await?;
+    let user_id = user.map(|u| u.id);
 
     let id = data.id;
-    let post_view = match blocking(pool, move |conn| PostView::read(conn, id, user_id)).await? {
+    let post_view = match blocking(context.pool(), move |conn| {
+      PostView::read(conn, id, user_id)
+    })
+    .await?
+    {
       Ok(post) => post,
       Err(_e) => return Err(APIError::err("couldnt_find_post").into()),
     };
 
     let id = data.id;
-    let comments = blocking(pool, move |conn| {
+    let comments = blocking(context.pool(), move |conn| {
       CommentQueryBuilder::create(conn)
         .for_post_id(id)
         .my_user_id(user_id)
@@ -292,42 +291,29 @@ impl Perform for Oper<GetPost> {
     .await??;
 
     let community_id = post_view.community_id;
-    let community = blocking(pool, move |conn| {
+    let community = blocking(context.pool(), move |conn| {
       CommunityView::read(conn, community_id, user_id)
     })
     .await??;
 
     let community_id = post_view.community_id;
-    let moderators = blocking(pool, move |conn| {
+    let moderators = blocking(context.pool(), move |conn| {
       CommunityModeratorView::for_community(conn, community_id)
     })
     .await??;
 
-    let site_creator_id =
-      blocking(pool, move |conn| Site::read(conn, 1).map(|s| s.creator_id)).await??;
-
-    let mut admins = blocking(pool, move |conn| UserView::admins(conn)).await??;
-    let creator_index = admins.iter().position(|r| r.id == site_creator_id).unwrap();
-    let creator_user = admins.remove(creator_index);
-    admins.insert(0, creator_user);
-
-    let online = if let Some(ws) = websocket_info {
-      if let Some(id) = ws.id {
-        ws.chatserver.do_send(JoinPostRoom {
-          post_id: data.id,
-          id,
-        });
-      }
+    if let Some(id) = websocket_id {
+      context.chat_server().do_send(JoinPostRoom {
+        post_id: data.id,
+        id,
+      });
+    }
 
-      // TODO
-      1
-    // let fut = async {
-    //   ws.chatserver.send(GetPostUsersOnline {post_id: data.id}).await.unwrap()
-    // };
-    // Runtime::new().unwrap().block_on(fut)
-    } else {
-      0
-    };
+    let online = context
+      .chat_server()
+      .send(GetPostUsersOnline { post_id: data.id })
+      .await
+      .unwrap_or(1);
 
     // Return the jwt
     Ok(GetPostResponse {
@@ -335,38 +321,30 @@ impl Perform for Oper<GetPost> {
       comments,
       community,
       moderators,
-      admins,
       online,
     })
   }
 }
 
 #[async_trait::async_trait(?Send)]
-impl Perform for Oper<GetPosts> {
+impl Perform for GetPosts {
   type Response = GetPostsResponse;
 
   async fn perform(
     &self,
-    pool: &DbPool,
-    websocket_info: Option<WebsocketInfo>,
+    context: &Data<LemmyContext>,
+    websocket_id: Option<ConnectionId>,
   ) -> Result<GetPostsResponse, LemmyError> {
-    let data: &GetPosts = &self.data;
+    let data: &GetPosts = &self;
+    let user = get_user_from_jwt_opt(&data.auth, context.pool()).await?;
 
-    let user_claims: Option<Claims> = match &data.auth {
-      Some(auth) => match Claims::decode(&auth) {
-        Ok(claims) => Some(claims.claims),
-        Err(_e) => None,
-      },
+    let user_id = match &user {
+      Some(user) => Some(user.id),
       None => None,
     };
 
-    let user_id = match &user_claims {
-      Some(claims) => Some(claims.id),
-      None => None,
-    };
-
-    let show_nsfw = match &user_claims {
-      Some(claims) => claims.show_nsfw,
+    let show_nsfw = match &user {
+      Some(user) => user.show_nsfw,
       None => false,
     };
 
@@ -377,7 +355,7 @@ impl Perform for Oper<GetPosts> {
     let limit = data.limit;
     let community_id = data.community_id;
     let community_name = data.community_name.to_owned();
-    let posts = match blocking(pool, move |conn| {
+    let posts = match blocking(context.pool(), move |conn| {
       PostQueryBuilder::create(conn)
         .listing_type(type_)
         .sort(&sort)
@@ -395,17 +373,15 @@ impl Perform for Oper<GetPosts> {
       Err(_e) => return Err(APIError::err("couldnt_get_posts").into()),
     };
 
-    if let Some(ws) = websocket_info {
+    if let Some(id) = websocket_id {
       // You don't need to join the specific community room, bc this is already handled by
       // GetCommunity
       if data.community_id.is_none() {
-        if let Some(id) = ws.id {
-          // 0 is the "all" community
-          ws.chatserver.do_send(JoinCommunityRoom {
-            community_id: 0,
-            id,
-          });
-        }
+        // 0 is the "all" community
+        context.chat_server().do_send(JoinCommunityRoom {
+          community_id: 0,
+          id,
+        });
       }
     }
 
@@ -414,26 +390,20 @@ impl Perform for Oper<GetPosts> {
 }
 
 #[async_trait::async_trait(?Send)]
-impl Perform for Oper<CreatePostLike> {
+impl Perform for CreatePostLike {
   type Response = PostResponse;
 
   async fn perform(
     &self,
-    pool: &DbPool,
-    websocket_info: Option<WebsocketInfo>,
+    context: &Data<LemmyContext>,
+    websocket_id: Option<ConnectionId>,
   ) -> Result<PostResponse, LemmyError> {
-    let data: &CreatePostLike = &self.data;
-
-    let claims = match Claims::decode(&data.auth) {
-      Ok(claims) => claims.claims,
-      Err(_e) => return Err(APIError::err("not_logged_in").into()),
-    };
-
-    let user_id = claims.id;
+    let data: &CreatePostLike = &self;
+    let user = get_user_from_jwt(&data.auth, context.pool()).await?;
 
     // Don't do a downvote if site has downvotes disabled
     if data.score == -1 {
-      let site = blocking(pool, move |conn| SiteView::read(conn)).await??;
+      let site = blocking(context.pool(), move |conn| SiteView::read(conn)).await??;
       if !site.enable_downvotes {
         return Err(APIError::err("downvotes_disabled").into());
       }
@@ -441,51 +411,44 @@ impl Perform for Oper<CreatePostLike> {
 
     // Check for a community ban
     let post_id = data.post_id;
-    let post = blocking(pool, move |conn| Post::read(conn, post_id)).await??;
+    let post = blocking(context.pool(), move |conn| Post::read(conn, post_id)).await??;
 
-    let community_id = post.community_id;
-    let is_banned =
-      move |conn: &'_ _| CommunityUserBanView::get(conn, user_id, community_id).is_ok();
-    if blocking(pool, is_banned).await? {
-      return Err(APIError::err("community_ban").into());
-    }
-
-    // Check for a site ban
-    let user = blocking(pool, move |conn| User_::read(conn, user_id)).await??;
-    if user.banned {
-      return Err(APIError::err("site_ban").into());
-    }
+    check_community_ban(user.id, post.community_id, context.pool()).await?;
 
     let like_form = PostLikeForm {
       post_id: data.post_id,
-      user_id,
+      user_id: user.id,
       score: data.score,
     };
 
     // Remove any likes first
-    let like_form2 = like_form.clone();
-    blocking(pool, move |conn| PostLike::remove(conn, &like_form2)).await??;
+    let user_id = user.id;
+    blocking(context.pool(), move |conn| {
+      PostLike::remove(conn, user_id, post_id)
+    })
+    .await??;
 
     // Only add the like if the score isnt 0
     let do_add = like_form.score != 0 && (like_form.score == 1 || like_form.score == -1);
     if do_add {
       let like_form2 = like_form.clone();
       let like = move |conn: &'_ _| PostLike::like(conn, &like_form2);
-      if blocking(pool, like).await?.is_err() {
+      if blocking(context.pool(), like).await?.is_err() {
         return Err(APIError::err("couldnt_like_post").into());
       }
 
       if like_form.score == 1 {
-        post.send_like(&user, &self.client, pool).await?;
+        post.send_like(&user, context).await?;
       } else if like_form.score == -1 {
-        post.send_dislike(&user, &self.client, pool).await?;
+        post.send_dislike(&user, context).await?;
       }
     } else {
-      post.send_undo_like(&user, &self.client, pool).await?;
+      post.send_undo_like(&user, context).await?;
     }
 
     let post_id = data.post_id;
-    let post_view = match blocking(pool, move |conn| {
+    let user_id = user.id;
+    let post_view = match blocking(context.pool(), move |conn| {
       PostView::read(conn, post_id, Some(user_id))
     })
     .await?
@@ -496,145 +459,75 @@ impl Perform for Oper<CreatePostLike> {
 
     let res = PostResponse { post: post_view };
 
-    if let Some(ws) = websocket_info {
-      ws.chatserver.do_send(SendPost {
-        op: UserOperation::CreatePostLike,
-        post: res.clone(),
-        my_id: ws.id,
-      });
-    }
+    context.chat_server().do_send(SendPost {
+      op: UserOperation::CreatePostLike,
+      post: res.clone(),
+      websocket_id,
+    });
 
     Ok(res)
   }
 }
 
 #[async_trait::async_trait(?Send)]
-impl Perform for Oper<EditPost> {
+impl Perform for EditPost {
   type Response = PostResponse;
 
   async fn perform(
     &self,
-    pool: &DbPool,
-    websocket_info: Option<WebsocketInfo>,
+    context: &Data<LemmyContext>,
+    websocket_id: Option<ConnectionId>,
   ) -> Result<PostResponse, LemmyError> {
-    let data: &EditPost = &self.data;
-
-    if let Err(slurs) = slur_check(&data.name) {
-      return Err(APIError::err(&slurs_vec_to_str(slurs)).into());
-    }
+    let data: &EditPost = &self;
+    let user = get_user_from_jwt(&data.auth, context.pool()).await?;
 
-    if let Some(body) = &data.body {
-      if let Err(slurs) = slur_check(body) {
-        return Err(APIError::err(&slurs_vec_to_str(slurs)).into());
-      }
-    }
+    check_slurs(&data.name)?;
+    check_slurs_opt(&data.body)?;
 
     if !is_valid_post_title(&data.name) {
       return Err(APIError::err("invalid_post_title").into());
     }
 
-    let claims = match Claims::decode(&data.auth) {
-      Ok(claims) => claims.claims,
-      Err(_e) => return Err(APIError::err("not_logged_in").into()),
-    };
-
-    let user_id = claims.id;
-
     let edit_id = data.edit_id;
-    let read_post = blocking(pool, move |conn| Post::read(conn, edit_id)).await??;
-
-    // Verify its the creator or a mod or admin
-    let community_id = read_post.community_id;
-    let mut editors: Vec<i32> = vec![read_post.creator_id];
-    let mut moderators: Vec<i32> = vec![];
-
-    moderators.append(
-      &mut blocking(pool, move |conn| {
-        CommunityModeratorView::for_community(conn, community_id)
-          .map(|v| v.into_iter().map(|m| m.user_id).collect())
-      })
-      .await??,
-    );
-    moderators.append(
-      &mut blocking(pool, move |conn| {
-        UserView::admins(conn).map(|v| v.into_iter().map(|a| a.id).collect())
-      })
-      .await??,
-    );
-
-    editors.extend(&moderators);
-
-    if !editors.contains(&user_id) {
-      return Err(APIError::err("no_post_edit_allowed").into());
-    }
+    let orig_post = blocking(context.pool(), move |conn| Post::read(conn, edit_id)).await??;
 
-    // Check for a community ban
-    let community_id = read_post.community_id;
-    let is_banned =
-      move |conn: &'_ _| CommunityUserBanView::get(conn, user_id, community_id).is_ok();
-    if blocking(pool, is_banned).await? {
-      return Err(APIError::err("community_ban").into());
-    }
+    check_community_ban(user.id, orig_post.community_id, context.pool()).await?;
 
-    // Check for a site ban
-    let user = blocking(pool, move |conn| User_::read(conn, user_id)).await??;
-    if user.banned {
-      return Err(APIError::err("site_ban").into());
+    // Verify that only the creator can edit
+    if !Post::is_post_creator(user.id, orig_post.creator_id) {
+      return Err(APIError::err("no_post_edit_allowed").into());
     }
 
     // Fetch Iframely and Pictrs cached image
     let (iframely_title, iframely_description, iframely_html, pictrs_thumbnail) =
-      fetch_iframely_and_pictrs_data(&self.client, data.url.to_owned()).await;
-
-    let post_form = {
-      // only modify some properties if they are a moderator
-      if moderators.contains(&user_id) {
-        PostForm {
-          name: data.name.trim().to_owned(),
-          url: data.url.to_owned(),
-          body: data.body.to_owned(),
-          creator_id: read_post.creator_id.to_owned(),
-          community_id: read_post.community_id,
-          removed: data.removed.to_owned(),
-          deleted: data.deleted.to_owned(),
-          nsfw: data.nsfw,
-          locked: data.locked.to_owned(),
-          stickied: data.stickied.to_owned(),
-          updated: Some(naive_now()),
-          embed_title: iframely_title,
-          embed_description: iframely_description,
-          embed_html: iframely_html,
-          thumbnail_url: pictrs_thumbnail,
-          ap_id: read_post.ap_id,
-          local: read_post.local,
-          published: None,
-        }
-      } else {
-        PostForm {
-          name: read_post.name.trim().to_owned(),
-          url: data.url.to_owned(),
-          body: data.body.to_owned(),
-          creator_id: read_post.creator_id.to_owned(),
-          community_id: read_post.community_id,
-          removed: Some(read_post.removed),
-          deleted: data.deleted.to_owned(),
-          nsfw: data.nsfw,
-          locked: Some(read_post.locked),
-          stickied: Some(read_post.stickied),
-          updated: Some(naive_now()),
-          embed_title: iframely_title,
-          embed_description: iframely_description,
-          embed_html: iframely_html,
-          thumbnail_url: pictrs_thumbnail,
-          ap_id: read_post.ap_id,
-          local: read_post.local,
-          published: None,
-        }
-      }
+      fetch_iframely_and_pictrs_data(context.client(), data.url.to_owned()).await;
+
+    let post_form = PostForm {
+      name: data.name.trim().to_owned(),
+      url: data.url.to_owned(),
+      body: data.body.to_owned(),
+      nsfw: data.nsfw,
+      creator_id: orig_post.creator_id.to_owned(),
+      community_id: orig_post.community_id,
+      removed: Some(orig_post.removed),
+      deleted: Some(orig_post.deleted),
+      locked: Some(orig_post.locked),
+      stickied: Some(orig_post.stickied),
+      updated: Some(naive_now()),
+      embed_title: iframely_title,
+      embed_description: iframely_description,
+      embed_html: iframely_html,
+      thumbnail_url: pictrs_thumbnail,
+      ap_id: orig_post.ap_id,
+      local: orig_post.local,
+      published: None,
     };
 
     let edit_id = data.edit_id;
-    let res = blocking(pool, move |conn| Post::update(conn, edit_id, &post_form)).await?;
+    let res = blocking(context.pool(), move |conn| {
+      Post::update(conn, edit_id, &post_form)
+    })
+    .await?;
     let updated_post: Post = match res {
       Ok(post) => post,
       Err(e) => {
@@ -648,116 +541,302 @@ impl Perform for Oper<EditPost> {
       }
     };
 
-    if moderators.contains(&user_id) {
-      // Mod tables
-      if let Some(removed) = data.removed.to_owned() {
-        let form = ModRemovePostForm {
-          mod_user_id: user_id,
-          post_id: data.edit_id,
-          removed: Some(removed),
-          reason: data.reason.to_owned(),
-        };
-        blocking(pool, move |conn| ModRemovePost::create(conn, &form)).await??;
-      }
+    // Send apub update
+    updated_post.send_update(&user, context).await?;
 
-      if let Some(locked) = data.locked.to_owned() {
-        let form = ModLockPostForm {
-          mod_user_id: user_id,
-          post_id: data.edit_id,
-          locked: Some(locked),
-        };
-        blocking(pool, move |conn| ModLockPost::create(conn, &form)).await??;
-      }
+    let edit_id = data.edit_id;
+    let post_view = blocking(context.pool(), move |conn| {
+      PostView::read(conn, edit_id, Some(user.id))
+    })
+    .await??;
 
-      if let Some(stickied) = data.stickied.to_owned() {
-        let form = ModStickyPostForm {
-          mod_user_id: user_id,
-          post_id: data.edit_id,
-          stickied: Some(stickied),
-        };
-        blocking(pool, move |conn| ModStickyPost::create(conn, &form)).await??;
-      }
+    let res = PostResponse { post: post_view };
+
+    context.chat_server().do_send(SendPost {
+      op: UserOperation::EditPost,
+      post: res.clone(),
+      websocket_id,
+    });
+
+    Ok(res)
+  }
+}
+
+#[async_trait::async_trait(?Send)]
+impl Perform for DeletePost {
+  type Response = PostResponse;
+
+  async fn perform(
+    &self,
+    context: &Data<LemmyContext>,
+    websocket_id: Option<ConnectionId>,
+  ) -> Result<PostResponse, LemmyError> {
+    let data: &DeletePost = &self;
+    let user = get_user_from_jwt(&data.auth, context.pool()).await?;
+
+    let edit_id = data.edit_id;
+    let orig_post = blocking(context.pool(), move |conn| Post::read(conn, edit_id)).await??;
+
+    check_community_ban(user.id, orig_post.community_id, context.pool()).await?;
+
+    // Verify that only the creator can delete
+    if !Post::is_post_creator(user.id, orig_post.creator_id) {
+      return Err(APIError::err("no_post_edit_allowed").into());
     }
 
-    if let Some(deleted) = data.deleted.to_owned() {
-      if deleted {
-        updated_post.send_delete(&user, &self.client, pool).await?;
-      } else {
-        updated_post
-          .send_undo_delete(&user, &self.client, pool)
-          .await?;
-      }
-    } else if let Some(removed) = data.removed.to_owned() {
-      if moderators.contains(&user_id) {
-        if removed {
-          updated_post.send_remove(&user, &self.client, pool).await?;
-        } else {
-          updated_post
-            .send_undo_remove(&user, &self.client, pool)
-            .await?;
-        }
-      }
+    // Update the post
+    let edit_id = data.edit_id;
+    let deleted = data.deleted;
+    let updated_post = blocking(context.pool(), move |conn| {
+      Post::update_deleted(conn, edit_id, deleted)
+    })
+    .await??;
+
+    // apub updates
+    if deleted {
+      updated_post.send_delete(&user, context).await?;
     } else {
-      updated_post.send_update(&user, &self.client, pool).await?;
+      updated_post.send_undo_delete(&user, context).await?;
     }
 
+    // Refetch the post
     let edit_id = data.edit_id;
-    let post_view = blocking(pool, move |conn| {
-      PostView::read(conn, edit_id, Some(user_id))
+    let post_view = blocking(context.pool(), move |conn| {
+      PostView::read(conn, edit_id, Some(user.id))
     })
     .await??;
 
     let res = PostResponse { post: post_view };
 
-    if let Some(ws) = websocket_info {
-      ws.chatserver.do_send(SendPost {
-        op: UserOperation::EditPost,
-        post: res.clone(),
-        my_id: ws.id,
-      });
+    context.chat_server().do_send(SendPost {
+      op: UserOperation::DeletePost,
+      post: res.clone(),
+      websocket_id,
+    });
+
+    Ok(res)
+  }
+}
+
+#[async_trait::async_trait(?Send)]
+impl Perform for RemovePost {
+  type Response = PostResponse;
+
+  async fn perform(
+    &self,
+    context: &Data<LemmyContext>,
+    websocket_id: Option<ConnectionId>,
+  ) -> Result<PostResponse, LemmyError> {
+    let data: &RemovePost = &self;
+    let user = get_user_from_jwt(&data.auth, context.pool()).await?;
+
+    let edit_id = data.edit_id;
+    let orig_post = blocking(context.pool(), move |conn| Post::read(conn, edit_id)).await??;
+
+    check_community_ban(user.id, orig_post.community_id, context.pool()).await?;
+
+    // Verify that only the mods can remove
+    is_mod_or_admin(context.pool(), user.id, orig_post.community_id).await?;
+
+    // Update the post
+    let edit_id = data.edit_id;
+    let removed = data.removed;
+    let updated_post = blocking(context.pool(), move |conn| {
+      Post::update_removed(conn, edit_id, removed)
+    })
+    .await??;
+
+    // Mod tables
+    let form = ModRemovePostForm {
+      mod_user_id: user.id,
+      post_id: data.edit_id,
+      removed: Some(removed),
+      reason: data.reason.to_owned(),
+    };
+    blocking(context.pool(), move |conn| {
+      ModRemovePost::create(conn, &form)
+    })
+    .await??;
+
+    // apub updates
+    if removed {
+      updated_post.send_remove(&user, context).await?;
+    } else {
+      updated_post.send_undo_remove(&user, context).await?;
     }
 
+    // Refetch the post
+    let edit_id = data.edit_id;
+    let user_id = user.id;
+    let post_view = blocking(context.pool(), move |conn| {
+      PostView::read(conn, edit_id, Some(user_id))
+    })
+    .await??;
+
+    let res = PostResponse { post: post_view };
+
+    context.chat_server().do_send(SendPost {
+      op: UserOperation::RemovePost,
+      post: res.clone(),
+      websocket_id,
+    });
+
+    Ok(res)
+  }
+}
+
+#[async_trait::async_trait(?Send)]
+impl Perform for LockPost {
+  type Response = PostResponse;
+
+  async fn perform(
+    &self,
+    context: &Data<LemmyContext>,
+    websocket_id: Option<ConnectionId>,
+  ) -> Result<PostResponse, LemmyError> {
+    let data: &LockPost = &self;
+    let user = get_user_from_jwt(&data.auth, context.pool()).await?;
+
+    let edit_id = data.edit_id;
+    let orig_post = blocking(context.pool(), move |conn| Post::read(conn, edit_id)).await??;
+
+    check_community_ban(user.id, orig_post.community_id, context.pool()).await?;
+
+    // Verify that only the mods can lock
+    is_mod_or_admin(context.pool(), user.id, orig_post.community_id).await?;
+
+    // Update the post
+    let edit_id = data.edit_id;
+    let locked = data.locked;
+    let updated_post = blocking(context.pool(), move |conn| {
+      Post::update_locked(conn, edit_id, locked)
+    })
+    .await??;
+
+    // Mod tables
+    let form = ModLockPostForm {
+      mod_user_id: user.id,
+      post_id: data.edit_id,
+      locked: Some(locked),
+    };
+    blocking(context.pool(), move |conn| ModLockPost::create(conn, &form)).await??;
+
+    // apub updates
+    updated_post.send_update(&user, context).await?;
+
+    // Refetch the post
+    let edit_id = data.edit_id;
+    let post_view = blocking(context.pool(), move |conn| {
+      PostView::read(conn, edit_id, Some(user.id))
+    })
+    .await??;
+
+    let res = PostResponse { post: post_view };
+
+    context.chat_server().do_send(SendPost {
+      op: UserOperation::LockPost,
+      post: res.clone(),
+      websocket_id,
+    });
+
     Ok(res)
   }
 }
 
 #[async_trait::async_trait(?Send)]
-impl Perform for Oper<SavePost> {
+impl Perform for StickyPost {
   type Response = PostResponse;
 
   async fn perform(
     &self,
-    pool: &DbPool,
-    _websocket_info: Option<WebsocketInfo>,
+    context: &Data<LemmyContext>,
+    websocket_id: Option<ConnectionId>,
   ) -> Result<PostResponse, LemmyError> {
-    let data: &SavePost = &self.data;
+    let data: &StickyPost = &self;
+    let user = get_user_from_jwt(&data.auth, context.pool()).await?;
+
+    let edit_id = data.edit_id;
+    let orig_post = blocking(context.pool(), move |conn| Post::read(conn, edit_id)).await??;
+
+    check_community_ban(user.id, orig_post.community_id, context.pool()).await?;
 
-    let claims = match Claims::decode(&data.auth) {
-      Ok(claims) => claims.claims,
-      Err(_e) => return Err(APIError::err("not_logged_in").into()),
+    // Verify that only the mods can sticky
+    is_mod_or_admin(context.pool(), user.id, orig_post.community_id).await?;
+
+    // Update the post
+    let edit_id = data.edit_id;
+    let stickied = data.stickied;
+    let updated_post = blocking(context.pool(), move |conn| {
+      Post::update_stickied(conn, edit_id, stickied)
+    })
+    .await??;
+
+    // Mod tables
+    let form = ModStickyPostForm {
+      mod_user_id: user.id,
+      post_id: data.edit_id,
+      stickied: Some(stickied),
     };
+    blocking(context.pool(), move |conn| {
+      ModStickyPost::create(conn, &form)
+    })
+    .await??;
+
+    // Apub updates
+    // TODO stickied should pry work like locked for ease of use
+    updated_post.send_update(&user, context).await?;
 
-    let user_id = claims.id;
+    // Refetch the post
+    let edit_id = data.edit_id;
+    let post_view = blocking(context.pool(), move |conn| {
+      PostView::read(conn, edit_id, Some(user.id))
+    })
+    .await??;
+
+    let res = PostResponse { post: post_view };
+
+    context.chat_server().do_send(SendPost {
+      op: UserOperation::StickyPost,
+      post: res.clone(),
+      websocket_id,
+    });
+
+    Ok(res)
+  }
+}
+
+#[async_trait::async_trait(?Send)]
+impl Perform for SavePost {
+  type Response = PostResponse;
+
+  async fn perform(
+    &self,
+    context: &Data<LemmyContext>,
+    _websocket_id: Option<ConnectionId>,
+  ) -> Result<PostResponse, LemmyError> {
+    let data: &SavePost = &self;
+    let user = get_user_from_jwt(&data.auth, context.pool()).await?;
 
     let post_saved_form = PostSavedForm {
       post_id: data.post_id,
-      user_id,
+      user_id: user.id,
     };
 
     if data.save {
       let save = move |conn: &'_ _| PostSaved::save(conn, &post_saved_form);
-      if blocking(pool, save).await?.is_err() {
+      if blocking(context.pool(), save).await?.is_err() {
         return Err(APIError::err("couldnt_save_post").into());
       }
     } else {
       let unsave = move |conn: &'_ _| PostSaved::unsave(conn, &post_saved_form);
-      if blocking(pool, unsave).await?.is_err() {
+      if blocking(context.pool(), unsave).await?.is_err() {
         return Err(APIError::err("couldnt_save_post").into());
       }
     }
 
     let post_id = data.post_id;
-    let post_view = blocking(pool, move |conn| {
+    let user_id = user.id;
+    let post_view = blocking(context.pool(), move |conn| {
       PostView::read(conn, post_id, Some(user_id))
     })
     .await??;
index 241a80e31e97014a4ba45504a1edab4b686a7e9a..8f5f0e93082f3feb318cb9aae8b01e4e672e3d03 100644 (file)
@@ -1,28 +1,45 @@
 use super::user::Register;
 use crate::{
-  api::{claims::Claims, APIError, Oper, Perform},
+  api::{
+    check_slurs,
+    check_slurs_opt,
+    get_user_from_jwt,
+    get_user_from_jwt_opt,
+    is_admin,
+    APIError,
+    Perform,
+  },
   apub::fetcher::search_by_apub_id,
   blocking,
-  websocket::{server::SendAllMessage, UserOperation, WebsocketInfo},
-  DbPool,
+  version,
+  websocket::{
+    server::{GetUsersOnline, SendAllMessage},
+    UserOperation,
+  },
+  ConnectionId,
+  LemmyContext,
   LemmyError,
 };
+use actix_web::web::Data;
+use anyhow::Context;
 use lemmy_db::{
   category::*,
   comment_view::*,
   community_view::*,
+  diesel_option_overwrite,
   moderator::*,
   moderator_views::*,
   naive_now,
   post_view::*,
   site::*,
   site_view::*,
+  user::*,
   user_view::*,
   Crud,
   SearchType,
   SortType,
 };
-use lemmy_utils::{settings::Settings, slur_check, slurs_vec_to_str};
+use lemmy_utils::{location_info, settings::Settings};
 use log::{debug, info};
 use serde::{Deserialize, Serialize};
 use std::str::FromStr;
@@ -80,6 +97,8 @@ pub struct GetModlogResponse {
 pub struct CreateSite {
   pub name: String,
   pub description: Option<String>,
+  pub icon: Option<String>,
+  pub banner: Option<String>,
   pub enable_downvotes: bool,
   pub open_registration: bool,
   pub enable_nsfw: bool,
@@ -90,6 +109,8 @@ pub struct CreateSite {
 pub struct EditSite {
   name: String,
   description: Option<String>,
+  icon: Option<String>,
+  banner: Option<String>,
   enable_downvotes: bool,
   open_registration: bool,
   enable_nsfw: bool,
@@ -97,7 +118,9 @@ pub struct EditSite {
 }
 
 #[derive(Serialize, Deserialize)]
-pub struct GetSite {}
+pub struct GetSite {
+  auth: Option<String>,
+}
 
 #[derive(Serialize, Deserialize, Clone)]
 pub struct SiteResponse {
@@ -110,6 +133,9 @@ pub struct GetSiteResponse {
   admins: Vec<UserView>,
   banned: Vec<UserView>,
   pub online: usize,
+  version: String,
+  my_user: Option<User_>,
+  federated_instances: Vec<String>,
 }
 
 #[derive(Serialize, Deserialize)]
@@ -135,17 +161,17 @@ pub struct SaveSiteConfig {
 }
 
 #[async_trait::async_trait(?Send)]
-impl Perform for Oper<ListCategories> {
+impl Perform for ListCategories {
   type Response = ListCategoriesResponse;
 
   async fn perform(
     &self,
-    pool: &DbPool,
-    _websocket_info: Option<WebsocketInfo>,
+    context: &Data<LemmyContext>,
+    _websocket_id: Option<ConnectionId>,
   ) -> Result<ListCategoriesResponse, LemmyError> {
-    let _data: &ListCategories = &self.data;
+    let _data: &ListCategories = &self;
 
-    let categories = blocking(pool, move |conn| Category::list_all(conn)).await??;
+    let categories = blocking(context.pool(), move |conn| Category::list_all(conn)).await??;
 
     // Return the jwt
     Ok(ListCategoriesResponse { categories })
@@ -153,53 +179,53 @@ impl Perform for Oper<ListCategories> {
 }
 
 #[async_trait::async_trait(?Send)]
-impl Perform for Oper<GetModlog> {
+impl Perform for GetModlog {
   type Response = GetModlogResponse;
 
   async fn perform(
     &self,
-    pool: &DbPool,
-    _websocket_info: Option<WebsocketInfo>,
+    context: &Data<LemmyContext>,
+    _websocket_id: Option<ConnectionId>,
   ) -> Result<GetModlogResponse, LemmyError> {
-    let data: &GetModlog = &self.data;
+    let data: &GetModlog = &self;
 
     let community_id = data.community_id;
     let mod_user_id = data.mod_user_id;
     let page = data.page;
     let limit = data.limit;
-    let removed_posts = blocking(pool, move |conn| {
+    let removed_posts = blocking(context.pool(), move |conn| {
       ModRemovePostView::list(conn, community_id, mod_user_id, page, limit)
     })
     .await??;
 
-    let locked_posts = blocking(pool, move |conn| {
+    let locked_posts = blocking(context.pool(), move |conn| {
       ModLockPostView::list(conn, community_id, mod_user_id, page, limit)
     })
     .await??;
 
-    let stickied_posts = blocking(pool, move |conn| {
+    let stickied_posts = blocking(context.pool(), move |conn| {
       ModStickyPostView::list(conn, community_id, mod_user_id, page, limit)
     })
     .await??;
 
-    let removed_comments = blocking(pool, move |conn| {
+    let removed_comments = blocking(context.pool(), move |conn| {
       ModRemoveCommentView::list(conn, community_id, mod_user_id, page, limit)
     })
     .await??;
 
-    let banned_from_community = blocking(pool, move |conn| {
+    let banned_from_community = blocking(context.pool(), move |conn| {
       ModBanFromCommunityView::list(conn, community_id, mod_user_id, page, limit)
     })
     .await??;
 
-    let added_to_community = blocking(pool, move |conn| {
+    let added_to_community = blocking(context.pool(), move |conn| {
       ModAddCommunityView::list(conn, community_id, mod_user_id, page, limit)
     })
     .await??;
 
     // These arrays are only for the full modlog, when a community isn't given
     let (removed_communities, banned, added) = if data.community_id.is_none() {
-      blocking(pool, move |conn| {
+      blocking(context.pool(), move |conn| {
         Ok((
           ModRemoveCommunityView::list(conn, mod_user_id, page, limit)?,
           ModBanView::list(conn, mod_user_id, page, limit)?,
@@ -227,43 +253,30 @@ impl Perform for Oper<GetModlog> {
 }
 
 #[async_trait::async_trait(?Send)]
-impl Perform for Oper<CreateSite> {
+impl Perform for CreateSite {
   type Response = SiteResponse;
 
   async fn perform(
     &self,
-    pool: &DbPool,
-    _websocket_info: Option<WebsocketInfo>,
+    context: &Data<LemmyContext>,
+    _websocket_id: Option<ConnectionId>,
   ) -> Result<SiteResponse, LemmyError> {
-    let data: &CreateSite = &self.data;
+    let data: &CreateSite = &self;
 
-    let claims = match Claims::decode(&data.auth) {
-      Ok(claims) => claims.claims,
-      Err(_e) => return Err(APIError::err("not_logged_in").into()),
-    };
+    let user = get_user_from_jwt(&data.auth, context.pool()).await?;
 
-    if let Err(slurs) = slur_check(&data.name) {
-      return Err(APIError::err(&slurs_vec_to_str(slurs)).into());
-    }
-
-    if let Some(description) = &data.description {
-      if let Err(slurs) = slur_check(description) {
-        return Err(APIError::err(&slurs_vec_to_str(slurs)).into());
-      }
-    }
-
-    let user_id = claims.id;
+    check_slurs(&data.name)?;
+    check_slurs_opt(&data.description)?;
 
     // Make sure user is an admin
-    let user = blocking(pool, move |conn| UserView::read(conn, user_id)).await??;
-    if !user.admin {
-      return Err(APIError::err("not_an_admin").into());
-    }
+    is_admin(context.pool(), user.id).await?;
 
     let site_form = SiteForm {
       name: data.name.to_owned(),
       description: data.description.to_owned(),
-      creator_id: user_id,
+      icon: Some(data.icon.to_owned()),
+      banner: Some(data.banner.to_owned()),
+      creator_id: user.id,
       enable_downvotes: data.enable_downvotes,
       open_registration: data.open_registration,
       enable_nsfw: data.enable_nsfw,
@@ -271,54 +284,43 @@ impl Perform for Oper<CreateSite> {
     };
 
     let create_site = move |conn: &'_ _| Site::create(conn, &site_form);
-    if blocking(pool, create_site).await?.is_err() {
+    if blocking(context.pool(), create_site).await?.is_err() {
       return Err(APIError::err("site_already_exists").into());
     }
 
-    let site_view = blocking(pool, move |conn| SiteView::read(conn)).await??;
+    let site_view = blocking(context.pool(), move |conn| SiteView::read(conn)).await??;
 
     Ok(SiteResponse { site: site_view })
   }
 }
 
 #[async_trait::async_trait(?Send)]
-impl Perform for Oper<EditSite> {
+impl Perform for EditSite {
   type Response = SiteResponse;
   async fn perform(
     &self,
-    pool: &DbPool,
-    websocket_info: Option<WebsocketInfo>,
+    context: &Data<LemmyContext>,
+    websocket_id: Option<ConnectionId>,
   ) -> Result<SiteResponse, LemmyError> {
-    let data: &EditSite = &self.data;
-
-    let claims = match Claims::decode(&data.auth) {
-      Ok(claims) => claims.claims,
-      Err(_e) => return Err(APIError::err("not_logged_in").into()),
-    };
-
-    if let Err(slurs) = slur_check(&data.name) {
-      return Err(APIError::err(&slurs_vec_to_str(slurs)).into());
-    }
-
-    if let Some(description) = &data.description {
-      if let Err(slurs) = slur_check(description) {
-        return Err(APIError::err(&slurs_vec_to_str(slurs)).into());
-      }
-    }
+    let data: &EditSite = &self;
+    let user = get_user_from_jwt(&data.auth, context.pool()).await?;
 
-    let user_id = claims.id;
+    check_slurs(&data.name)?;
+    check_slurs_opt(&data.description)?;
 
     // Make sure user is an admin
-    let user = blocking(pool, move |conn| UserView::read(conn, user_id)).await??;
-    if !user.admin {
-      return Err(APIError::err("not_an_admin").into());
-    }
+    is_admin(context.pool(), user.id).await?;
+
+    let found_site = blocking(context.pool(), move |conn| Site::read(conn, 1)).await??;
 
-    let found_site = blocking(pool, move |conn| Site::read(conn, 1)).await??;
+    let icon = diesel_option_overwrite(&data.icon);
+    let banner = diesel_option_overwrite(&data.banner);
 
     let site_form = SiteForm {
       name: data.name.to_owned(),
       description: data.description.to_owned(),
+      icon,
+      banner,
       creator_id: found_site.creator_id,
       updated: Some(naive_now()),
       enable_downvotes: data.enable_downvotes,
@@ -327,41 +329,39 @@ impl Perform for Oper<EditSite> {
     };
 
     let update_site = move |conn: &'_ _| Site::update(conn, 1, &site_form);
-    if blocking(pool, update_site).await?.is_err() {
+    if blocking(context.pool(), update_site).await?.is_err() {
       return Err(APIError::err("couldnt_update_site").into());
     }
 
-    let site_view = blocking(pool, move |conn| SiteView::read(conn)).await??;
+    let site_view = blocking(context.pool(), move |conn| SiteView::read(conn)).await??;
 
     let res = SiteResponse { site: site_view };
 
-    if let Some(ws) = websocket_info {
-      ws.chatserver.do_send(SendAllMessage {
-        op: UserOperation::EditSite,
-        response: res.clone(),
-        my_id: ws.id,
-      });
-    }
+    context.chat_server().do_send(SendAllMessage {
+      op: UserOperation::EditSite,
+      response: res.clone(),
+      websocket_id,
+    });
 
     Ok(res)
   }
 }
 
 #[async_trait::async_trait(?Send)]
-impl Perform for Oper<GetSite> {
+impl Perform for GetSite {
   type Response = GetSiteResponse;
 
   async fn perform(
     &self,
-    pool: &DbPool,
-    websocket_info: Option<WebsocketInfo>,
+    context: &Data<LemmyContext>,
+    websocket_id: Option<ConnectionId>,
   ) -> Result<GetSiteResponse, LemmyError> {
-    let _data: &GetSite = &self.data;
+    let data: &GetSite = &self;
 
     // TODO refactor this a little
-    let res = blocking(pool, move |conn| Site::read(conn, 1)).await?;
+    let res = blocking(context.pool(), move |conn| Site::read(conn, 1)).await?;
     let site_view = if res.is_ok() {
-      Some(blocking(pool, move |conn| SiteView::read(conn)).await??)
+      Some(blocking(context.pool(), move |conn| SiteView::read(conn)).await??)
     } else if let Some(setup) = Settings::get().setup.as_ref() {
       let register = Register {
         username: setup.admin_username.to_owned(),
@@ -370,30 +370,30 @@ impl Perform for Oper<GetSite> {
         password_verify: setup.admin_password.to_owned(),
         admin: true,
         show_nsfw: true,
+        captcha_uuid: None,
+        captcha_answer: None,
       };
-      let login_response = Oper::new(register, self.client.clone())
-        .perform(pool, websocket_info.clone())
-        .await?;
+      let login_response = register.perform(context, websocket_id).await?;
       info!("Admin {} created", setup.admin_username);
 
       let create_site = CreateSite {
         name: setup.site_name.to_owned(),
         description: None,
+        icon: None,
+        banner: None,
         enable_downvotes: true,
         open_registration: true,
         enable_nsfw: true,
         auth: login_response.jwt,
       };
-      Oper::new(create_site, self.client.clone())
-        .perform(pool, websocket_info.clone())
-        .await?;
+      create_site.perform(context, websocket_id).await?;
       info!("Site {} created", setup.site_name);
-      Some(blocking(pool, move |conn| SiteView::read(conn)).await??)
+      Some(blocking(context.pool(), move |conn| SiteView::read(conn)).await??)
     } else {
       None
     };
 
-    let mut admins = blocking(pool, move |conn| UserView::admins(conn)).await??;
+    let mut admins = blocking(context.pool(), move |conn| UserView::admins(conn)).await??;
 
     // Make sure the site creator is the top admin
     if let Some(site_view) = site_view.to_owned() {
@@ -406,56 +406,55 @@ impl Perform for Oper<GetSite> {
       }
     }
 
-    let banned = blocking(pool, move |conn| UserView::banned(conn)).await??;
-
-    let online = if let Some(_ws) = websocket_info {
-      // TODO
-      1
-    // let fut = async {
-    //   ws.chatserver.send(GetUsersOnline).await.unwrap()
-    // };
-    // Runtime::new().unwrap().block_on(fut)
-    } else {
-      0
-    };
+    let banned = blocking(context.pool(), move |conn| UserView::banned(conn)).await??;
+
+    let online = context
+      .chat_server()
+      .send(GetUsersOnline)
+      .await
+      .unwrap_or(1);
+
+    let my_user = get_user_from_jwt_opt(&data.auth, context.pool())
+      .await?
+      .map(|mut u| {
+        u.password_encrypted = "".to_string();
+        u.private_key = None;
+        u.public_key = None;
+        u
+      });
 
     Ok(GetSiteResponse {
       site: site_view,
       admins,
       banned,
       online,
+      version: version::VERSION.to_string(),
+      my_user,
+      federated_instances: Settings::get().get_allowed_instances(),
     })
   }
 }
 
 #[async_trait::async_trait(?Send)]
-impl Perform for Oper<Search> {
+impl Perform for Search {
   type Response = SearchResponse;
 
   async fn perform(
     &self,
-    pool: &DbPool,
-    _websocket_info: Option<WebsocketInfo>,
+    context: &Data<LemmyContext>,
+    _websocket_id: Option<ConnectionId>,
   ) -> Result<SearchResponse, LemmyError> {
-    let data: &Search = &self.data;
+    let data: &Search = &self;
 
     dbg!(&data);
 
-    match search_by_apub_id(&data.q, &self.client, pool).await {
+    match search_by_apub_id(&data.q, context).await {
       Ok(r) => return Ok(r),
       Err(e) => debug!("Failed to resolve search query as activitypub ID: {}", e),
     }
 
-    let user_id: Option<i32> = match &data.auth {
-      Some(auth) => match Claims::decode(&auth) {
-        Ok(claims) => {
-          let user_id = claims.claims.id;
-          Some(user_id)
-        }
-        Err(_e) => None,
-      },
-      None => None,
-    };
+    let user = get_user_from_jwt_opt(&data.auth, context.pool()).await?;
+    let user_id = user.map(|u| u.id);
 
     let type_ = SearchType::from_str(&data.type_)?;
 
@@ -473,7 +472,7 @@ impl Perform for Oper<Search> {
     let community_id = data.community_id;
     match type_ {
       SearchType::Posts => {
-        posts = blocking(pool, move |conn| {
+        posts = blocking(context.pool(), move |conn| {
           PostQueryBuilder::create(conn)
             .sort(&sort)
             .show_nsfw(true)
@@ -487,7 +486,7 @@ impl Perform for Oper<Search> {
         .await??;
       }
       SearchType::Comments => {
-        comments = blocking(pool, move |conn| {
+        comments = blocking(context.pool(), move |conn| {
           CommentQueryBuilder::create(&conn)
             .sort(&sort)
             .search_term(q)
@@ -499,7 +498,7 @@ impl Perform for Oper<Search> {
         .await??;
       }
       SearchType::Communities => {
-        communities = blocking(pool, move |conn| {
+        communities = blocking(context.pool(), move |conn| {
           CommunityQueryBuilder::create(conn)
             .sort(&sort)
             .search_term(q)
@@ -510,7 +509,7 @@ impl Perform for Oper<Search> {
         .await??;
       }
       SearchType::Users => {
-        users = blocking(pool, move |conn| {
+        users = blocking(context.pool(), move |conn| {
           UserQueryBuilder::create(conn)
             .sort(&sort)
             .search_term(q)
@@ -521,7 +520,7 @@ impl Perform for Oper<Search> {
         .await??;
       }
       SearchType::All => {
-        posts = blocking(pool, move |conn| {
+        posts = blocking(context.pool(), move |conn| {
           PostQueryBuilder::create(conn)
             .sort(&sort)
             .show_nsfw(true)
@@ -537,7 +536,7 @@ impl Perform for Oper<Search> {
         let q = data.q.to_owned();
         let sort = SortType::from_str(&data.sort)?;
 
-        comments = blocking(pool, move |conn| {
+        comments = blocking(context.pool(), move |conn| {
           CommentQueryBuilder::create(conn)
             .sort(&sort)
             .search_term(q)
@@ -551,7 +550,7 @@ impl Perform for Oper<Search> {
         let q = data.q.to_owned();
         let sort = SortType::from_str(&data.sort)?;
 
-        communities = blocking(pool, move |conn| {
+        communities = blocking(context.pool(), move |conn| {
           CommunityQueryBuilder::create(conn)
             .sort(&sort)
             .search_term(q)
@@ -564,7 +563,7 @@ impl Perform for Oper<Search> {
         let q = data.q.to_owned();
         let sort = SortType::from_str(&data.sort)?;
 
-        users = blocking(pool, move |conn| {
+        users = blocking(context.pool(), move |conn| {
           UserQueryBuilder::create(conn)
             .sort(&sort)
             .search_term(q)
@@ -575,7 +574,7 @@ impl Perform for Oper<Search> {
         .await??;
       }
       SearchType::Url => {
-        posts = blocking(pool, move |conn| {
+        posts = blocking(context.pool(), move |conn| {
           PostQueryBuilder::create(conn)
             .sort(&sort)
             .show_nsfw(true)
@@ -601,100 +600,82 @@ impl Perform for Oper<Search> {
 }
 
 #[async_trait::async_trait(?Send)]
-impl Perform for Oper<TransferSite> {
+impl Perform for TransferSite {
   type Response = GetSiteResponse;
 
   async fn perform(
     &self,
-    pool: &DbPool,
-    _websocket_info: Option<WebsocketInfo>,
+    context: &Data<LemmyContext>,
+    _websocket_id: Option<ConnectionId>,
   ) -> Result<GetSiteResponse, LemmyError> {
-    let data: &TransferSite = &self.data;
+    let data: &TransferSite = &self;
+    let mut user = get_user_from_jwt(&data.auth, context.pool()).await?;
 
-    let claims = match Claims::decode(&data.auth) {
-      Ok(claims) => claims.claims,
-      Err(_e) => return Err(APIError::err("not_logged_in").into()),
-    };
+    // TODO add a User_::read_safe() for this.
+    user.password_encrypted = "".to_string();
+    user.private_key = None;
+    user.public_key = None;
 
-    let user_id = claims.id;
-
-    let read_site = blocking(pool, move |conn| Site::read(conn, 1)).await??;
+    let read_site = blocking(context.pool(), move |conn| Site::read(conn, 1)).await??;
 
     // Make sure user is the creator
-    if read_site.creator_id != user_id {
+    if read_site.creator_id != user.id {
       return Err(APIError::err("not_an_admin").into());
     }
 
-    let site_form = SiteForm {
-      name: read_site.name,
-      description: read_site.description,
-      creator_id: data.user_id,
-      updated: Some(naive_now()),
-      enable_downvotes: read_site.enable_downvotes,
-      open_registration: read_site.open_registration,
-      enable_nsfw: read_site.enable_nsfw,
-    };
-
-    let update_site = move |conn: &'_ _| Site::update(conn, 1, &site_form);
-    if blocking(pool, update_site).await?.is_err() {
+    let new_creator_id = data.user_id;
+    let transfer_site = move |conn: &'_ _| Site::transfer(conn, new_creator_id);
+    if blocking(context.pool(), transfer_site).await?.is_err() {
       return Err(APIError::err("couldnt_update_site").into());
     };
 
     // Mod tables
     let form = ModAddForm {
-      mod_user_id: user_id,
+      mod_user_id: user.id,
       other_user_id: data.user_id,
       removed: Some(false),
     };
 
-    blocking(pool, move |conn| ModAdd::create(conn, &form)).await??;
+    blocking(context.pool(), move |conn| ModAdd::create(conn, &form)).await??;
 
-    let site_view = blocking(pool, move |conn| SiteView::read(conn)).await??;
+    let site_view = blocking(context.pool(), move |conn| SiteView::read(conn)).await??;
 
-    let mut admins = blocking(pool, move |conn| UserView::admins(conn)).await??;
+    let mut admins = blocking(context.pool(), move |conn| UserView::admins(conn)).await??;
     let creator_index = admins
       .iter()
       .position(|r| r.id == site_view.creator_id)
-      .unwrap();
+      .context(location_info!())?;
     let creator_user = admins.remove(creator_index);
     admins.insert(0, creator_user);
 
-    let banned = blocking(pool, move |conn| UserView::banned(conn)).await??;
+    let banned = blocking(context.pool(), move |conn| UserView::banned(conn)).await??;
 
     Ok(GetSiteResponse {
       site: Some(site_view),
       admins,
       banned,
       online: 0,
+      version: version::VERSION.to_string(),
+      my_user: Some(user),
+      federated_instances: Settings::get().get_allowed_instances(),
     })
   }
 }
 
 #[async_trait::async_trait(?Send)]
-impl Perform for Oper<GetSiteConfig> {
+impl Perform for GetSiteConfig {
   type Response = GetSiteConfigResponse;
 
   async fn perform(
     &self,
-    pool: &DbPool,
-    _websocket_info: Option<WebsocketInfo>,
+    context: &Data<LemmyContext>,
+    _websocket_id: Option<ConnectionId>,
   ) -> Result<GetSiteConfigResponse, LemmyError> {
-    let data: &GetSiteConfig = &self.data;
-
-    let claims = match Claims::decode(&data.auth) {
-      Ok(claims) => claims.claims,
-      Err(_e) => return Err(APIError::err("not_logged_in").into()),
-    };
-
-    let user_id = claims.id;
+    let data: &GetSiteConfig = &self;
+    let user = get_user_from_jwt(&data.auth, context.pool()).await?;
 
     // Only let admins read this
-    let admins = blocking(pool, move |conn| UserView::admins(conn)).await??;
-    let admin_ids: Vec<i32> = admins.into_iter().map(|m| m.id).collect();
-
-    if !admin_ids.contains(&user_id) {
-      return Err(APIError::err("not_an_admin").into());
-    }
+    is_admin(context.pool(), user.id).await?;
 
     let config_hjson = Settings::read_config_file()?;
 
@@ -703,28 +684,22 @@ impl Perform for Oper<GetSiteConfig> {
 }
 
 #[async_trait::async_trait(?Send)]
-impl Perform for Oper<SaveSiteConfig> {
+impl Perform for SaveSiteConfig {
   type Response = GetSiteConfigResponse;
 
   async fn perform(
     &self,
-    pool: &DbPool,
-    _websocket_info: Option<WebsocketInfo>,
+    context: &Data<LemmyContext>,
+    _websocket_id: Option<ConnectionId>,
   ) -> Result<GetSiteConfigResponse, LemmyError> {
-    let data: &SaveSiteConfig = &self.data;
-
-    let claims = match Claims::decode(&data.auth) {
-      Ok(claims) => claims.claims,
-      Err(_e) => return Err(APIError::err("not_logged_in").into()),
-    };
-
-    let user_id = claims.id;
+    let data: &SaveSiteConfig = &self;
+    let user = get_user_from_jwt(&data.auth, context.pool()).await?;
 
     // Only let admins read this
-    let admins = blocking(pool, move |conn| UserView::admins(conn)).await??;
+    let admins = blocking(context.pool(), move |conn| UserView::admins(conn)).await??;
     let admin_ids: Vec<i32> = admins.into_iter().map(|m| m.id).collect();
 
-    if !admin_ids.contains(&user_id) {
+    if !admin_ids.contains(&user.id) {
       return Err(APIError::err("not_an_admin").into());
     }
 
index d547f64b229e4bc9350c157abfb54f8c690eea1f..e97a6d33beb6f8017ee99d1f428980db941d0f4b 100644 (file)
@@ -1,21 +1,35 @@
 use crate::{
-  api::{claims::Claims, APIError, Oper, Perform},
+  api::{
+    check_slurs,
+    claims::Claims,
+    get_user_from_jwt,
+    get_user_from_jwt_opt,
+    is_admin,
+    APIError,
+    Perform,
+  },
   apub::ApubObjectType,
   blocking,
+  captcha_espeak_wav_base64,
   websocket::{
-    server::{JoinUserRoom, SendAllMessage, SendUserRoomMessage},
+    server::{CaptchaItem, CheckCaptcha, JoinUserRoom, SendAllMessage, SendUserRoomMessage},
     UserOperation,
-    WebsocketInfo,
   },
-  DbPool,
+  ConnectionId,
+  LemmyContext,
   LemmyError,
 };
+use actix_web::web::Data;
+use anyhow::Context;
 use bcrypt::verify;
+use captcha::{gen, Difficulty};
+use chrono::Duration;
 use lemmy_db::{
   comment::*,
   comment_view::*,
   community::*,
   community_view::*,
+  diesel_option_overwrite,
   moderator::*,
   naive_now,
   password_reset_request::*,
@@ -38,14 +52,14 @@ use lemmy_db::{
 use lemmy_utils::{
   generate_actor_keypair,
   generate_random_string,
+  is_valid_preferred_username,
   is_valid_username,
+  location_info,
   make_apub_endpoint,
   naive_from_unix,
   remove_slurs,
   send_email,
   settings::Settings,
-  slur_check,
-  slurs_vec_to_str,
   EndpointType,
 };
 use log::error;
@@ -66,6 +80,23 @@ pub struct Register {
   pub password_verify: String,
   pub admin: bool,
   pub show_nsfw: bool,
+  pub captcha_uuid: Option<String>,
+  pub captcha_answer: Option<String>,
+}
+
+#[derive(Serialize, Deserialize)]
+pub struct GetCaptcha {}
+
+#[derive(Serialize, Deserialize)]
+pub struct GetCaptchaResponse {
+  ok: Option<CaptchaResponse>,
+}
+
+#[derive(Serialize, Deserialize)]
+pub struct CaptchaResponse {
+  png: String,         // A Base64 encoded png
+  wav: Option<String>, // A Base64 encoded wav audio
+  uuid: String,
 }
 
 #[derive(Serialize, Deserialize)]
@@ -76,7 +107,10 @@ pub struct SaveUserSettings {
   default_listing_type: i16,
   lang: String,
   avatar: Option<String>,
+  banner: Option<String>,
+  preferred_username: Option<String>,
   email: Option<String>,
+  bio: Option<String>,
   matrix_user_id: Option<String>,
   new_password: Option<String>,
   new_password_verify: Option<String>,
@@ -110,7 +144,6 @@ pub struct GetUserDetailsResponse {
   moderates: Vec<CommunityModeratorView>,
   comments: Vec<CommentView>,
   posts: Vec<PostView>,
-  admins: Vec<UserView>,
 }
 
 #[derive(Serialize, Deserialize)]
@@ -144,6 +177,7 @@ pub struct AddAdminResponse {
 pub struct BanUser {
   user_id: i32,
   ban: bool,
+  remove_data: Option<bool>,
   reason: Option<String>,
   expires: Option<i64>,
   auth: String,
@@ -174,9 +208,9 @@ pub struct GetUserMentions {
 }
 
 #[derive(Serialize, Deserialize)]
-pub struct EditUserMention {
+pub struct MarkUserMentionAsRead {
   user_mention_id: i32,
-  read: Option<bool>,
+  read: bool,
   auth: String,
 }
 
@@ -216,9 +250,21 @@ pub struct CreatePrivateMessage {
 #[derive(Serialize, Deserialize)]
 pub struct EditPrivateMessage {
   edit_id: i32,
-  content: Option<String>,
-  deleted: Option<bool>,
-  read: Option<bool>,
+  content: String,
+  auth: String,
+}
+
+#[derive(Serialize, Deserialize)]
+pub struct DeletePrivateMessage {
+  edit_id: i32,
+  deleted: bool,
+  auth: String,
+}
+
+#[derive(Serialize, Deserialize)]
+pub struct MarkPrivateMessageAsRead {
+  edit_id: i32,
+  read: bool,
   auth: String,
 }
 
@@ -251,20 +297,20 @@ pub struct UserJoinResponse {
 }
 
 #[async_trait::async_trait(?Send)]
-impl Perform for Oper<Login> {
+impl Perform for Login {
   type Response = LoginResponse;
 
   async fn perform(
     &self,
-    pool: &DbPool,
-    _websocket_info: Option<WebsocketInfo>,
+    context: &Data<LemmyContext>,
+    _websocket_id: Option<ConnectionId>,
   ) -> Result<LoginResponse, LemmyError> {
-    let data: &Login = &self.data;
+    let data: &Login = &self;
 
     // Fetch that username / email
     let username_or_email = data.username_or_email.clone();
-    let user = match blocking(pool, move |conn| {
-      Claims::find_by_email_or_username(conn, &username_or_email)
+    let user = match blocking(context.pool(), move |conn| {
+      User_::find_by_email_or_username(conn, &username_or_email)
     })
     .await?
     {
@@ -280,24 +326,24 @@ impl Perform for Oper<Login> {
 
     // Return the jwt
     Ok(LoginResponse {
-      jwt: Claims::jwt(user, Settings::get().hostname),
+      jwt: Claims::jwt(user, Settings::get().hostname)?,
     })
   }
 }
 
 #[async_trait::async_trait(?Send)]
-impl Perform for Oper<Register> {
+impl Perform for Register {
   type Response = LoginResponse;
 
   async fn perform(
     &self,
-    pool: &DbPool,
-    _websocket_info: Option<WebsocketInfo>,
+    context: &Data<LemmyContext>,
+    _websocket_id: Option<ConnectionId>,
   ) -> Result<LoginResponse, LemmyError> {
-    let data: &Register = &self.data;
+    let data: &Register = &self;
 
     // Make sure site has open registration
-    if let Ok(site) = blocking(pool, move |conn| SiteView::read(conn)).await? {
+    if let Ok(site) = blocking(context.pool(), move |conn| SiteView::read(conn)).await? {
       let site: SiteView = site;
       if !site.open_registration {
         return Err(APIError::err("registration_closed").into());
@@ -309,12 +355,30 @@ impl Perform for Oper<Register> {
       return Err(APIError::err("passwords_dont_match").into());
     }
 
-    if let Err(slurs) = slur_check(&data.username) {
-      return Err(APIError::err(&slurs_vec_to_str(slurs)).into());
+    // If its not the admin, check the captcha
+    if !data.admin && Settings::get().captcha.enabled {
+      let check = context
+        .chat_server()
+        .send(CheckCaptcha {
+          uuid: data
+            .captcha_uuid
+            .to_owned()
+            .unwrap_or_else(|| "".to_string()),
+          answer: data
+            .captcha_answer
+            .to_owned()
+            .unwrap_or_else(|| "".to_string()),
+        })
+        .await?;
+      if !check {
+        return Err(APIError::err("captcha_incorrect").into());
+      }
     }
 
+    check_slurs(&data.username)?;
+
     // Make sure there are no admins
-    let any_admins = blocking(pool, move |conn| {
+    let any_admins = blocking(context.pool(), move |conn| {
       UserView::admins(conn).map(|a| a.is_empty())
     })
     .await??;
@@ -330,9 +394,10 @@ impl Perform for Oper<Register> {
     // Register the new user
     let user_form = UserForm {
       name: data.username.to_owned(),
-      email: data.email.to_owned(),
+      email: Some(data.email.to_owned()),
       matrix_user_id: None,
       avatar: None,
+      banner: None,
       password_encrypted: data.password.to_owned(),
       preferred_username: None,
       updated: None,
@@ -340,7 +405,7 @@ impl Perform for Oper<Register> {
       banned: false,
       show_nsfw: data.show_nsfw,
       theme: "darkly".into(),
-      default_sort_type: SortType::Hot as i16,
+      default_sort_type: SortType::Active as i16,
       default_listing_type: ListingType::Subscribed as i16,
       lang: "browser".into(),
       show_avatars: true,
@@ -354,7 +419,11 @@ impl Perform for Oper<Register> {
     };
 
     // Create the user
-    let inserted_user = match blocking(pool, move |conn| User_::register(conn, &user_form)).await? {
+    let inserted_user = match blocking(context.pool(), move |conn| {
+      User_::register(conn, &user_form)
+    })
+    .await?
+    {
       Ok(user) => user,
       Err(e) => {
         let err_type = if e.to_string()
@@ -372,7 +441,9 @@ impl Perform for Oper<Register> {
     let main_community_keypair = generate_actor_keypair()?;
 
     // Create the main community if it doesn't exist
-    let main_community = match blocking(pool, move |conn| Community::read(conn, 2)).await? {
+    let main_community = match blocking(context.pool(), move |conn| Community::read(conn, 2))
+      .await?
+    {
       Ok(c) => c,
       Err(_e) => {
         let default_community_name = "main";
@@ -392,8 +463,13 @@ impl Perform for Oper<Register> {
           public_key: Some(main_community_keypair.public_key),
           last_refreshed_at: None,
           published: None,
+          icon: None,
+          banner: None,
         };
-        blocking(pool, move |conn| Community::create(conn, &community_form)).await??
+        blocking(context.pool(), move |conn| {
+          Community::create(conn, &community_form)
+        })
+        .await??
       }
     };
 
@@ -404,7 +480,7 @@ impl Perform for Oper<Register> {
     };
 
     let follow = move |conn: &'_ _| CommunityFollower::follow(conn, &community_follower_form);
-    if blocking(pool, follow).await?.is_err() {
+    if blocking(context.pool(), follow).await?.is_err() {
       return Err(APIError::err("community_follower_already_exists").into());
     };
 
@@ -416,46 +492,104 @@ impl Perform for Oper<Register> {
       };
 
       let join = move |conn: &'_ _| CommunityModerator::join(conn, &community_moderator_form);
-      if blocking(pool, join).await?.is_err() {
+      if blocking(context.pool(), join).await?.is_err() {
         return Err(APIError::err("community_moderator_already_exists").into());
       }
     }
 
     // Return the jwt
     Ok(LoginResponse {
-      jwt: Claims::jwt(inserted_user, Settings::get().hostname),
+      jwt: Claims::jwt(inserted_user, Settings::get().hostname)?,
     })
   }
 }
 
 #[async_trait::async_trait(?Send)]
-impl Perform for Oper<SaveUserSettings> {
-  type Response = LoginResponse;
+impl Perform for GetCaptcha {
+  type Response = GetCaptchaResponse;
 
   async fn perform(
     &self,
-    pool: &DbPool,
-    _websocket_info: Option<WebsocketInfo>,
-  ) -> Result<LoginResponse, LemmyError> {
-    let data: &SaveUserSettings = &self.data;
+    context: &Data<LemmyContext>,
+    _websocket_id: Option<ConnectionId>,
+  ) -> Result<Self::Response, LemmyError> {
+    let captcha_settings = Settings::get().captcha;
+
+    if !captcha_settings.enabled {
+      return Ok(GetCaptchaResponse { ok: None });
+    }
 
-    let claims = match Claims::decode(&data.auth) {
-      Ok(claims) => claims.claims,
-      Err(_e) => return Err(APIError::err("not_logged_in").into()),
+    let captcha = match captcha_settings.difficulty.as_str() {
+      "easy" => gen(Difficulty::Easy),
+      "medium" => gen(Difficulty::Medium),
+      "hard" => gen(Difficulty::Hard),
+      _ => gen(Difficulty::Medium),
     };
 
-    let user_id = claims.id;
+    let answer = captcha.chars_as_string();
 
-    let read_user = blocking(pool, move |conn| User_::read(conn, user_id)).await??;
+    let png_byte_array = captcha.as_png().expect("failed to generate captcha");
 
-    let email = match &data.email {
-      Some(email) => Some(email.to_owned()),
-      None => read_user.email,
+    let png = base64::encode(png_byte_array);
+
+    let uuid = uuid::Uuid::new_v4().to_string();
+
+    let wav = captcha_espeak_wav_base64(&answer).ok();
+
+    let captcha_item = CaptchaItem {
+      answer,
+      uuid: uuid.to_owned(),
+      expires: naive_now() + Duration::minutes(10), // expires in 10 minutes
+    };
+
+    // Stores the captcha item on the queue
+    context.chat_server().do_send(captcha_item);
+
+    Ok(GetCaptchaResponse {
+      ok: Some(CaptchaResponse { png, uuid, wav }),
+    })
+  }
+}
+
+#[async_trait::async_trait(?Send)]
+impl Perform for SaveUserSettings {
+  type Response = LoginResponse;
+
+  async fn perform(
+    &self,
+    context: &Data<LemmyContext>,
+    _websocket_id: Option<ConnectionId>,
+  ) -> Result<LoginResponse, LemmyError> {
+    let data: &SaveUserSettings = &self;
+    let user = get_user_from_jwt(&data.auth, context.pool()).await?;
+
+    let user_id = user.id;
+    let read_user = blocking(context.pool(), move |conn| User_::read(conn, user_id)).await??;
+
+    let bio = match &data.bio {
+      Some(bio) => {
+        if bio.chars().count() <= 300 {
+          Some(bio.to_owned())
+        } else {
+          return Err(APIError::err("bio_length_overflow").into());
+        }
+      }
+      None => read_user.bio,
     };
 
-    let avatar = match &data.avatar {
-      Some(avatar) => Some(avatar.to_owned()),
-      None => read_user.avatar,
+    let avatar = diesel_option_overwrite(&data.avatar);
+    let banner = diesel_option_overwrite(&data.banner);
+    let email = diesel_option_overwrite(&data.email);
+
+    // The DB constraint should stop too many characters
+    let preferred_username = match &data.preferred_username {
+      Some(preferred_username) => {
+        if !is_valid_preferred_username(preferred_username.trim()) {
+          return Err(APIError::err("invalid_username").into());
+        }
+        Some(preferred_username.trim().to_string())
+      }
+      None => read_user.preferred_username,
     };
 
     let password_encrypted = match &data.new_password {
@@ -476,7 +610,7 @@ impl Perform for Oper<SaveUserSettings> {
                   return Err(APIError::err("password_incorrect").into());
                 }
                 let new_password = new_password.to_owned();
-                let user = blocking(pool, move |conn| {
+                let user = blocking(context.pool(), move |conn| {
                   User_::update_password(conn, user_id, &new_password)
                 })
                 .await??;
@@ -496,8 +630,9 @@ impl Perform for Oper<SaveUserSettings> {
       email,
       matrix_user_id: data.matrix_user_id.to_owned(),
       avatar,
+      banner,
       password_encrypted,
-      preferred_username: read_user.preferred_username,
+      preferred_username,
       updated: Some(naive_now()),
       admin: read_user.admin,
       banned: read_user.banned,
@@ -509,14 +644,17 @@ impl Perform for Oper<SaveUserSettings> {
       show_avatars: data.show_avatars,
       send_notifications_to_email: data.send_notifications_to_email,
       actor_id: read_user.actor_id,
-      bio: read_user.bio,
+      bio,
       local: read_user.local,
       private_key: read_user.private_key,
       public_key: read_user.public_key,
       last_refreshed_at: None,
     };
 
-    let res = blocking(pool, move |conn| User_::update(conn, user_id, &user_form)).await?;
+    let res = blocking(context.pool(), move |conn| {
+      User_::update(conn, user_id, &user_form)
+    })
+    .await?;
     let updated_user: User_ = match res {
       Ok(user) => user,
       Err(e) => {
@@ -534,37 +672,25 @@ impl Perform for Oper<SaveUserSettings> {
 
     // Return the jwt
     Ok(LoginResponse {
-      jwt: Claims::jwt(updated_user, Settings::get().hostname),
+      jwt: Claims::jwt(updated_user, Settings::get().hostname)?,
     })
   }
 }
 
 #[async_trait::async_trait(?Send)]
-impl Perform for Oper<GetUserDetails> {
+impl Perform for GetUserDetails {
   type Response = GetUserDetailsResponse;
 
   async fn perform(
     &self,
-    pool: &DbPool,
-    _websocket_info: Option<WebsocketInfo>,
+    context: &Data<LemmyContext>,
+    _websocket_id: Option<ConnectionId>,
   ) -> Result<GetUserDetailsResponse, LemmyError> {
-    let data: &GetUserDetails = &self.data;
-
-    let user_claims: Option<Claims> = match &data.auth {
-      Some(auth) => match Claims::decode(&auth) {
-        Ok(claims) => Some(claims.claims),
-        Err(_e) => None,
-      },
-      None => None,
-    };
-
-    let user_id = match &user_claims {
-      Some(claims) => Some(claims.id),
-      None => None,
-    };
+    let data: &GetUserDetails = &self;
+    let user = get_user_from_jwt_opt(&data.auth, context.pool()).await?;
 
-    let show_nsfw = match &user_claims {
-      Some(claims) => claims.show_nsfw,
+    let show_nsfw = match &user {
+      Some(user) => user.show_nsfw,
       None => false,
     };
 
@@ -577,7 +703,10 @@ impl Perform for Oper<GetUserDetails> {
     let user_details_id = match data.user_id {
       Some(id) => id,
       None => {
-        let user = blocking(pool, move |conn| User_::read_from_name(conn, &username)).await?;
+        let user = blocking(context.pool(), move |conn| {
+          User_::read_from_name(conn, &username)
+        })
+        .await?;
         match user {
           Ok(user) => user.id,
           Err(_e) => return Err(APIError::err("couldnt_find_that_username_or_email").into()),
@@ -585,13 +714,17 @@ impl Perform for Oper<GetUserDetails> {
       }
     };
 
-    let mut user_view = blocking(pool, move |conn| UserView::read(conn, user_details_id)).await??;
+    let user_view = blocking(context.pool(), move |conn| {
+      UserView::get_user_secure(conn, user_details_id)
+    })
+    .await??;
 
     let page = data.page;
     let limit = data.limit;
     let saved_only = data.saved_only;
     let community_id = data.community_id;
-    let (posts, comments) = blocking(pool, move |conn| {
+    let user_id = user.map(|u| u.id);
+    let (posts, comments) = blocking(context.pool(), move |conn| {
       let mut posts_query = PostQueryBuilder::create(conn)
         .sort(&sort)
         .show_nsfw(show_nsfw)
@@ -622,32 +755,15 @@ impl Perform for Oper<GetUserDetails> {
     })
     .await??;
 
-    let follows = blocking(pool, move |conn| {
+    let follows = blocking(context.pool(), move |conn| {
       CommunityFollowerView::for_user(conn, user_details_id)
     })
     .await??;
-    let moderates = blocking(pool, move |conn| {
+    let moderates = blocking(context.pool(), move |conn| {
       CommunityModeratorView::for_user(conn, user_details_id)
     })
     .await??;
 
-    let site_creator_id =
-      blocking(pool, move |conn| Site::read(conn, 1).map(|s| s.creator_id)).await??;
-
-    let mut admins = blocking(pool, move |conn| UserView::admins(conn)).await??;
-    let creator_index = admins.iter().position(|r| r.id == site_creator_id).unwrap();
-    let creator_user = admins.remove(creator_index);
-    admins.insert(0, creator_user);
-
-    // If its not the same user, remove the email
-    if let Some(user_id) = user_id {
-      if user_details_id != user_id {
-        user_view.email = None;
-      }
-    } else {
-      user_view.email = None;
-    }
-
     // Return the jwt
     Ok(GetUserDetailsResponse {
       user: user_view,
@@ -655,104 +771,109 @@ impl Perform for Oper<GetUserDetails> {
       moderates,
       comments,
       posts,
-      admins,
     })
   }
 }
 
 #[async_trait::async_trait(?Send)]
-impl Perform for Oper<AddAdmin> {
+impl Perform for AddAdmin {
   type Response = AddAdminResponse;
 
   async fn perform(
     &self,
-    pool: &DbPool,
-    websocket_info: Option<WebsocketInfo>,
+    context: &Data<LemmyContext>,
+    websocket_id: Option<ConnectionId>,
   ) -> Result<AddAdminResponse, LemmyError> {
-    let data: &AddAdmin = &self.data;
-
-    let claims = match Claims::decode(&data.auth) {
-      Ok(claims) => claims.claims,
-      Err(_e) => return Err(APIError::err("not_logged_in").into()),
-    };
-
-    let user_id = claims.id;
+    let data: &AddAdmin = &self;
+    let user = get_user_from_jwt(&data.auth, context.pool()).await?;
 
     // Make sure user is an admin
-    let is_admin = move |conn: &'_ _| UserView::read(conn, user_id).map(|u| u.admin);
-    if !blocking(pool, is_admin).await?? {
-      return Err(APIError::err("not_an_admin").into());
-    }
+    is_admin(context.pool(), user.id).await?;
 
     let added = data.added;
     let added_user_id = data.user_id;
     let add_admin = move |conn: &'_ _| User_::add_admin(conn, added_user_id, added);
-    if blocking(pool, add_admin).await?.is_err() {
+    if blocking(context.pool(), add_admin).await?.is_err() {
       return Err(APIError::err("couldnt_update_user").into());
     }
 
     // Mod tables
     let form = ModAddForm {
-      mod_user_id: user_id,
+      mod_user_id: user.id,
       other_user_id: data.user_id,
       removed: Some(!data.added),
     };
 
-    blocking(pool, move |conn| ModAdd::create(conn, &form)).await??;
+    blocking(context.pool(), move |conn| ModAdd::create(conn, &form)).await??;
 
-    let site_creator_id =
-      blocking(pool, move |conn| Site::read(conn, 1).map(|s| s.creator_id)).await??;
+    let site_creator_id = blocking(context.pool(), move |conn| {
+      Site::read(conn, 1).map(|s| s.creator_id)
+    })
+    .await??;
 
-    let mut admins = blocking(pool, move |conn| UserView::admins(conn)).await??;
-    let creator_index = admins.iter().position(|r| r.id == site_creator_id).unwrap();
+    let mut admins = blocking(context.pool(), move |conn| UserView::admins(conn)).await??;
+    let creator_index = admins
+      .iter()
+      .position(|r| r.id == site_creator_id)
+      .context(location_info!())?;
     let creator_user = admins.remove(creator_index);
     admins.insert(0, creator_user);
 
     let res = AddAdminResponse { admins };
 
-    if let Some(ws) = websocket_info {
-      ws.chatserver.do_send(SendAllMessage {
-        op: UserOperation::AddAdmin,
-        response: res.clone(),
-        my_id: ws.id,
-      });
-    }
+    context.chat_server().do_send(SendAllMessage {
+      op: UserOperation::AddAdmin,
+      response: res.clone(),
+      websocket_id,
+    });
 
     Ok(res)
   }
 }
 
 #[async_trait::async_trait(?Send)]
-impl Perform for Oper<BanUser> {
+impl Perform for BanUser {
   type Response = BanUserResponse;
 
   async fn perform(
     &self,
-    pool: &DbPool,
-    websocket_info: Option<WebsocketInfo>,
+    context: &Data<LemmyContext>,
+    websocket_id: Option<ConnectionId>,
   ) -> Result<BanUserResponse, LemmyError> {
-    let data: &BanUser = &self.data;
-
-    let claims = match Claims::decode(&data.auth) {
-      Ok(claims) => claims.claims,
-      Err(_e) => return Err(APIError::err("not_logged_in").into()),
-    };
-
-    let user_id = claims.id;
+    let data: &BanUser = &self;
+    let user = get_user_from_jwt(&data.auth, context.pool()).await?;
 
     // Make sure user is an admin
-    let is_admin = move |conn: &'_ _| UserView::read(conn, user_id).map(|u| u.admin);
-    if !blocking(pool, is_admin).await?? {
-      return Err(APIError::err("not_an_admin").into());
-    }
+    is_admin(context.pool(), user.id).await?;
 
     let ban = data.ban;
     let banned_user_id = data.user_id;
     let ban_user = move |conn: &'_ _| User_::ban_user(conn, banned_user_id, ban);
-    if blocking(pool, ban_user).await?.is_err() {
+    if blocking(context.pool(), ban_user).await?.is_err() {
       return Err(APIError::err("couldnt_update_user").into());
     }
 
+    // Remove their data if that's desired
+    if let Some(remove_data) = data.remove_data {
+      // Posts
+      blocking(context.pool(), move |conn: &'_ _| {
+        Post::update_removed_for_creator(conn, banned_user_id, None, remove_data)
+      })
+      .await??;
+
+      // Communities
+      blocking(context.pool(), move |conn: &'_ _| {
+        Community::update_removed_for_creator(conn, banned_user_id, remove_data)
+      })
+      .await??;
+
+      // Comments
+      blocking(context.pool(), move |conn: &'_ _| {
+        Comment::update_removed_for_creator(conn, banned_user_id, remove_data)
+      })
+      .await??;
+    }
+
     // Mod tables
     let expires = match data.expires {
       Some(time) => Some(naive_from_unix(time)),
@@ -760,59 +881,55 @@ impl Perform for Oper<BanUser> {
     };
 
     let form = ModBanForm {
-      mod_user_id: user_id,
+      mod_user_id: user.id,
       other_user_id: data.user_id,
       reason: data.reason.to_owned(),
       banned: Some(data.ban),
       expires,
     };
 
-    blocking(pool, move |conn| ModBan::create(conn, &form)).await??;
+    blocking(context.pool(), move |conn| ModBan::create(conn, &form)).await??;
 
     let user_id = data.user_id;
-    let user_view = blocking(pool, move |conn| UserView::read(conn, user_id)).await??;
+    let user_view = blocking(context.pool(), move |conn| {
+      UserView::get_user_secure(conn, user_id)
+    })
+    .await??;
 
     let res = BanUserResponse {
       user: user_view,
       banned: data.ban,
     };
 
-    if let Some(ws) = websocket_info {
-      ws.chatserver.do_send(SendAllMessage {
-        op: UserOperation::BanUser,
-        response: res.clone(),
-        my_id: ws.id,
-      });
-    }
+    context.chat_server().do_send(SendAllMessage {
+      op: UserOperation::BanUser,
+      response: res.clone(),
+      websocket_id,
+    });
 
     Ok(res)
   }
 }
 
 #[async_trait::async_trait(?Send)]
-impl Perform for Oper<GetReplies> {
+impl Perform for GetReplies {
   type Response = GetRepliesResponse;
 
   async fn perform(
     &self,
-    pool: &DbPool,
-    _websocket_info: Option<WebsocketInfo>,
+    context: &Data<LemmyContext>,
+    _websocket_id: Option<ConnectionId>,
   ) -> Result<GetRepliesResponse, LemmyError> {
-    let data: &GetReplies = &self.data;
-
-    let claims = match Claims::decode(&data.auth) {
-      Ok(claims) => claims.claims,
-      Err(_e) => return Err(APIError::err("not_logged_in").into()),
-    };
-
-    let user_id = claims.id;
+    let data: &GetReplies = &self;
+    let user = get_user_from_jwt(&data.auth, context.pool()).await?;
 
     let sort = SortType::from_str(&data.sort)?;
 
     let page = data.page;
     let limit = data.limit;
     let unread_only = data.unread_only;
-    let replies = blocking(pool, move |conn| {
+    let user_id = user.id;
+    let replies = blocking(context.pool(), move |conn| {
       ReplyQueryBuilder::create(conn, user_id)
         .sort(&sort)
         .unread_only(unread_only)
@@ -827,29 +944,24 @@ impl Perform for Oper<GetReplies> {
 }
 
 #[async_trait::async_trait(?Send)]
-impl Perform for Oper<GetUserMentions> {
+impl Perform for GetUserMentions {
   type Response = GetUserMentionsResponse;
 
   async fn perform(
     &self,
-    pool: &DbPool,
-    _websocket_info: Option<WebsocketInfo>,
+    context: &Data<LemmyContext>,
+    _websocket_id: Option<ConnectionId>,
   ) -> Result<GetUserMentionsResponse, LemmyError> {
-    let data: &GetUserMentions = &self.data;
-
-    let claims = match Claims::decode(&data.auth) {
-      Ok(claims) => claims.claims,
-      Err(_e) => return Err(APIError::err("not_logged_in").into()),
-    };
-
-    let user_id = claims.id;
+    let data: &GetUserMentions = &self;
+    let user = get_user_from_jwt(&data.auth, context.pool()).await?;
 
     let sort = SortType::from_str(&data.sort)?;
 
     let page = data.page;
     let limit = data.limit;
     let unread_only = data.unread_only;
-    let mentions = blocking(pool, move |conn| {
+    let user_id = user.id;
+    let mentions = blocking(context.pool(), move |conn| {
       UserMentionQueryBuilder::create(conn, user_id)
         .sort(&sort)
         .unread_only(unread_only)
@@ -864,46 +976,37 @@ impl Perform for Oper<GetUserMentions> {
 }
 
 #[async_trait::async_trait(?Send)]
-impl Perform for Oper<EditUserMention> {
+impl Perform for MarkUserMentionAsRead {
   type Response = UserMentionResponse;
 
   async fn perform(
     &self,
-    pool: &DbPool,
-    _websocket_info: Option<WebsocketInfo>,
+    context: &Data<LemmyContext>,
+    _websocket_id: Option<ConnectionId>,
   ) -> Result<UserMentionResponse, LemmyError> {
-    let data: &EditUserMention = &self.data;
-
-    let claims = match Claims::decode(&data.auth) {
-      Ok(claims) => claims.claims,
-      Err(_e) => return Err(APIError::err("not_logged_in").into()),
-    };
-
-    let user_id = claims.id;
+    let data: &MarkUserMentionAsRead = &self;
+    let user = get_user_from_jwt(&data.auth, context.pool()).await?;
 
     let user_mention_id = data.user_mention_id;
-    let read_user_mention =
-      blocking(pool, move |conn| UserMention::read(conn, user_mention_id)).await??;
+    let read_user_mention = blocking(context.pool(), move |conn| {
+      UserMention::read(conn, user_mention_id)
+    })
+    .await??;
 
-    if user_id != read_user_mention.recipient_id {
+    if user.id != read_user_mention.recipient_id {
       return Err(APIError::err("couldnt_update_comment").into());
     }
 
-    let user_mention_form = UserMentionForm {
-      recipient_id: read_user_mention.recipient_id,
-      comment_id: read_user_mention.comment_id,
-      read: data.read.to_owned(),
-    };
-
     let user_mention_id = read_user_mention.id;
-    let update_mention =
-      move |conn: &'_ _| UserMention::update(conn, user_mention_id, &user_mention_form);
-    if blocking(pool, update_mention).await?.is_err() {
+    let read = data.read;
+    let update_mention = move |conn: &'_ _| UserMention::update_read(conn, user_mention_id, read);
+    if blocking(context.pool(), update_mention).await?.is_err() {
       return Err(APIError::err("couldnt_update_comment").into());
     };
 
     let user_mention_id = read_user_mention.id;
-    let user_mention_view = blocking(pool, move |conn| {
+    let user_id = user.id;
+    let user_mention_view = blocking(context.pool(), move |conn| {
       UserMentionView::read(conn, user_mention_id, user_id)
     })
     .await??;
@@ -915,24 +1018,19 @@ impl Perform for Oper<EditUserMention> {
 }
 
 #[async_trait::async_trait(?Send)]
-impl Perform for Oper<MarkAllAsRead> {
+impl Perform for MarkAllAsRead {
   type Response = GetRepliesResponse;
 
   async fn perform(
     &self,
-    pool: &DbPool,
-    _websocket_info: Option<WebsocketInfo>,
+    context: &Data<LemmyContext>,
+    _websocket_id: Option<ConnectionId>,
   ) -> Result<GetRepliesResponse, LemmyError> {
-    let data: &MarkAllAsRead = &self.data;
-
-    let claims = match Claims::decode(&data.auth) {
-      Ok(claims) => claims.claims,
-      Err(_e) => return Err(APIError::err("not_logged_in").into()),
-    };
-
-    let user_id = claims.id;
+    let data: &MarkAllAsRead = &self;
+    let user = get_user_from_jwt(&data.auth, context.pool()).await?;
 
-    let replies = blocking(pool, move |conn| {
+    let user_id = user.id;
+    let replies = blocking(context.pool(), move |conn| {
       ReplyQueryBuilder::create(conn, user_id)
         .unread_only(true)
         .page(1)
@@ -942,70 +1040,29 @@ impl Perform for Oper<MarkAllAsRead> {
     .await??;
 
     // TODO: this should probably be a bulk operation
+    // Not easy to do as a bulk operation,
+    // because recipient_id isn't in the comment table
     for reply in &replies {
       let reply_id = reply.id;
-      let mark_as_read = move |conn: &'_ _| Comment::mark_as_read(conn, reply_id);
-      if blocking(pool, mark_as_read).await?.is_err() {
+      let mark_as_read = move |conn: &'_ _| Comment::update_read(conn, reply_id, true);
+      if blocking(context.pool(), mark_as_read).await?.is_err() {
         return Err(APIError::err("couldnt_update_comment").into());
       }
     }
 
-    // Mentions
-    let mentions = blocking(pool, move |conn| {
-      UserMentionQueryBuilder::create(conn, user_id)
-        .unread_only(true)
-        .page(1)
-        .limit(999)
-        .list()
-    })
-    .await??;
-
-    // TODO: this should probably be a bulk operation
-    for mention in &mentions {
-      let mention_form = UserMentionForm {
-        recipient_id: mention.to_owned().recipient_id,
-        comment_id: mention.to_owned().id,
-        read: Some(true),
-      };
-
-      let user_mention_id = mention.user_mention_id;
-      let update_mention =
-        move |conn: &'_ _| UserMention::update(conn, user_mention_id, &mention_form);
-      if blocking(pool, update_mention).await?.is_err() {
-        return Err(APIError::err("couldnt_update_comment").into());
-      }
+    // Mark all user mentions as read
+    let update_user_mentions = move |conn: &'_ _| UserMention::mark_all_as_read(conn, user_id);
+    if blocking(context.pool(), update_user_mentions)
+      .await?
+      .is_err()
+    {
+      return Err(APIError::err("couldnt_update_comment").into());
     }
 
-    // messages
-    let messages = blocking(pool, move |conn| {
-      PrivateMessageQueryBuilder::create(conn, user_id)
-        .page(1)
-        .limit(999)
-        .unread_only(true)
-        .list()
-    })
-    .await??;
-
-    // TODO: this should probably be a bulk operation
-    for message in &messages {
-      let private_message_form = PrivateMessageForm {
-        content: message.to_owned().content,
-        creator_id: message.to_owned().creator_id,
-        recipient_id: message.to_owned().recipient_id,
-        deleted: None,
-        read: Some(true),
-        updated: None,
-        ap_id: message.to_owned().ap_id,
-        local: message.local,
-        published: None,
-      };
-
-      let message_id = message.id;
-      let update_pm =
-        move |conn: &'_ _| PrivateMessage::update(conn, message_id, &private_message_form);
-      if blocking(pool, update_pm).await?.is_err() {
-        return Err(APIError::err("couldnt_update_private_message").into());
-      }
+    // Mark all private_messages as read
+    let update_pm = move |conn: &'_ _| PrivateMessage::mark_all_as_read(conn, user_id);
+    if blocking(context.pool(), update_pm).await?.is_err() {
+      return Err(APIError::err("couldnt_update_private_message").into());
     }
 
     Ok(GetRepliesResponse { replies: vec![] })
@@ -1013,24 +1070,16 @@ impl Perform for Oper<MarkAllAsRead> {
 }
 
 #[async_trait::async_trait(?Send)]
-impl Perform for Oper<DeleteAccount> {
+impl Perform for DeleteAccount {
   type Response = LoginResponse;
 
   async fn perform(
     &self,
-    pool: &DbPool,
-    _websocket_info: Option<WebsocketInfo>,
+    context: &Data<LemmyContext>,
+    _websocket_id: Option<ConnectionId>,
   ) -> Result<LoginResponse, LemmyError> {
-    let data: &DeleteAccount = &self.data;
-
-    let claims = match Claims::decode(&data.auth) {
-      Ok(claims) => claims.claims,
-      Err(_e) => return Err(APIError::err("not_logged_in").into()),
-    };
-
-    let user_id = claims.id;
-
-    let user = blocking(pool, move |conn| User_::read(conn, user_id)).await??;
+    let data: &DeleteAccount = &self;
+    let user = get_user_from_jwt(&data.auth, context.pool()).await?;
 
     // Verify the password
     let valid: bool = verify(&data.password, &user.password_encrypted).unwrap_or(false);
@@ -1039,40 +1088,16 @@ impl Perform for Oper<DeleteAccount> {
     }
 
     // Comments
-    let comments = blocking(pool, move |conn| {
-      CommentQueryBuilder::create(conn)
-        .for_creator_id(user_id)
-        .limit(std::i64::MAX)
-        .list()
-    })
-    .await??;
-
-    // TODO: this should probably be a bulk operation
-    for comment in &comments {
-      let comment_id = comment.id;
-      let permadelete = move |conn: &'_ _| Comment::permadelete(conn, comment_id);
-      if blocking(pool, permadelete).await?.is_err() {
-        return Err(APIError::err("couldnt_update_comment").into());
-      }
+    let user_id = user.id;
+    let permadelete = move |conn: &'_ _| Comment::permadelete_for_creator(conn, user_id);
+    if blocking(context.pool(), permadelete).await?.is_err() {
+      return Err(APIError::err("couldnt_update_comment").into());
     }
 
     // Posts
-    let posts = blocking(pool, move |conn| {
-      PostQueryBuilder::create(conn)
-        .sort(&SortType::New)
-        .for_creator_id(user_id)
-        .limit(std::i64::MAX)
-        .list()
-    })
-    .await??;
-
-    // TODO: this should probably be a bulk operation
-    for post in &posts {
-      let post_id = post.id;
-      let permadelete = move |conn: &'_ _| Post::permadelete(conn, post_id);
-      if blocking(pool, permadelete).await?.is_err() {
-        return Err(APIError::err("couldnt_update_post").into());
-      }
+    let permadelete = move |conn: &'_ _| Post::permadelete_for_creator(conn, user_id);
+    if blocking(context.pool(), permadelete).await?.is_err() {
+      return Err(APIError::err("couldnt_update_post").into());
     }
 
     Ok(LoginResponse {
@@ -1082,19 +1107,23 @@ impl Perform for Oper<DeleteAccount> {
 }
 
 #[async_trait::async_trait(?Send)]
-impl Perform for Oper<PasswordReset> {
+impl Perform for PasswordReset {
   type Response = PasswordResetResponse;
 
   async fn perform(
     &self,
-    pool: &DbPool,
-    _websocket_info: Option<WebsocketInfo>,
+    context: &Data<LemmyContext>,
+    _websocket_id: Option<ConnectionId>,
   ) -> Result<PasswordResetResponse, LemmyError> {
-    let data: &PasswordReset = &self.data;
+    let data: &PasswordReset = &self;
 
     // Fetch that email
     let email = data.email.clone();
-    let user = match blocking(pool, move |conn| User_::find_by_email(conn, &email)).await? {
+    let user = match blocking(context.pool(), move |conn| {
+      User_::find_by_email(conn, &email)
+    })
+    .await?
+    {
       Ok(user) => user,
       Err(_e) => return Err(APIError::err("couldnt_find_that_username_or_email").into()),
     };
@@ -1105,7 +1134,7 @@ impl Perform for Oper<PasswordReset> {
     // Insert the row
     let token2 = token.clone();
     let user_id = user.id;
-    blocking(pool, move |conn| {
+    blocking(context.pool(), move |conn| {
       PasswordResetRequest::create_token(conn, user_id, &token2)
     })
     .await??;
@@ -1126,19 +1155,19 @@ impl Perform for Oper<PasswordReset> {
 }
 
 #[async_trait::async_trait(?Send)]
-impl Perform for Oper<PasswordChange> {
+impl Perform for PasswordChange {
   type Response = LoginResponse;
 
   async fn perform(
     &self,
-    pool: &DbPool,
-    _websocket_info: Option<WebsocketInfo>,
+    context: &Data<LemmyContext>,
+    _websocket_id: Option<ConnectionId>,
   ) -> Result<LoginResponse, LemmyError> {
-    let data: &PasswordChange = &self.data;
+    let data: &PasswordChange = &self;
 
     // Fetch the user_id from the token
     let token = data.token.clone();
-    let user_id = blocking(pool, move |conn| {
+    let user_id = blocking(context.pool(), move |conn| {
       PasswordResetRequest::read_from_token(conn, &token).map(|p| p.user_id)
     })
     .await??;
@@ -1150,7 +1179,7 @@ impl Perform for Oper<PasswordChange> {
 
     // Update the user with the new password
     let password = data.password.clone();
-    let updated_user = match blocking(pool, move |conn| {
+    let updated_user = match blocking(context.pool(), move |conn| {
       User_::update_password(conn, user_id, &password)
     })
     .await?
@@ -1161,42 +1190,30 @@ impl Perform for Oper<PasswordChange> {
 
     // Return the jwt
     Ok(LoginResponse {
-      jwt: Claims::jwt(updated_user, Settings::get().hostname),
+      jwt: Claims::jwt(updated_user, Settings::get().hostname)?,
     })
   }
 }
 
 #[async_trait::async_trait(?Send)]
-impl Perform for Oper<CreatePrivateMessage> {
+impl Perform for CreatePrivateMessage {
   type Response = PrivateMessageResponse;
 
   async fn perform(
     &self,
-    pool: &DbPool,
-    websocket_info: Option<WebsocketInfo>,
+    context: &Data<LemmyContext>,
+    websocket_id: Option<ConnectionId>,
   ) -> Result<PrivateMessageResponse, LemmyError> {
-    let data: &CreatePrivateMessage = &self.data;
-
-    let claims = match Claims::decode(&data.auth) {
-      Ok(claims) => claims.claims,
-      Err(_e) => return Err(APIError::err("not_logged_in").into()),
-    };
-
-    let user_id = claims.id;
+    let data: &CreatePrivateMessage = &self;
+    let user = get_user_from_jwt(&data.auth, context.pool()).await?;
 
     let hostname = &format!("https://{}", Settings::get().hostname);
 
-    // Check for a site ban
-    let user = blocking(pool, move |conn| User_::read(conn, user_id)).await??;
-    if user.banned {
-      return Err(APIError::err("site_ban").into());
-    }
-
     let content_slurs_removed = remove_slurs(&data.content.to_owned());
 
     let private_message_form = PrivateMessageForm {
       content: content_slurs_removed.to_owned(),
-      creator_id: user_id,
+      creator_id: user.id,
       recipient_id: data.recipient_id,
       deleted: None,
       read: None,
@@ -1206,7 +1223,7 @@ impl Perform for Oper<CreatePrivateMessage> {
       published: None,
     };
 
-    let inserted_private_message = match blocking(pool, move |conn| {
+    let inserted_private_message = match blocking(context.pool(), move |conn| {
       PrivateMessage::create(conn, &private_message_form)
     })
     .await?
@@ -1218,7 +1235,7 @@ impl Perform for Oper<CreatePrivateMessage> {
     };
 
     let inserted_private_message_id = inserted_private_message.id;
-    let updated_private_message = match blocking(pool, move |conn| {
+    let updated_private_message = match blocking(context.pool(), move |conn| {
       let apub_id = make_apub_endpoint(
         EndpointType::PrivateMessage,
         &inserted_private_message_id.to_string(),
@@ -1232,23 +1249,22 @@ impl Perform for Oper<CreatePrivateMessage> {
       Err(_e) => return Err(APIError::err("couldnt_create_private_message").into()),
     };
 
-    updated_private_message
-      .send_create(&user, &self.client, pool)
-      .await?;
+    updated_private_message.send_create(&user, context).await?;
 
     // Send notifications to the recipient
     let recipient_id = data.recipient_id;
-    let recipient_user = blocking(pool, move |conn| User_::read(conn, recipient_id)).await??;
+    let recipient_user =
+      blocking(context.pool(), move |conn| User_::read(conn, recipient_id)).await??;
     if recipient_user.send_notifications_to_email {
       if let Some(email) = recipient_user.email {
         let subject = &format!(
           "{} - Private Message from {}",
           Settings::get().hostname,
-          claims.username
+          user.name,
         );
         let html = &format!(
           "<h1>Private Message</h1><br><div>{} - {}</div><br><a href={}/inbox>inbox</a>",
-          claims.username, &content_slurs_removed, hostname
+          user.name, &content_slurs_removed, hostname
         );
         match send_email(subject, &email, &recipient_user.name, html) {
           Ok(_o) => _o,
@@ -1257,97 +1273,108 @@ impl Perform for Oper<CreatePrivateMessage> {
       }
     }
 
-    let message = blocking(pool, move |conn| {
+    let message = blocking(context.pool(), move |conn| {
       PrivateMessageView::read(conn, inserted_private_message.id)
     })
     .await??;
 
     let res = PrivateMessageResponse { message };
 
-    if let Some(ws) = websocket_info {
-      ws.chatserver.do_send(SendUserRoomMessage {
-        op: UserOperation::CreatePrivateMessage,
-        response: res.clone(),
-        recipient_id: recipient_user.id,
-        my_id: ws.id,
-      });
-    }
+    context.chat_server().do_send(SendUserRoomMessage {
+      op: UserOperation::CreatePrivateMessage,
+      response: res.clone(),
+      recipient_id,
+      websocket_id,
+    });
 
     Ok(res)
   }
 }
 
 #[async_trait::async_trait(?Send)]
-impl Perform for Oper<EditPrivateMessage> {
+impl Perform for EditPrivateMessage {
   type Response = PrivateMessageResponse;
 
   async fn perform(
     &self,
-    pool: &DbPool,
-    websocket_info: Option<WebsocketInfo>,
+    context: &Data<LemmyContext>,
+    websocket_id: Option<ConnectionId>,
   ) -> Result<PrivateMessageResponse, LemmyError> {
-    let data: &EditPrivateMessage = &self.data;
+    let data: &EditPrivateMessage = &self;
+    let user = get_user_from_jwt(&data.auth, context.pool()).await?;
 
-    let claims = match Claims::decode(&data.auth) {
-      Ok(claims) => claims.claims,
-      Err(_e) => return Err(APIError::err("not_logged_in").into()),
+    // Checking permissions
+    let edit_id = data.edit_id;
+    let orig_private_message = blocking(context.pool(), move |conn| {
+      PrivateMessage::read(conn, edit_id)
+    })
+    .await??;
+    if user.id != orig_private_message.creator_id {
+      return Err(APIError::err("no_private_message_edit_allowed").into());
+    }
+
+    // Doing the update
+    let content_slurs_removed = remove_slurs(&data.content);
+    let edit_id = data.edit_id;
+    let updated_private_message = match blocking(context.pool(), move |conn| {
+      PrivateMessage::update_content(conn, edit_id, &content_slurs_removed)
+    })
+    .await?
+    {
+      Ok(private_message) => private_message,
+      Err(_e) => return Err(APIError::err("couldnt_update_private_message").into()),
     };
 
-    let user_id = claims.id;
+    // Send the apub update
+    updated_private_message.send_update(&user, context).await?;
 
     let edit_id = data.edit_id;
-    let orig_private_message =
-      blocking(pool, move |conn| PrivateMessage::read(conn, edit_id)).await??;
+    let message = blocking(context.pool(), move |conn| {
+      PrivateMessageView::read(conn, edit_id)
+    })
+    .await??;
+    let recipient_id = message.recipient_id;
 
-    // Check for a site ban
-    let user = blocking(pool, move |conn| User_::read(conn, user_id)).await??;
-    if user.banned {
-      return Err(APIError::err("site_ban").into());
-    }
+    let res = PrivateMessageResponse { message };
 
-    // Check to make sure they are the creator (or the recipient marking as read
-    if !(data.read.is_some() && orig_private_message.recipient_id.eq(&user_id)
-      || orig_private_message.creator_id.eq(&user_id))
-    {
-      return Err(APIError::err("no_private_message_edit_allowed").into());
-    }
+    context.chat_server().do_send(SendUserRoomMessage {
+      op: UserOperation::EditPrivateMessage,
+      response: res.clone(),
+      recipient_id,
+      websocket_id,
+    });
 
-    let content_slurs_removed = match &data.content {
-      Some(content) => remove_slurs(content),
-      None => orig_private_message.content.clone(),
-    };
+    Ok(res)
+  }
+}
 
-    let private_message_form = {
-      if data.read.is_some() {
-        PrivateMessageForm {
-          content: orig_private_message.content.to_owned(),
-          creator_id: orig_private_message.creator_id,
-          recipient_id: orig_private_message.recipient_id,
-          read: data.read.to_owned(),
-          updated: orig_private_message.updated,
-          deleted: Some(orig_private_message.deleted),
-          ap_id: orig_private_message.ap_id,
-          local: orig_private_message.local,
-          published: None,
-        }
-      } else {
-        PrivateMessageForm {
-          content: content_slurs_removed,
-          creator_id: orig_private_message.creator_id,
-          recipient_id: orig_private_message.recipient_id,
-          deleted: data.deleted.to_owned(),
-          read: Some(orig_private_message.read),
-          updated: Some(naive_now()),
-          ap_id: orig_private_message.ap_id,
-          local: orig_private_message.local,
-          published: None,
-        }
-      }
-    };
+#[async_trait::async_trait(?Send)]
+impl Perform for DeletePrivateMessage {
+  type Response = PrivateMessageResponse;
+
+  async fn perform(
+    &self,
+    context: &Data<LemmyContext>,
+    websocket_id: Option<ConnectionId>,
+  ) -> Result<PrivateMessageResponse, LemmyError> {
+    let data: &DeletePrivateMessage = &self;
+    let user = get_user_from_jwt(&data.auth, context.pool()).await?;
+
+    // Checking permissions
+    let edit_id = data.edit_id;
+    let orig_private_message = blocking(context.pool(), move |conn| {
+      PrivateMessage::read(conn, edit_id)
+    })
+    .await??;
+    if user.id != orig_private_message.creator_id {
+      return Err(APIError::err("no_private_message_edit_allowed").into());
+    }
 
+    // Doing the update
     let edit_id = data.edit_id;
-    let updated_private_message = match blocking(pool, move |conn| {
-      PrivateMessage::update(conn, edit_id, &private_message_form)
+    let deleted = data.deleted;
+    let updated_private_message = match blocking(context.pool(), move |conn| {
+      PrivateMessage::update_deleted(conn, edit_id, deleted)
     })
     .await?
     {
@@ -1355,68 +1382,108 @@ impl Perform for Oper<EditPrivateMessage> {
       Err(_e) => return Err(APIError::err("couldnt_update_private_message").into()),
     };
 
-    if data.read.is_none() {
-      if let Some(deleted) = data.deleted.to_owned() {
-        if deleted {
-          updated_private_message
-            .send_delete(&user, &self.client, pool)
-            .await?;
-        } else {
-          updated_private_message
-            .send_undo_delete(&user, &self.client, pool)
-            .await?;
-        }
-      } else {
-        updated_private_message
-          .send_update(&user, &self.client, pool)
-          .await?;
-      }
+    // Send the apub update
+    if data.deleted {
+      updated_private_message.send_delete(&user, context).await?;
     } else {
       updated_private_message
-        .send_update(&user, &self.client, pool)
+        .send_undo_delete(&user, context)
         .await?;
     }
 
     let edit_id = data.edit_id;
-    let message = blocking(pool, move |conn| PrivateMessageView::read(conn, edit_id)).await??;
+    let message = blocking(context.pool(), move |conn| {
+      PrivateMessageView::read(conn, edit_id)
+    })
+    .await??;
+    let recipient_id = message.recipient_id;
 
     let res = PrivateMessageResponse { message };
 
-    if let Some(ws) = websocket_info {
-      ws.chatserver.do_send(SendUserRoomMessage {
-        op: UserOperation::EditPrivateMessage,
-        response: res.clone(),
-        recipient_id: orig_private_message.recipient_id,
-        my_id: ws.id,
-      });
-    }
+    context.chat_server().do_send(SendUserRoomMessage {
+      op: UserOperation::DeletePrivateMessage,
+      response: res.clone(),
+      recipient_id,
+      websocket_id,
+    });
 
     Ok(res)
   }
 }
 
 #[async_trait::async_trait(?Send)]
-impl Perform for Oper<GetPrivateMessages> {
-  type Response = PrivateMessagesResponse;
+impl Perform for MarkPrivateMessageAsRead {
+  type Response = PrivateMessageResponse;
 
   async fn perform(
     &self,
-    pool: &DbPool,
-    _websocket_info: Option<WebsocketInfo>,
-  ) -> Result<PrivateMessagesResponse, LemmyError> {
-    let data: &GetPrivateMessages = &self.data;
+    context: &Data<LemmyContext>,
+    websocket_id: Option<ConnectionId>,
+  ) -> Result<PrivateMessageResponse, LemmyError> {
+    let data: &MarkPrivateMessageAsRead = &self;
+    let user = get_user_from_jwt(&data.auth, context.pool()).await?;
 
-    let claims = match Claims::decode(&data.auth) {
-      Ok(claims) => claims.claims,
-      Err(_e) => return Err(APIError::err("not_logged_in").into()),
+    // Checking permissions
+    let edit_id = data.edit_id;
+    let orig_private_message = blocking(context.pool(), move |conn| {
+      PrivateMessage::read(conn, edit_id)
+    })
+    .await??;
+    if user.id != orig_private_message.recipient_id {
+      return Err(APIError::err("couldnt_update_private_message").into());
+    }
+
+    // Doing the update
+    let edit_id = data.edit_id;
+    let read = data.read;
+    match blocking(context.pool(), move |conn| {
+      PrivateMessage::update_read(conn, edit_id, read)
+    })
+    .await?
+    {
+      Ok(private_message) => private_message,
+      Err(_e) => return Err(APIError::err("couldnt_update_private_message").into()),
     };
 
-    let user_id = claims.id;
+    // No need to send an apub update
+
+    let edit_id = data.edit_id;
+    let message = blocking(context.pool(), move |conn| {
+      PrivateMessageView::read(conn, edit_id)
+    })
+    .await??;
+    let recipient_id = message.recipient_id;
+
+    let res = PrivateMessageResponse { message };
+
+    context.chat_server().do_send(SendUserRoomMessage {
+      op: UserOperation::MarkPrivateMessageAsRead,
+      response: res.clone(),
+      recipient_id,
+      websocket_id,
+    });
+
+    Ok(res)
+  }
+}
+
+#[async_trait::async_trait(?Send)]
+impl Perform for GetPrivateMessages {
+  type Response = PrivateMessagesResponse;
+
+  async fn perform(
+    &self,
+    context: &Data<LemmyContext>,
+    _websocket_id: Option<ConnectionId>,
+  ) -> Result<PrivateMessagesResponse, LemmyError> {
+    let data: &GetPrivateMessages = &self;
+    let user = get_user_from_jwt(&data.auth, context.pool()).await?;
+    let user_id = user.id;
 
     let page = data.page;
     let limit = data.limit;
     let unread_only = data.unread_only;
-    let messages = blocking(pool, move |conn| {
+    let messages = blocking(context.pool(), move |conn| {
       PrivateMessageQueryBuilder::create(&conn, user_id)
         .page(page)
         .limit(limit)
@@ -1430,29 +1497,24 @@ impl Perform for Oper<GetPrivateMessages> {
 }
 
 #[async_trait::async_trait(?Send)]
-impl Perform for Oper<UserJoin> {
+impl Perform for UserJoin {
   type Response = UserJoinResponse;
 
   async fn perform(
     &self,
-    _pool: &DbPool,
-    websocket_info: Option<WebsocketInfo>,
+    context: &Data<LemmyContext>,
+    websocket_id: Option<ConnectionId>,
   ) -> Result<UserJoinResponse, LemmyError> {
-    let data: &UserJoin = &self.data;
-
-    let claims = match Claims::decode(&data.auth) {
-      Ok(claims) => claims.claims,
-      Err(_e) => return Err(APIError::err("not_logged_in").into()),
-    };
+    let data: &UserJoin = &self;
+    let user = get_user_from_jwt(&data.auth, context.pool()).await?;
 
-    let user_id = claims.id;
-
-    if let Some(ws) = websocket_info {
-      if let Some(id) = ws.id {
-        ws.chatserver.do_send(JoinUserRoom { user_id, id });
-      }
+    if let Some(ws_id) = websocket_id {
+      context.chat_server().do_send(JoinUserRoom {
+        user_id: user.id,
+        id: ws_id,
+      });
     }
 
-    Ok(UserJoinResponse { user_id })
+    Ok(UserJoinResponse { user_id: user.id })
   }
 }
index 204a380d39b387e66abb7a334e4ff6d94d9215f7..4700bb0892ebc3598907fd1ffe357301b29d9b7a 100644 (file)
@@ -1,84 +1,63 @@
 use crate::{
   apub::{
+    check_is_apub_id_valid,
     community::do_announce,
     extensions::signatures::sign,
     insert_activity,
-    is_apub_id_valid,
     ActorType,
   },
   request::retry_custom,
-  DbPool,
+  LemmyContext,
   LemmyError,
 };
-use activitystreams::{context, object::properties::ObjectProperties, public, Activity, Base};
+use activitystreams::base::AnyBase;
 use actix_web::client::Client;
 use lemmy_db::{community::Community, user::User_};
+use lemmy_utils::{get_apub_protocol_string, settings::Settings};
 use log::debug;
-use serde::Serialize;
-use std::fmt::Debug;
-use url::Url;
+use url::{ParseError, Url};
+use uuid::Uuid;
 
-pub fn populate_object_props(
-  props: &mut ObjectProperties,
-  addressed_ccs: Vec<String>,
-  object_id: &str,
-) -> Result<(), LemmyError> {
-  props
-    .set_context_xsd_any_uri(context())?
-    // TODO: the activity needs a seperate id from the object
-    .set_id(object_id)?
-    // TODO: should to/cc go on the Create, or on the Post? or on both?
-    // TODO: handle privacy on the receiving side (at least ignore anything thats not public)
-    .set_to_xsd_any_uri(public())?
-    .set_many_cc_xsd_any_uris(addressed_ccs)?;
-  Ok(())
-}
-
-pub async fn send_activity_to_community<A>(
+pub async fn send_activity_to_community(
   creator: &User_,
   community: &Community,
-  to: Vec<String>,
-  activity: A,
-  client: &Client,
-  pool: &DbPool,
-) -> Result<(), LemmyError>
-where
-  A: Activity + Base + Serialize + Debug + Clone + Send + 'static,
-{
-  insert_activity(creator.id, activity.clone(), true, pool).await?;
+  to: Vec<Url>,
+  activity: AnyBase,
+  context: &LemmyContext,
+) -> Result<(), LemmyError> {
+  insert_activity(creator.id, activity.clone(), true, context.pool()).await?;
 
   // if this is a local community, we need to do an announce from the community instead
   if community.local {
-    do_announce(activity, &community, creator, client, pool).await?;
+    do_announce(activity, &community, creator, context).await?;
   } else {
-    send_activity(client, &activity, creator, to).await?;
+    send_activity(context.client(), &activity, creator, to).await?;
   }
 
   Ok(())
 }
 
 /// Send an activity to a list of recipients, using the correct headers etc.
-pub async fn send_activity<A>(
+pub async fn send_activity(
   client: &Client,
-  activity: &A,
+  activity: &AnyBase,
   actor: &dyn ActorType,
-  to: Vec<String>,
-) -> Result<(), LemmyError>
-where
-  A: Serialize,
-{
+  to: Vec<Url>,
+) -> Result<(), LemmyError> {
+  if !Settings::get().federation.enabled {
+    return Ok(());
+  }
+
   let activity = serde_json::to_string(&activity)?;
   debug!("Sending activitypub activity {} to {:?}", activity, to);
 
-  for t in to {
-    let to_url = Url::parse(&t)?;
-    if !is_apub_id_valid(&to_url) {
-      debug!("Not sending activity to {} (invalid or blocklisted)", t);
-      continue;
-    }
+  for to_url in to {
+    check_is_apub_id_valid(&to_url)?;
 
     let res = retry_custom(|| async {
-      let request = client.post(&t).header("Content-Type", "application/json");
+      let request = client
+        .post(to_url.as_str())
+        .header("Content-Type", "application/json");
 
       match sign(request, actor, activity.clone()).await {
         Ok(signed) => Ok(signed.send().await),
@@ -92,3 +71,17 @@ where
 
   Ok(())
 }
+
+pub(in crate::apub) fn generate_activity_id<T>(kind: T) -> Result<Url, ParseError>
+where
+  T: ToString,
+{
+  let id = format!(
+    "{}://{}/activities/{}/{}",
+    get_apub_protocol_string(),
+    Settings::get().hostname,
+    kind.to_string().to_lowercase(),
+    Uuid::new_v4()
+  );
+  Url::parse(&id)
+}
index 9e5e53a7b48225238873fecf223dff6884ecfa31..a9a97c0833a609aa24c6720fe7eac6dda570da94 100644 (file)
@@ -1,14 +1,15 @@
 use crate::{
   apub::{
-    activities::{populate_object_props, send_activity_to_community},
+    activities::{generate_activity_id, send_activity_to_community},
+    check_actor_domain,
     create_apub_response,
     create_apub_tombstone_response,
     create_tombstone,
     fetch_webfinger_url,
     fetcher::{
-      get_or_fetch_and_insert_remote_comment,
-      get_or_fetch_and_insert_remote_post,
-      get_or_fetch_and_upsert_remote_user,
+      get_or_fetch_and_insert_comment,
+      get_or_fetch_and_insert_post,
+      get_or_fetch_and_upsert_user,
     },
     ActorType,
     ApubLikeableType,
@@ -17,18 +18,29 @@ use crate::{
     ToApub,
   },
   blocking,
-  routes::DbPoolParam,
   DbPool,
+  LemmyContext,
   LemmyError,
 };
 use activitystreams::{
-  activity::{Create, Delete, Dislike, Like, Remove, Undo, Update},
-  context,
+  activity::{
+    kind::{CreateType, DeleteType, DislikeType, LikeType, RemoveType, UndoType, UpdateType},
+    Create,
+    Delete,
+    Dislike,
+    Like,
+    Remove,
+    Undo,
+    Update,
+  },
+  base::AnyBase,
   link::Mention,
-  object::{kind::NoteType, properties::ObjectProperties, Note},
+  object::{kind::NoteType, Note, Tombstone},
+  prelude::*,
+  public,
 };
-use activitystreams_new::object::Tombstone;
-use actix_web::{body::Body, client::Client, web::Path, HttpResponse};
+use actix_web::{body::Body, web, web::Path, HttpResponse};
+use anyhow::Context;
 use itertools::Itertools;
 use lemmy_db::{
   comment::{Comment, CommentForm},
@@ -37,9 +49,17 @@ use lemmy_db::{
   user::User_,
   Crud,
 };
-use lemmy_utils::{convert_datetime, scrape_text_for_mentions, MentionData};
+use lemmy_utils::{
+  convert_datetime,
+  location_info,
+  remove_slurs,
+  scrape_text_for_mentions,
+  MentionData,
+};
 use log::debug;
 use serde::Deserialize;
+use serde_json::Error;
+use url::Url;
 
 #[derive(Deserialize)]
 pub struct CommentQuery {
@@ -49,13 +69,15 @@ pub struct CommentQuery {
 /// Return the post json over HTTP.
 pub async fn get_apub_comment(
   info: Path<CommentQuery>,
-  db: DbPoolParam,
+  context: web::Data<LemmyContext>,
 ) -> Result<HttpResponse<Body>, LemmyError> {
   let id = info.comment_id.parse::<i32>()?;
-  let comment = blocking(&db, move |conn| Comment::read(conn, id)).await??;
+  let comment = blocking(context.pool(), move |conn| Comment::read(conn, id)).await??;
 
   if !comment.deleted {
-    Ok(create_apub_response(&comment.to_apub(&db).await?))
+    Ok(create_apub_response(
+      &comment.to_apub(context.pool()).await?,
+    ))
   } else {
     Ok(create_apub_tombstone_response(&comment.to_tombstone()?))
   }
@@ -66,8 +88,7 @@ impl ToApub for Comment {
   type Response = Note;
 
   async fn to_apub(&self, pool: &DbPool) -> Result<Note, LemmyError> {
-    let mut comment = Note::default();
-    let oprops: &mut ObjectProperties = comment.as_mut();
+    let mut comment = Note::new();
 
     let creator_id = self.creator_id;
     let creator = blocking(pool, move |conn| User_::read(conn, creator_id)).await??;
@@ -88,30 +109,25 @@ impl ToApub for Comment {
       in_reply_to_vec.push(parent_comment.ap_id);
     }
 
-    oprops
+    comment
       // Not needed when the Post is embedded in a collection (like for community outbox)
-      .set_context_xsd_any_uri(context())?
-      .set_id(self.ap_id.to_owned())?
-      .set_published(convert_datetime(self.published))?
-      .set_to_xsd_any_uri(community.actor_id)?
-      .set_many_in_reply_to_xsd_any_uris(in_reply_to_vec)?
-      .set_content_xsd_string(self.content.to_owned())?
-      .set_attributed_to_xsd_any_uri(creator.actor_id)?;
+      .set_context(activitystreams::context())
+      .set_id(Url::parse(&self.ap_id)?)
+      .set_published(convert_datetime(self.published))
+      .set_to(community.actor_id)
+      .set_many_in_reply_tos(in_reply_to_vec)
+      .set_content(self.content.to_owned())
+      .set_attributed_to(creator.actor_id);
 
     if let Some(u) = self.updated {
-      oprops.set_updated(convert_datetime(u))?;
+      comment.set_updated(convert_datetime(u));
     }
 
     Ok(comment)
   }
 
   fn to_tombstone(&self) -> Result<Tombstone, LemmyError> {
-    create_tombstone(
-      self.deleted,
-      &self.ap_id,
-      self.updated,
-      NoteType.to_string(),
-    )
+    create_tombstone(self.deleted, &self.ap_id, self.updated, NoteType::Note)
   }
 }
 
@@ -122,51 +138,61 @@ impl FromApub for CommentForm {
   /// Parse an ActivityPub note received from another instance into a Lemmy comment
   async fn from_apub(
     note: &Note,
-    client: &Client,
-    pool: &DbPool,
+    context: &LemmyContext,
+    expected_domain: Option<Url>,
   ) -> Result<CommentForm, LemmyError> {
-    let oprops = &note.object_props;
-    let creator_actor_id = &oprops.get_attributed_to_xsd_any_uri().unwrap().to_string();
-
-    let creator = get_or_fetch_and_upsert_remote_user(&creator_actor_id, client, pool).await?;
-
-    let mut in_reply_tos = oprops.get_many_in_reply_to_xsd_any_uris().unwrap();
-    let post_ap_id = in_reply_tos.next().unwrap().to_string();
+    let creator_actor_id = &note
+      .attributed_to()
+      .context(location_info!())?
+      .as_single_xsd_any_uri()
+      .context(location_info!())?;
+
+    let creator = get_or_fetch_and_upsert_user(creator_actor_id, context).await?;
+
+    let mut in_reply_tos = note
+      .in_reply_to()
+      .as_ref()
+      .context(location_info!())?
+      .as_many()
+      .context(location_info!())?
+      .iter()
+      .map(|i| i.as_xsd_any_uri().context(""));
+    let post_ap_id = in_reply_tos.next().context(location_info!())??;
 
     // This post, or the parent comment might not yet exist on this server yet, fetch them.
-    let post = get_or_fetch_and_insert_remote_post(&post_ap_id, client, pool).await?;
+    let post = get_or_fetch_and_insert_post(&post_ap_id, context).await?;
 
     // The 2nd item, if it exists, is the parent comment apub_id
     // For deeply nested comments, FromApub automatically gets called recursively
     let parent_id: Option<i32> = match in_reply_tos.next() {
       Some(parent_comment_uri) => {
-        let parent_comment_ap_id = &parent_comment_uri.to_string();
+        let parent_comment_ap_id = &parent_comment_uri?;
         let parent_comment =
-          get_or_fetch_and_insert_remote_comment(&parent_comment_ap_id, client, pool).await?;
+          get_or_fetch_and_insert_comment(&parent_comment_ap_id, context).await?;
 
         Some(parent_comment.id)
       }
       None => None,
     };
+    let content = note
+      .content()
+      .context(location_info!())?
+      .as_single_xsd_string()
+      .context(location_info!())?
+      .to_string();
+    let content_slurs_removed = remove_slurs(&content);
 
     Ok(CommentForm {
       creator_id: creator.id,
       post_id: post.id,
       parent_id,
-      content: oprops
-        .get_content_xsd_string()
-        .map(|c| c.to_string())
-        .unwrap(),
+      content: content_slurs_removed,
       removed: None,
       read: None,
-      published: oprops
-        .get_published()
-        .map(|u| u.as_ref().to_owned().naive_local()),
-      updated: oprops
-        .get_updated()
-        .map(|u| u.as_ref().to_owned().naive_local()),
+      published: note.published().map(|u| u.to_owned().naive_local()),
+      updated: note.updated().map(|u| u.to_owned().naive_local()),
       deleted: None,
-      ap_id: oprops.get_id().unwrap().to_string(),
+      ap_id: check_actor_domain(note, expected_domain)?,
       local: false,
     })
   }
@@ -175,108 +201,100 @@ impl FromApub for CommentForm {
 #[async_trait::async_trait(?Send)]
 impl ApubObjectType for Comment {
   /// Send out information about a newly created comment, to the followers of the community.
-  async fn send_create(
-    &self,
-    creator: &User_,
-    client: &Client,
-    pool: &DbPool,
-  ) -> Result<(), LemmyError> {
-    let note = self.to_apub(pool).await?;
+  async fn send_create(&self, creator: &User_, context: &LemmyContext) -> Result<(), LemmyError> {
+    let note = self.to_apub(context.pool()).await?;
 
     let post_id = self.post_id;
-    let post = blocking(pool, move |conn| Post::read(conn, post_id)).await??;
+    let post = blocking(context.pool(), move |conn| Post::read(conn, post_id)).await??;
 
     let community_id = post.community_id;
-    let community = blocking(pool, move |conn| Community::read(conn, community_id)).await??;
-
-    let maa =
-      collect_non_local_mentions_and_addresses(&self.content, &community, client, pool).await?;
-
-    let id = format!("{}/create/{}", self.ap_id, uuid::Uuid::new_v4());
-    let mut create = Create::new();
-    populate_object_props(&mut create.object_props, maa.addressed_ccs, &id)?;
+    let community = blocking(context.pool(), move |conn| {
+      Community::read(conn, community_id)
+    })
+    .await??;
 
-    // Set the mention tags
-    create.object_props.set_many_tag_base_boxes(maa.tags)?;
+    let maa = collect_non_local_mentions_and_addresses(&self.content, &community, context).await?;
 
+    let mut create = Create::new(creator.actor_id.to_owned(), note.into_any_base()?);
     create
-      .create_props
-      .set_actor_xsd_any_uri(creator.actor_id.to_owned())?
-      .set_object_base_box(note)?;
+      .set_context(activitystreams::context())
+      .set_id(generate_activity_id(CreateType::Create)?)
+      .set_to(public())
+      .set_many_ccs(maa.addressed_ccs.to_owned())
+      // Set the mention tags
+      .set_many_tags(maa.get_tags()?);
 
-    send_activity_to_community(&creator, &community, maa.inboxes, create, client, pool).await?;
+    send_activity_to_community(
+      &creator,
+      &community,
+      maa.inboxes,
+      create.into_any_base()?,
+      context,
+    )
+    .await?;
     Ok(())
   }
 
   /// Send out information about an edited post, to the followers of the community.
-  async fn send_update(
-    &self,
-    creator: &User_,
-    client: &Client,
-    pool: &DbPool,
-  ) -> Result<(), LemmyError> {
-    let note = self.to_apub(pool).await?;
+  async fn send_update(&self, creator: &User_, context: &LemmyContext) -> Result<(), LemmyError> {
+    let note = self.to_apub(context.pool()).await?;
 
     let post_id = self.post_id;
-    let post = blocking(pool, move |conn| Post::read(conn, post_id)).await??;
+    let post = blocking(context.pool(), move |conn| Post::read(conn, post_id)).await??;
 
     let community_id = post.community_id;
-    let community = blocking(pool, move |conn| Community::read(conn, community_id)).await??;
-
-    let maa =
-      collect_non_local_mentions_and_addresses(&self.content, &community, client, pool).await?;
-
-    let id = format!("{}/update/{}", self.ap_id, uuid::Uuid::new_v4());
-    let mut update = Update::new();
-    populate_object_props(&mut update.object_props, maa.addressed_ccs, &id)?;
+    let community = blocking(context.pool(), move |conn| {
+      Community::read(conn, community_id)
+    })
+    .await??;
 
-    // Set the mention tags
-    update.object_props.set_many_tag_base_boxes(maa.tags)?;
+    let maa = collect_non_local_mentions_and_addresses(&self.content, &community, context).await?;
 
+    let mut update = Update::new(creator.actor_id.to_owned(), note.into_any_base()?);
     update
-      .update_props
-      .set_actor_xsd_any_uri(creator.actor_id.to_owned())?
-      .set_object_base_box(note)?;
+      .set_context(activitystreams::context())
+      .set_id(generate_activity_id(UpdateType::Update)?)
+      .set_to(public())
+      .set_many_ccs(maa.addressed_ccs.to_owned())
+      // Set the mention tags
+      .set_many_tags(maa.get_tags()?);
 
-    send_activity_to_community(&creator, &community, maa.inboxes, update, client, pool).await?;
+    send_activity_to_community(
+      &creator,
+      &community,
+      maa.inboxes,
+      update.into_any_base()?,
+      context,
+    )
+    .await?;
     Ok(())
   }
 
-  async fn send_delete(
-    &self,
-    creator: &User_,
-    client: &Client,
-    pool: &DbPool,
-  ) -> Result<(), LemmyError> {
-    let note = self.to_apub(pool).await?;
+  async fn send_delete(&self, creator: &User_, context: &LemmyContext) -> Result<(), LemmyError> {
+    let note = self.to_apub(context.pool()).await?;
 
     let post_id = self.post_id;
-    let post = blocking(pool, move |conn| Post::read(conn, post_id)).await??;
+    let post = blocking(context.pool(), move |conn| Post::read(conn, post_id)).await??;
 
     let community_id = post.community_id;
-    let community = blocking(pool, move |conn| Community::read(conn, community_id)).await??;
-
-    let id = format!("{}/delete/{}", self.ap_id, uuid::Uuid::new_v4());
-    let mut delete = Delete::default();
-
-    populate_object_props(
-      &mut delete.object_props,
-      vec![community.get_followers_url()],
-      &id,
-    )?;
+    let community = blocking(context.pool(), move |conn| {
+      Community::read(conn, community_id)
+    })
+    .await??;
 
+    let mut delete = Delete::new(creator.actor_id.to_owned(), note.into_any_base()?);
     delete
-      .delete_props
-      .set_actor_xsd_any_uri(creator.actor_id.to_owned())?
-      .set_object_base_box(note)?;
+      .set_context(activitystreams::context())
+      .set_id(generate_activity_id(DeleteType::Delete)?)
+      .set_to(public())
+      .set_many_ccs(vec![community.get_followers_url()?]);
 
     send_activity_to_community(
       &creator,
       &community,
-      vec![community.get_shared_inbox_url()],
-      delete,
-      client,
-      pool,
+      vec![community.get_shared_inbox_url()?],
+      delete.into_any_base()?,
+      context,
     )
     .await?;
     Ok(())
@@ -285,151 +303,110 @@ impl ApubObjectType for Comment {
   async fn send_undo_delete(
     &self,
     creator: &User_,
-    client: &Client,
-    pool: &DbPool,
+    context: &LemmyContext,
   ) -> Result<(), LemmyError> {
-    let note = self.to_apub(pool).await?;
+    let note = self.to_apub(context.pool()).await?;
 
     let post_id = self.post_id;
-    let post = blocking(pool, move |conn| Post::read(conn, post_id)).await??;
+    let post = blocking(context.pool(), move |conn| Post::read(conn, post_id)).await??;
 
     let community_id = post.community_id;
-    let community = blocking(pool, move |conn| Community::read(conn, community_id)).await??;
+    let community = blocking(context.pool(), move |conn| {
+      Community::read(conn, community_id)
+    })
+    .await??;
 
     // Generate a fake delete activity, with the correct object
-    let id = format!("{}/delete/{}", self.ap_id, uuid::Uuid::new_v4());
-    let mut delete = Delete::default();
-
-    populate_object_props(
-      &mut delete.object_props,
-      vec![community.get_followers_url()],
-      &id,
-    )?;
-
+    let mut delete = Delete::new(creator.actor_id.to_owned(), note.into_any_base()?);
     delete
-      .delete_props
-      .set_actor_xsd_any_uri(creator.actor_id.to_owned())?
-      .set_object_base_box(note)?;
+      .set_context(activitystreams::context())
+      .set_id(generate_activity_id(DeleteType::Delete)?)
+      .set_to(public())
+      .set_many_ccs(vec![community.get_followers_url()?]);
 
-    // TODO
     // Undo that fake activity
-    let undo_id = format!("{}/undo/delete/{}", self.ap_id, uuid::Uuid::new_v4());
-    let mut undo = Undo::default();
-
-    populate_object_props(
-      &mut undo.object_props,
-      vec![community.get_followers_url()],
-      &undo_id,
-    )?;
-
+    let mut undo = Undo::new(creator.actor_id.to_owned(), delete.into_any_base()?);
     undo
-      .undo_props
-      .set_actor_xsd_any_uri(creator.actor_id.to_owned())?
-      .set_object_base_box(delete)?;
+      .set_context(activitystreams::context())
+      .set_id(generate_activity_id(UndoType::Undo)?)
+      .set_to(public())
+      .set_many_ccs(vec![community.get_followers_url()?]);
 
     send_activity_to_community(
       &creator,
       &community,
-      vec![community.get_shared_inbox_url()],
-      undo,
-      client,
-      pool,
+      vec![community.get_shared_inbox_url()?],
+      undo.into_any_base()?,
+      context,
     )
     .await?;
     Ok(())
   }
 
-  async fn send_remove(
-    &self,
-    mod_: &User_,
-    client: &Client,
-    pool: &DbPool,
-  ) -> Result<(), LemmyError> {
-    let note = self.to_apub(pool).await?;
+  async fn send_remove(&self, mod_: &User_, context: &LemmyContext) -> Result<(), LemmyError> {
+    let note = self.to_apub(context.pool()).await?;
 
     let post_id = self.post_id;
-    let post = blocking(pool, move |conn| Post::read(conn, post_id)).await??;
+    let post = blocking(context.pool(), move |conn| Post::read(conn, post_id)).await??;
 
     let community_id = post.community_id;
-    let community = blocking(pool, move |conn| Community::read(conn, community_id)).await??;
-
-    let id = format!("{}/remove/{}", self.ap_id, uuid::Uuid::new_v4());
-    let mut remove = Remove::default();
-
-    populate_object_props(
-      &mut remove.object_props,
-      vec![community.get_followers_url()],
-      &id,
-    )?;
+    let community = blocking(context.pool(), move |conn| {
+      Community::read(conn, community_id)
+    })
+    .await??;
 
+    let mut remove = Remove::new(mod_.actor_id.to_owned(), note.into_any_base()?);
     remove
-      .remove_props
-      .set_actor_xsd_any_uri(mod_.actor_id.to_owned())?
-      .set_object_base_box(note)?;
+      .set_context(activitystreams::context())
+      .set_id(generate_activity_id(RemoveType::Remove)?)
+      .set_to(public())
+      .set_many_ccs(vec![community.get_followers_url()?]);
 
     send_activity_to_community(
       &mod_,
       &community,
-      vec![community.get_shared_inbox_url()],
-      remove,
-      client,
-      pool,
+      vec![community.get_shared_inbox_url()?],
+      remove.into_any_base()?,
+      context,
     )
     .await?;
     Ok(())
   }
 
-  async fn send_undo_remove(
-    &self,
-    mod_: &User_,
-    client: &Client,
-    pool: &DbPool,
-  ) -> Result<(), LemmyError> {
-    let note = self.to_apub(pool).await?;
+  async fn send_undo_remove(&self, mod_: &User_, context: &LemmyContext) -> Result<(), LemmyError> {
+    let note = self.to_apub(context.pool()).await?;
 
     let post_id = self.post_id;
-    let post = blocking(pool, move |conn| Post::read(conn, post_id)).await??;
+    let post = blocking(context.pool(), move |conn| Post::read(conn, post_id)).await??;
 
     let community_id = post.community_id;
-    let community = blocking(pool, move |conn| Community::read(conn, community_id)).await??;
+    let community = blocking(context.pool(), move |conn| {
+      Community::read(conn, community_id)
+    })
+    .await??;
 
     // Generate a fake delete activity, with the correct object
-    let id = format!("{}/remove/{}", self.ap_id, uuid::Uuid::new_v4());
-    let mut remove = Remove::default();
-
-    populate_object_props(
-      &mut remove.object_props,
-      vec![community.get_followers_url()],
-      &id,
-    )?;
-
+    let mut remove = Remove::new(mod_.actor_id.to_owned(), note.into_any_base()?);
     remove
-      .remove_props
-      .set_actor_xsd_any_uri(mod_.actor_id.to_owned())?
-      .set_object_base_box(note)?;
+      .set_context(activitystreams::context())
+      .set_id(generate_activity_id(RemoveType::Remove)?)
+      .set_to(public())
+      .set_many_ccs(vec![community.get_followers_url()?]);
 
     // Undo that fake activity
-    let undo_id = format!("{}/undo/remove/{}", self.ap_id, uuid::Uuid::new_v4());
-    let mut undo = Undo::default();
-
-    populate_object_props(
-      &mut undo.object_props,
-      vec![community.get_followers_url()],
-      &undo_id,
-    )?;
-
+    let mut undo = Undo::new(mod_.actor_id.to_owned(), remove.into_any_base()?);
     undo
-      .undo_props
-      .set_actor_xsd_any_uri(mod_.actor_id.to_owned())?
-      .set_object_base_box(remove)?;
+      .set_context(activitystreams::context())
+      .set_id(generate_activity_id(UndoType::Undo)?)
+      .set_to(public())
+      .set_many_ccs(vec![community.get_followers_url()?]);
 
     send_activity_to_community(
       &mod_,
       &community,
-      vec![community.get_shared_inbox_url()],
-      undo,
-      client,
-      pool,
+      vec![community.get_shared_inbox_url()?],
+      undo.into_any_base()?,
+      context,
     )
     .await?;
     Ok(())
@@ -438,79 +415,61 @@ impl ApubObjectType for Comment {
 
 #[async_trait::async_trait(?Send)]
 impl ApubLikeableType for Comment {
-  async fn send_like(
-    &self,
-    creator: &User_,
-    client: &Client,
-    pool: &DbPool,
-  ) -> Result<(), LemmyError> {
-    let note = self.to_apub(pool).await?;
+  async fn send_like(&self, creator: &User_, context: &LemmyContext) -> Result<(), LemmyError> {
+    let note = self.to_apub(context.pool()).await?;
 
     let post_id = self.post_id;
-    let post = blocking(pool, move |conn| Post::read(conn, post_id)).await??;
+    let post = blocking(context.pool(), move |conn| Post::read(conn, post_id)).await??;
 
     let community_id = post.community_id;
-    let community = blocking(pool, move |conn| Community::read(conn, community_id)).await??;
-
-    let id = format!("{}/like/{}", self.ap_id, uuid::Uuid::new_v4());
+    let community = blocking(context.pool(), move |conn| {
+      Community::read(conn, community_id)
+    })
+    .await??;
 
-    let mut like = Like::new();
-    populate_object_props(
-      &mut like.object_props,
-      vec![community.get_followers_url()],
-      &id,
-    )?;
+    let mut like = Like::new(creator.actor_id.to_owned(), note.into_any_base()?);
     like
-      .like_props
-      .set_actor_xsd_any_uri(creator.actor_id.to_owned())?
-      .set_object_base_box(note)?;
+      .set_context(activitystreams::context())
+      .set_id(generate_activity_id(LikeType::Like)?)
+      .set_to(public())
+      .set_many_ccs(vec![community.get_followers_url()?]);
 
     send_activity_to_community(
       &creator,
       &community,
-      vec![community.get_shared_inbox_url()],
-      like,
-      client,
-      pool,
+      vec![community.get_shared_inbox_url()?],
+      like.into_any_base()?,
+      context,
     )
     .await?;
     Ok(())
   }
 
-  async fn send_dislike(
-    &self,
-    creator: &User_,
-    client: &Client,
-    pool: &DbPool,
-  ) -> Result<(), LemmyError> {
-    let note = self.to_apub(pool).await?;
+  async fn send_dislike(&self, creator: &User_, context: &LemmyContext) -> Result<(), LemmyError> {
+    let note = self.to_apub(context.pool()).await?;
 
     let post_id = self.post_id;
-    let post = blocking(pool, move |conn| Post::read(conn, post_id)).await??;
+    let post = blocking(context.pool(), move |conn| Post::read(conn, post_id)).await??;
 
     let community_id = post.community_id;
-    let community = blocking(pool, move |conn| Community::read(conn, community_id)).await??;
-
-    let id = format!("{}/dislike/{}", self.ap_id, uuid::Uuid::new_v4());
+    let community = blocking(context.pool(), move |conn| {
+      Community::read(conn, community_id)
+    })
+    .await??;
 
-    let mut dislike = Dislike::new();
-    populate_object_props(
-      &mut dislike.object_props,
-      vec![community.get_followers_url()],
-      &id,
-    )?;
+    let mut dislike = Dislike::new(creator.actor_id.to_owned(), note.into_any_base()?);
     dislike
-      .dislike_props
-      .set_actor_xsd_any_uri(creator.actor_id.to_owned())?
-      .set_object_base_box(note)?;
+      .set_context(activitystreams::context())
+      .set_id(generate_activity_id(DislikeType::Dislike)?)
+      .set_to(public())
+      .set_many_ccs(vec![community.get_followers_url()?]);
 
     send_activity_to_community(
       &creator,
       &community,
-      vec![community.get_shared_inbox_url()],
-      dislike,
-      client,
-      pool,
+      vec![community.get_shared_inbox_url()?],
+      dislike.into_any_base()?,
+      context,
     )
     .await?;
     Ok(())
@@ -519,53 +478,40 @@ impl ApubLikeableType for Comment {
   async fn send_undo_like(
     &self,
     creator: &User_,
-    client: &Client,
-    pool: &DbPool,
+    context: &LemmyContext,
   ) -> Result<(), LemmyError> {
-    let note = self.to_apub(pool).await?;
+    let note = self.to_apub(context.pool()).await?;
 
     let post_id = self.post_id;
-    let post = blocking(pool, move |conn| Post::read(conn, post_id)).await??;
+    let post = blocking(context.pool(), move |conn| Post::read(conn, post_id)).await??;
 
     let community_id = post.community_id;
-    let community = blocking(pool, move |conn| Community::read(conn, community_id)).await??;
-
-    let id = format!("{}/dislike/{}", self.ap_id, uuid::Uuid::new_v4());
+    let community = blocking(context.pool(), move |conn| {
+      Community::read(conn, community_id)
+    })
+    .await??;
 
-    let mut like = Like::new();
-    populate_object_props(
-      &mut like.object_props,
-      vec![community.get_followers_url()],
-      &id,
-    )?;
+    let mut like = Like::new(creator.actor_id.to_owned(), note.into_any_base()?);
     like
-      .like_props
-      .set_actor_xsd_any_uri(creator.actor_id.to_owned())?
-      .set_object_base_box(note)?;
+      .set_context(activitystreams::context())
+      .set_id(generate_activity_id(DislikeType::Dislike)?)
+      .set_to(public())
+      .set_many_ccs(vec![community.get_followers_url()?]);
 
-    // TODO
     // Undo that fake activity
-    let undo_id = format!("{}/undo/like/{}", self.ap_id, uuid::Uuid::new_v4());
-    let mut undo = Undo::default();
-
-    populate_object_props(
-      &mut undo.object_props,
-      vec![community.get_followers_url()],
-      &undo_id,
-    )?;
-
+    let mut undo = Undo::new(creator.actor_id.to_owned(), like.into_any_base()?);
     undo
-      .undo_props
-      .set_actor_xsd_any_uri(creator.actor_id.to_owned())?
-      .set_object_base_box(like)?;
+      .set_context(activitystreams::context())
+      .set_id(generate_activity_id(UndoType::Undo)?)
+      .set_to(public())
+      .set_many_ccs(vec![community.get_followers_url()?]);
 
     send_activity_to_community(
       &creator,
       &community,
-      vec![community.get_shared_inbox_url()],
-      undo,
-      client,
-      pool,
+      vec![community.get_shared_inbox_url()?],
+      undo.into_any_base()?,
+      context,
     )
     .await?;
     Ok(())
@@ -573,21 +519,30 @@ impl ApubLikeableType for Comment {
 }
 
 struct MentionsAndAddresses {
-  addressed_ccs: Vec<String>,
-  inboxes: Vec<String>,
+  addressed_ccs: Vec<Url>,
+  inboxes: Vec<Url>,
   tags: Vec<Mention>,
 }
 
+impl MentionsAndAddresses {
+  fn get_tags(&self) -> Result<Vec<AnyBase>, Error> {
+    self
+      .tags
+      .iter()
+      .map(|t| t.to_owned().into_any_base())
+      .collect::<Result<Vec<AnyBase>, Error>>()
+  }
+}
+
 /// This takes a comment, and builds a list of to_addresses, inboxes,
 /// and mention tags, so they know where to be sent to.
 /// Addresses are the users / addresses that go in the cc field.
 async fn collect_non_local_mentions_and_addresses(
   content: &str,
   community: &Community,
-  client: &Client,
-  pool: &DbPool,
+  context: &LemmyContext,
 ) -> Result<MentionsAndAddresses, LemmyError> {
-  let mut addressed_ccs = vec![community.get_followers_url()];
+  let mut addressed_ccs = vec![community.get_followers_url()?];
 
   // Add the mention tag
   let mut tags = Vec::new();
@@ -599,27 +554,24 @@ async fn collect_non_local_mentions_and_addresses(
     .filter(|m| !m.is_local())
     .collect::<Vec<MentionData>>();
 
-  let mut mention_inboxes = Vec::new();
+  let mut mention_inboxes: Vec<Url> = Vec::new();
   for mention in &mentions {
     // TODO should it be fetching it every time?
-    if let Ok(actor_id) = fetch_webfinger_url(mention, client).await {
+    if let Ok(actor_id) = fetch_webfinger_url(mention, context.client()).await {
       debug!("mention actor_id: {}", actor_id);
-      addressed_ccs.push(actor_id.to_owned());
+      addressed_ccs.push(actor_id.to_owned().to_string().parse()?);
 
-      let mention_user = get_or_fetch_and_upsert_remote_user(&actor_id, client, pool).await?;
-      let shared_inbox = mention_user.get_shared_inbox_url();
+      let mention_user = get_or_fetch_and_upsert_user(&actor_id, context).await?;
+      let shared_inbox = mention_user.get_shared_inbox_url()?;
 
       mention_inboxes.push(shared_inbox);
       let mut mention_tag = Mention::new();
-      mention_tag
-        .link_props
-        .set_href(actor_id)?
-        .set_name_xsd_string(mention.full_name())?;
+      mention_tag.set_href(actor_id).set_name(mention.full_name());
       tags.push(mention_tag);
     }
   }
 
-  let mut inboxes = vec![community.get_shared_inbox_url()];
+  let mut inboxes = vec![community.get_shared_inbox_url()?];
   inboxes.extend(mention_inboxes);
   inboxes = inboxes.into_iter().unique().collect();
 
index 529039fc0261586ea6e06116c2193596fa30d270..016f342dc629eb1d36d4fb361d39ac4b77b0659a 100644 (file)
@@ -1,12 +1,13 @@
 use crate::{
+  api::{check_slurs, check_slurs_opt},
   apub::{
-    activities::{populate_object_props, send_activity},
+    activities::{generate_activity_id, send_activity},
+    check_actor_domain,
     create_apub_response,
     create_apub_tombstone_response,
     create_tombstone,
     extensions::group_extensions::GroupExtension,
-    fetcher::get_or_fetch_and_upsert_remote_user,
-    get_shared_inbox,
+    fetcher::{get_or_fetch_and_upsert_actor, get_or_fetch_and_upsert_user},
     insert_activity,
     ActorType,
     FromApub,
@@ -14,38 +15,41 @@ use crate::{
     ToApub,
   },
   blocking,
-  routes::DbPoolParam,
   DbPool,
+  LemmyContext,
   LemmyError,
 };
 use activitystreams::{
-  activity::{Accept, Announce, Delete, Remove, Undo},
-  Activity,
-  Base,
-  BaseBox,
-};
-use activitystreams_ext::Ext2;
-use activitystreams_new::{
-  activity::Follow,
+  activity::{
+    kind::{AcceptType, AnnounceType, DeleteType, LikeType, RemoveType, UndoType},
+    Accept,
+    Announce,
+    Delete,
+    Follow,
+    Remove,
+    Undo,
+  },
   actor::{kind::GroupType, ApActor, Endpoints, Group},
-  base::BaseExt,
-  collection::UnorderedCollection,
-  context,
-  object::Tombstone,
+  base::{AnyBase, BaseExt},
+  collection::{OrderedCollection, UnorderedCollection},
+  object::{Image, Tombstone},
   prelude::*,
-  primitives::{XsdAnyUri, XsdDateTime},
+  public,
 };
-use actix_web::{body::Body, client::Client, web, HttpResponse};
+use activitystreams_ext::Ext2;
+use actix_web::{body::Body, web, HttpResponse};
+use anyhow::Context;
 use itertools::Itertools;
 use lemmy_db::{
   community::{Community, CommunityForm},
   community_view::{CommunityFollowerView, CommunityModeratorView},
   naive_now,
+  post::Post,
   user::User_,
 };
-use lemmy_utils::convert_datetime;
-use serde::{Deserialize, Serialize};
-use std::{fmt::Debug, str::FromStr};
+use lemmy_utils::{convert_datetime, get_apub_protocol_string, location_info};
+use serde::Deserialize;
+use url::Url;
 
 #[derive(Deserialize)]
 pub struct CommunityQuery {
@@ -71,14 +75,14 @@ impl ToApub for Community {
 
     let mut group = Group::new();
     group
-      .set_context(context())
-      .set_id(XsdAnyUri::from_str(&self.actor_id)?)
+      .set_context(activitystreams::context())
+      .set_id(Url::parse(&self.actor_id)?)
       .set_name(self.name.to_owned())
-      .set_published(XsdDateTime::from(convert_datetime(self.published)))
+      .set_published(convert_datetime(self.published))
       .set_many_attributed_tos(moderators);
 
     if let Some(u) = self.updated.to_owned() {
-      group.set_updated(XsdDateTime::from(convert_datetime(u)));
+      group.set_updated(convert_datetime(u));
     }
     if let Some(d) = self.description.to_owned() {
       // TODO: this should be html, also add source field with raw markdown
@@ -86,15 +90,15 @@ impl ToApub for Community {
       group.set_content(d);
     }
 
-    let mut ap_actor = ApActor::new(self.get_inbox_url().parse()?, group);
+    let mut ap_actor = ApActor::new(self.get_inbox_url()?, group);
     ap_actor
       .set_preferred_username(self.title.to_owned())
-      .set_outbox(self.get_outbox_url().parse()?)
-      .set_followers(self.get_followers_url().parse()?)
+      .set_outbox(self.get_outbox_url()?)
+      .set_followers(self.get_followers_url()?)
       .set_following(self.get_following_url().parse()?)
       .set_liked(self.get_liked_url().parse()?)
       .set_endpoints(Endpoints {
-        shared_inbox: Some(self.get_shared_inbox_url().parse()?),
+        shared_inbox: Some(self.get_shared_inbox_url()?),
         ..Default::default()
       });
 
@@ -108,225 +112,161 @@ impl ToApub for Community {
     Ok(Ext2::new(
       ap_actor,
       group_extension,
-      self.get_public_key_ext(),
+      self.get_public_key_ext()?,
     ))
   }
 
   fn to_tombstone(&self) -> Result<Tombstone, LemmyError> {
-    create_tombstone(
-      self.deleted,
-      &self.actor_id,
-      self.updated,
-      GroupType.to_string(),
-    )
+    create_tombstone(self.deleted, &self.actor_id, self.updated, GroupType::Group)
   }
 }
 
 #[async_trait::async_trait(?Send)]
 impl ActorType for Community {
-  fn actor_id(&self) -> String {
+  fn actor_id_str(&self) -> String {
     self.actor_id.to_owned()
   }
 
-  fn public_key(&self) -> String {
-    self.public_key.to_owned().unwrap()
+  fn public_key(&self) -> Option<String> {
+    self.public_key.to_owned()
   }
-  fn private_key(&self) -> String {
-    self.private_key.to_owned().unwrap()
+  fn private_key(&self) -> Option<String> {
+    self.private_key.to_owned()
   }
 
   /// As a local community, accept the follow request from a remote user.
   async fn send_accept_follow(
     &self,
-    follow: &Follow,
-    client: &Client,
-    pool: &DbPool,
+    follow: Follow,
+    context: &LemmyContext,
   ) -> Result<(), LemmyError> {
-    let actor_uri = follow.actor.as_single_xsd_any_uri().unwrap().to_string();
-    let id = format!("{}/accept/{}", self.actor_id, uuid::Uuid::new_v4());
-
-    let mut accept = Accept::new();
-    accept
-      .object_props
-      .set_context_xsd_any_uri(context())?
-      .set_id(id)?;
+    let actor_uri = follow
+      .actor()?
+      .as_single_xsd_any_uri()
+      .context(location_info!())?;
+    let actor = get_or_fetch_and_upsert_actor(actor_uri, context).await?;
+
+    let mut accept = Accept::new(self.actor_id.to_owned(), follow.into_any_base()?);
+    let to = actor.get_inbox_url()?;
     accept
-      .accept_props
-      .set_actor_xsd_any_uri(self.actor_id.to_owned())?
-      .set_object_base_box(BaseBox::from_concrete(follow.clone())?)?;
-    let to = format!("{}/inbox", actor_uri);
+      .set_context(activitystreams::context())
+      .set_id(generate_activity_id(AcceptType::Accept)?)
+      .set_to(to.clone());
 
-    insert_activity(self.creator_id, accept.clone(), true, pool).await?;
+    insert_activity(self.creator_id, accept.clone(), true, context.pool()).await?;
 
-    send_activity(client, &accept, self, vec![to]).await?;
+    send_activity(context.client(), &accept.into_any_base()?, self, vec![to]).await?;
     Ok(())
   }
 
-  async fn send_delete(
-    &self,
-    creator: &User_,
-    client: &Client,
-    pool: &DbPool,
-  ) -> Result<(), LemmyError> {
-    let group = self.to_apub(pool).await?;
-
-    let id = format!("{}/delete/{}", self.actor_id, uuid::Uuid::new_v4());
-
-    let mut delete = Delete::default();
-    populate_object_props(
-      &mut delete.object_props,
-      vec![self.get_followers_url()],
-      &id,
-    )?;
+  async fn send_delete(&self, creator: &User_, context: &LemmyContext) -> Result<(), LemmyError> {
+    let group = self.to_apub(context.pool()).await?;
 
+    let mut delete = Delete::new(creator.actor_id.to_owned(), group.into_any_base()?);
     delete
-      .delete_props
-      .set_actor_xsd_any_uri(creator.actor_id.to_owned())?
-      .set_object_base_box(BaseBox::from_concrete(group)?)?;
+      .set_context(activitystreams::context())
+      .set_id(generate_activity_id(DeleteType::Delete)?)
+      .set_to(public())
+      .set_many_ccs(vec![self.get_followers_url()?]);
 
-    insert_activity(self.creator_id, delete.clone(), true, pool).await?;
+    insert_activity(self.creator_id, delete.clone(), true, context.pool()).await?;
 
-    let inboxes = self.get_follower_inboxes(pool).await?;
+    let inboxes = self.get_follower_inboxes(context.pool()).await?;
 
     // Note: For an accept, since it was automatic, no one pushed a button,
     // the community was the actor.
     // But for delete, the creator is the actor, and does the signing
-    send_activity(client, &delete, creator, inboxes).await?;
+    send_activity(context.client(), &delete.into_any_base()?, creator, inboxes).await?;
     Ok(())
   }
 
   async fn send_undo_delete(
     &self,
     creator: &User_,
-    client: &Client,
-    pool: &DbPool,
+    context: &LemmyContext,
   ) -> Result<(), LemmyError> {
-    let group = self.to_apub(pool).await?;
-
-    let id = format!("{}/delete/{}", self.actor_id, uuid::Uuid::new_v4());
-
-    let mut delete = Delete::default();
-    populate_object_props(
-      &mut delete.object_props,
-      vec![self.get_followers_url()],
-      &id,
-    )?;
+    let group = self.to_apub(context.pool()).await?;
 
+    let mut delete = Delete::new(creator.actor_id.to_owned(), group.into_any_base()?);
     delete
-      .delete_props
-      .set_actor_xsd_any_uri(creator.actor_id.to_owned())?
-      .set_object_base_box(BaseBox::from_concrete(group)?)?;
-
-    // TODO
-    // Undo that fake activity
-    let undo_id = format!("{}/undo/delete/{}", self.actor_id, uuid::Uuid::new_v4());
-    let mut undo = Undo::default();
-
-    populate_object_props(
-      &mut undo.object_props,
-      vec![self.get_followers_url()],
-      &undo_id,
-    )?;
+      .set_context(activitystreams::context())
+      .set_id(generate_activity_id(DeleteType::Delete)?)
+      .set_to(public())
+      .set_many_ccs(vec![self.get_followers_url()?]);
 
+    let mut undo = Undo::new(creator.actor_id.to_owned(), delete.into_any_base()?);
     undo
-      .undo_props
-      .set_actor_xsd_any_uri(creator.actor_id.to_owned())?
-      .set_object_base_box(delete)?;
+      .set_context(activitystreams::context())
+      .set_id(generate_activity_id(UndoType::Undo)?)
+      .set_to(public())
+      .set_many_ccs(vec![self.get_followers_url()?]);
 
-    insert_activity(self.creator_id, undo.clone(), true, pool).await?;
+    insert_activity(self.creator_id, undo.clone(), true, context.pool()).await?;
 
-    let inboxes = self.get_follower_inboxes(pool).await?;
+    let inboxes = self.get_follower_inboxes(context.pool()).await?;
 
     // Note: For an accept, since it was automatic, no one pushed a button,
     // the community was the actor.
     // But for delete, the creator is the actor, and does the signing
-    send_activity(client, &undo, creator, inboxes).await?;
+    send_activity(context.client(), &undo.into_any_base()?, creator, inboxes).await?;
     Ok(())
   }
 
-  async fn send_remove(
-    &self,
-    mod_: &User_,
-    client: &Client,
-    pool: &DbPool,
-  ) -> Result<(), LemmyError> {
-    let group = self.to_apub(pool).await?;
-
-    let id = format!("{}/remove/{}", self.actor_id, uuid::Uuid::new_v4());
-
-    let mut remove = Remove::default();
-    populate_object_props(
-      &mut remove.object_props,
-      vec![self.get_followers_url()],
-      &id,
-    )?;
+  async fn send_remove(&self, mod_: &User_, context: &LemmyContext) -> Result<(), LemmyError> {
+    let group = self.to_apub(context.pool()).await?;
 
+    let mut remove = Remove::new(mod_.actor_id.to_owned(), group.into_any_base()?);
     remove
-      .remove_props
-      .set_actor_xsd_any_uri(mod_.actor_id.to_owned())?
-      .set_object_base_box(BaseBox::from_concrete(group)?)?;
+      .set_context(activitystreams::context())
+      .set_id(generate_activity_id(RemoveType::Remove)?)
+      .set_to(public())
+      .set_many_ccs(vec![self.get_followers_url()?]);
 
-    insert_activity(mod_.id, remove.clone(), true, pool).await?;
+    insert_activity(mod_.id, remove.clone(), true, context.pool()).await?;
 
-    let inboxes = self.get_follower_inboxes(pool).await?;
+    let inboxes = self.get_follower_inboxes(context.pool()).await?;
 
     // Note: For an accept, since it was automatic, no one pushed a button,
     // the community was the actor.
     // But for delete, the creator is the actor, and does the signing
-    send_activity(client, &remove, mod_, inboxes).await?;
+    send_activity(context.client(), &remove.into_any_base()?, mod_, inboxes).await?;
     Ok(())
   }
 
-  async fn send_undo_remove(
-    &self,
-    mod_: &User_,
-    client: &Client,
-    pool: &DbPool,
-  ) -> Result<(), LemmyError> {
-    let group = self.to_apub(pool).await?;
-
-    let id = format!("{}/remove/{}", self.actor_id, uuid::Uuid::new_v4());
-
-    let mut remove = Remove::default();
-    populate_object_props(
-      &mut remove.object_props,
-      vec![self.get_followers_url()],
-      &id,
-    )?;
+  async fn send_undo_remove(&self, mod_: &User_, context: &LemmyContext) -> Result<(), LemmyError> {
+    let group = self.to_apub(context.pool()).await?;
 
+    let mut remove = Remove::new(mod_.actor_id.to_owned(), group.into_any_base()?);
     remove
-      .remove_props
-      .set_actor_xsd_any_uri(mod_.actor_id.to_owned())?
-      .set_object_base_box(BaseBox::from_concrete(group)?)?;
+      .set_context(activitystreams::context())
+      .set_id(generate_activity_id(RemoveType::Remove)?)
+      .set_to(public())
+      .set_many_ccs(vec![self.get_followers_url()?]);
 
     // Undo that fake activity
-    let undo_id = format!("{}/undo/remove/{}", self.actor_id, uuid::Uuid::new_v4());
-    let mut undo = Undo::default();
-
-    populate_object_props(
-      &mut undo.object_props,
-      vec![self.get_followers_url()],
-      &undo_id,
-    )?;
-
+    let mut undo = Undo::new(mod_.actor_id.to_owned(), remove.into_any_base()?);
     undo
-      .undo_props
-      .set_actor_xsd_any_uri(mod_.actor_id.to_owned())?
-      .set_object_base_box(remove)?;
+      .set_context(activitystreams::context())
+      .set_id(generate_activity_id(LikeType::Like)?)
+      .set_to(public())
+      .set_many_ccs(vec![self.get_followers_url()?]);
 
-    insert_activity(mod_.id, undo.clone(), true, pool).await?;
+    insert_activity(mod_.id, undo.clone(), true, context.pool()).await?;
 
-    let inboxes = self.get_follower_inboxes(pool).await?;
+    let inboxes = self.get_follower_inboxes(context.pool()).await?;
 
     // Note: For an accept, since it was automatic, no one pushed a button,
     // the community was the actor.
     // But for remove , the creator is the actor, and does the signing
-    send_activity(client, &undo, mod_, inboxes).await?;
+    send_activity(context.client(), &undo.into_any_base()?, mod_, inboxes).await?;
     Ok(())
   }
 
   /// For a given community, returns the inboxes of all followers.
-  async fn get_follower_inboxes(&self, pool: &DbPool) -> Result<Vec<String>, LemmyError> {
+  ///
+  /// TODO: this function is very badly implemented, we should just store shared_inbox_url in
+  ///       CommunityFollowerView
+  async fn get_follower_inboxes(&self, pool: &DbPool) -> Result<Vec<Url>, LemmyError> {
     let id = self.id;
 
     let inboxes = blocking(pool, move |conn| {
@@ -335,8 +275,22 @@ impl ActorType for Community {
     .await??;
     let inboxes = inboxes
       .into_iter()
-      .map(|c| get_shared_inbox(&c.user_actor_id))
-      .filter(|s| !s.is_empty())
+      .map(|u| -> Result<Url, LemmyError> {
+        let url = Url::parse(&u.user_actor_id)?;
+        let domain = url.domain().context(location_info!())?;
+        let port = if let Some(port) = url.port() {
+          format!(":{}", port)
+        } else {
+          "".to_string()
+        };
+        Ok(Url::parse(&format!(
+          "{}://{}{}/inbox",
+          get_apub_protocol_string(),
+          domain,
+          port,
+        ))?)
+      })
+      .filter_map(Result::ok)
       .unique()
       .collect();
 
@@ -345,21 +299,23 @@ impl ActorType for Community {
 
   async fn send_follow(
     &self,
-    _follow_actor_id: &str,
-    _client: &Client,
-    _pool: &DbPool,
+    _follow_actor_id: &Url,
+    _context: &LemmyContext,
   ) -> Result<(), LemmyError> {
     unimplemented!()
   }
 
   async fn send_unfollow(
     &self,
-    _follow_actor_id: &str,
-    _client: &Client,
-    _pool: &DbPool,
+    _follow_actor_id: &Url,
+    _context: &LemmyContext,
   ) -> Result<(), LemmyError> {
     unimplemented!()
   }
+
+  fn user_id(&self) -> i32 {
+    self.creator_id
+  }
 }
 
 #[async_trait::async_trait(?Send)]
@@ -367,41 +323,92 @@ impl FromApub for CommunityForm {
   type ApubType = GroupExt;
 
   /// Parse an ActivityPub group received from another instance into a Lemmy community.
-  async fn from_apub(group: &GroupExt, client: &Client, pool: &DbPool) -> Result<Self, LemmyError> {
-    let creator_and_moderator_uris = group.attributed_to().unwrap();
+  async fn from_apub(
+    group: &GroupExt,
+    context: &LemmyContext,
+    expected_domain: Option<Url>,
+  ) -> Result<Self, LemmyError> {
+    let creator_and_moderator_uris = group.inner.attributed_to().context(location_info!())?;
     let creator_uri = creator_and_moderator_uris
       .as_many()
-      .unwrap()
+      .context(location_info!())?
       .iter()
       .next()
-      .unwrap()
+      .context(location_info!())?
       .as_xsd_any_uri()
-      .unwrap();
-
-    let creator = get_or_fetch_and_upsert_remote_user(creator_uri.as_str(), client, pool).await?;
+      .context(location_info!())?;
+
+    let creator = get_or_fetch_and_upsert_user(creator_uri, context).await?;
+    let name = group
+      .inner
+      .name()
+      .context(location_info!())?
+      .as_one()
+      .context(location_info!())?
+      .as_xsd_string()
+      .context(location_info!())?
+      .to_string();
+    let title = group
+      .inner
+      .preferred_username()
+      .context(location_info!())?
+      .to_string();
+    // TODO: should be parsed as html and tags like <script> removed (or use markdown source)
+    //       -> same for post.content etc
+    let description = group
+      .inner
+      .content()
+      .map(|s| s.as_single_xsd_string())
+      .flatten()
+      .map(|s| s.to_string());
+    check_slurs(&name)?;
+    check_slurs(&title)?;
+    check_slurs_opt(&description)?;
+
+    let icon = match group.icon() {
+      Some(any_image) => Some(
+        Image::from_any_base(any_image.as_one().context(location_info!())?.clone())
+          .context(location_info!())?
+          .context(location_info!())?
+          .url()
+          .context(location_info!())?
+          .as_single_xsd_any_uri()
+          .map(|u| u.to_string()),
+      ),
+      None => None,
+    };
+
+    let banner = match group.image() {
+      Some(any_image) => Some(
+        Image::from_any_base(any_image.as_one().context(location_info!())?.clone())
+          .context(location_info!())?
+          .context(location_info!())?
+          .url()
+          .context(location_info!())?
+          .as_single_xsd_any_uri()
+          .map(|u| u.to_string()),
+      ),
+      None => None,
+    };
 
     Ok(CommunityForm {
-      name: group.name().unwrap().as_single_xsd_string().unwrap().into(),
-      title: group.inner.preferred_username().unwrap().to_string(),
-      // TODO: should be parsed as html and tags like <script> removed (or use markdown source)
-      //       -> same for post.content etc
-      description: group
-        .content()
-        .map(|s| s.as_single_xsd_string().unwrap().into()),
+      name,
+      title,
+      description,
       category_id: group.ext_one.category.identifier.parse::<i32>()?,
       creator_id: creator.id,
       removed: None,
-      published: group
-        .published()
-        .map(|u| u.as_ref().to_owned().naive_local()),
-      updated: group.updated().map(|u| u.as_ref().to_owned().naive_local()),
+      published: group.inner.published().map(|u| u.to_owned().naive_local()),
+      updated: group.inner.updated().map(|u| u.to_owned().naive_local()),
       deleted: None,
       nsfw: group.ext_one.sensitive,
-      actor_id: group.id().unwrap().to_string(),
+      actor_id: check_actor_domain(group, expected_domain)?,
       local: false,
       private_key: None,
       public_key: Some(group.ext_two.to_owned().public_key.public_key_pem),
       last_refreshed_at: Some(naive_now()),
+      icon,
+      banner,
     })
   }
 }
@@ -409,15 +416,15 @@ impl FromApub for CommunityForm {
 /// Return the community json over HTTP.
 pub async fn get_apub_community_http(
   info: web::Path<CommunityQuery>,
-  db: DbPoolParam,
+  context: web::Data<LemmyContext>,
 ) -> Result<HttpResponse<Body>, LemmyError> {
-  let community = blocking(&db, move |conn| {
+  let community = blocking(context.pool(), move |conn| {
     Community::read_from_name(conn, &info.community_name)
   })
   .await??;
 
   if !community.deleted {
-    let apub = community.to_apub(&db).await?;
+    let apub = community.to_apub(context.pool()).await?;
 
     Ok(create_apub_response(&apub))
   } else {
@@ -428,59 +435,83 @@ pub async fn get_apub_community_http(
 /// Returns an empty followers collection, only populating the size (for privacy).
 pub async fn get_apub_community_followers(
   info: web::Path<CommunityQuery>,
-  db: DbPoolParam,
+  context: web::Data<LemmyContext>,
 ) -> Result<HttpResponse<Body>, LemmyError> {
-  let community = blocking(&db, move |conn| {
+  let community = blocking(context.pool(), move |conn| {
     Community::read_from_name(&conn, &info.community_name)
   })
   .await??;
 
   let community_id = community.id;
-  let community_followers = blocking(&db, move |conn| {
+  let community_followers = blocking(context.pool(), move |conn| {
     CommunityFollowerView::for_community(&conn, community_id)
   })
   .await??;
 
-  let mut collection = UnorderedCollection::new(vec![]);
+  let mut collection = UnorderedCollection::new();
   collection
-    .set_context(context())
-    // TODO: this needs its own ID
-    .set_id(community.actor_id.parse()?)
+    .set_context(activitystreams::context())
+    .set_id(community.get_followers_url()?)
     .set_total_items(community_followers.len() as u64);
   Ok(create_apub_response(&collection))
 }
 
-pub async fn do_announce<A>(
-  activity: A,
+pub async fn get_apub_community_outbox(
+  info: web::Path<CommunityQuery>,
+  context: web::Data<LemmyContext>,
+) -> Result<HttpResponse<Body>, LemmyError> {
+  let community = blocking(context.pool(), move |conn| {
+    Community::read_from_name(&conn, &info.community_name)
+  })
+  .await??;
+
+  let community_id = community.id;
+  let posts = blocking(context.pool(), move |conn| {
+    Post::list_for_community(conn, community_id)
+  })
+  .await??;
+
+  let mut pages: Vec<AnyBase> = vec![];
+  for p in posts {
+    pages.push(p.to_apub(context.pool()).await?.into_any_base()?);
+  }
+
+  let len = pages.len();
+  let mut collection = OrderedCollection::new();
+  collection
+    .set_many_items(pages)
+    .set_context(activitystreams::context())
+    .set_id(community.get_outbox_url()?)
+    .set_total_items(len as u64);
+  Ok(create_apub_response(&collection))
+}
+
+pub async fn do_announce(
+  activity: AnyBase,
   community: &Community,
-  sender: &dyn ActorType,
-  client: &Client,
-  pool: &DbPool,
-) -> Result<HttpResponse, LemmyError>
-where
-  A: Activity + Base + Serialize + Debug,
-{
-  let mut announce = Announce::default();
-  populate_object_props(
-    &mut announce.object_props,
-    vec![community.get_followers_url()],
-    &format!("{}/announce/{}", community.actor_id, uuid::Uuid::new_v4()),
-  )?;
+  sender: &User_,
+  context: &LemmyContext,
+) -> Result<(), LemmyError> {
+  let mut announce = Announce::new(community.actor_id.to_owned(), activity);
   announce
-    .announce_props
-    .set_actor_xsd_any_uri(community.actor_id.to_owned())?
-    .set_object_base_box(BaseBox::from_concrete(activity)?)?;
+    .set_context(activitystreams::context())
+    .set_id(generate_activity_id(AnnounceType::Announce)?)
+    .set_to(public())
+    .set_many_ccs(vec![community.get_followers_url()?]);
 
-  insert_activity(community.creator_id, announce.clone(), true, pool).await?;
+  insert_activity(community.creator_id, announce.clone(), true, context.pool()).await?;
 
-  // dont send to the instance where the activity originally came from, because that would result
-  // in a database error (same data inserted twice)
-  let mut to = community.get_follower_inboxes(pool).await?;
+  let mut to: Vec<Url> = community.get_follower_inboxes(context.pool()).await?;
 
+  // dont send to the local instance, nor to the instance where the activity originally came from,
+  // because that would result in a database error (same data inserted twice)
   // this seems to be the "easiest" stable alternative for remove_item()
-  to.retain(|x| *x != sender.get_shared_inbox_url());
+  let sender_shared_inbox = sender.get_shared_inbox_url()?;
+  to.retain(|x| x != &sender_shared_inbox);
+  let community_shared_inbox = community.get_shared_inbox_url()?;
+  to.retain(|x| x != &community_shared_inbox);
 
-  send_activity(client, &announce, community, to).await?;
+  send_activity(context.client(), &announce.into_any_base()?, community, to).await?;
 
-  Ok(HttpResponse::Ok().finish())
+  Ok(())
 }
diff --git a/server/src/apub/community_inbox.rs b/server/src/apub/community_inbox.rs
deleted file mode 100644 (file)
index 8ea6443..0000000
+++ /dev/null
@@ -1,136 +0,0 @@
-use crate::{
-  apub::{
-    extensions::signatures::verify,
-    fetcher::{get_or_fetch_and_upsert_remote_community, get_or_fetch_and_upsert_remote_user},
-    insert_activity,
-    ActorType,
-  },
-  blocking,
-  routes::{ChatServerParam, DbPoolParam},
-  LemmyError,
-};
-use activitystreams::activity::Undo;
-use activitystreams_new::activity::Follow;
-use actix_web::{client::Client, web, HttpRequest, HttpResponse};
-use lemmy_db::{
-  community::{Community, CommunityFollower, CommunityFollowerForm},
-  user::User_,
-  Followable,
-};
-use log::debug;
-use serde::Deserialize;
-use std::fmt::Debug;
-
-#[serde(untagged)]
-#[derive(Deserialize, Debug)]
-pub enum CommunityAcceptedObjects {
-  Follow(Follow),
-  Undo(Undo),
-}
-
-impl CommunityAcceptedObjects {
-  fn follow(&self) -> Result<Follow, LemmyError> {
-    match self {
-      CommunityAcceptedObjects::Follow(f) => Ok(f.to_owned()),
-      CommunityAcceptedObjects::Undo(u) => Ok(
-        u.undo_props
-          .get_object_base_box()
-          .to_owned()
-          .unwrap()
-          .to_owned()
-          .into_concrete::<Follow>()?,
-      ),
-    }
-  }
-}
-
-/// Handler for all incoming activities to community inboxes.
-pub async fn community_inbox(
-  request: HttpRequest,
-  input: web::Json<CommunityAcceptedObjects>,
-  path: web::Path<String>,
-  db: DbPoolParam,
-  client: web::Data<Client>,
-  _chat_server: ChatServerParam,
-) -> Result<HttpResponse, LemmyError> {
-  let input = input.into_inner();
-
-  let path = path.into_inner();
-  let community = blocking(&db, move |conn| Community::read_from_name(&conn, &path)).await??;
-
-  if !community.local {
-    return Err(
-      format_err!(
-        "Received activity is addressed to remote community {}",
-        &community.actor_id
-      )
-      .into(),
-    );
-  }
-  debug!(
-    "Community {} received activity {:?}",
-    &community.name, &input
-  );
-  let follow = input.follow()?;
-  let user_uri = follow.actor.as_single_xsd_any_uri().unwrap().to_string();
-  let community_uri = follow.object.as_single_xsd_any_uri().unwrap().to_string();
-
-  let user = get_or_fetch_and_upsert_remote_user(&user_uri, &client, &db).await?;
-  let community = get_or_fetch_and_upsert_remote_community(&community_uri, &client, &db).await?;
-
-  verify(&request, &user)?;
-
-  match input {
-    CommunityAcceptedObjects::Follow(f) => handle_follow(f, user, community, &client, db).await,
-    CommunityAcceptedObjects::Undo(u) => handle_undo_follow(u, user, community, db).await,
-  }
-}
-
-/// Handle a follow request from a remote user, adding it to the local database and returning an
-/// Accept activity.
-async fn handle_follow(
-  follow: Follow,
-  user: User_,
-  community: Community,
-  client: &Client,
-  db: DbPoolParam,
-) -> Result<HttpResponse, LemmyError> {
-  insert_activity(user.id, follow.clone(), false, &db).await?;
-
-  let community_follower_form = CommunityFollowerForm {
-    community_id: community.id,
-    user_id: user.id,
-  };
-
-  // This will fail if they're already a follower, but ignore the error.
-  blocking(&db, move |conn| {
-    CommunityFollower::follow(&conn, &community_follower_form).ok()
-  })
-  .await?;
-
-  community.send_accept_follow(&follow, &client, &db).await?;
-
-  Ok(HttpResponse::Ok().finish())
-}
-
-async fn handle_undo_follow(
-  undo: Undo,
-  user: User_,
-  community: Community,
-  db: DbPoolParam,
-) -> Result<HttpResponse, LemmyError> {
-  insert_activity(user.id, undo, false, &db).await?;
-
-  let community_follower_form = CommunityFollowerForm {
-    community_id: community.id,
-    user_id: user.id,
-  };
-
-  // This will fail if they aren't a follower, but ignore the error.
-  blocking(&db, move |conn| {
-    CommunityFollower::unfollow(&conn, &community_follower_form).ok()
-  })
-  .await?;
-
-  Ok(HttpResponse::Ok().finish())
-}
index 2120f6f14f5ba121c3627b441990b7e56d04a2de..3099a273eaca63aeaecd71f35fb1877d8ff6577b 100644 (file)
@@ -1,5 +1,6 @@
 use crate::LemmyError;
-use activitystreams::{ext::Extension, Actor};
+use activitystreams::unparsed::UnparsedMutExt;
+use activitystreams_ext::UnparsedExtension;
 use diesel::PgConnection;
 use lemmy_db::{category::Category, Crud};
 use serde::{Deserialize, Serialize};
@@ -37,4 +38,22 @@ impl GroupExtension {
   }
 }
 
-impl<T> Extension<T> for GroupExtension where T: Actor {}
+impl<U> UnparsedExtension<U> for GroupExtension
+where
+  U: UnparsedMutExt,
+{
+  type Error = serde_json::Error;
+
+  fn try_from_unparsed(unparsed_mut: &mut U) -> Result<Self, Self::Error> {
+    Ok(GroupExtension {
+      category: unparsed_mut.remove("category")?,
+      sensitive: unparsed_mut.remove("sensitive")?,
+    })
+  }
+
+  fn try_into_unparsed(self, unparsed_mut: &mut U) -> Result<(), Self::Error> {
+    unparsed_mut.insert("category", self.category)?;
+    unparsed_mut.insert("sensitive", self.sensitive)?;
+    Ok(())
+  }
+}
index 484807e3eaa208ecb87b2cb5cdccc32ff5d27114..aa3d01604dd2592609e6d479746ef6d306c02f0f 100644 (file)
@@ -1,4 +1,5 @@
-use activitystreams::{ext::Extension, Base};
+use activitystreams::unparsed::UnparsedMutExt;
+use activitystreams_ext::UnparsedExtension;
 use serde::{Deserialize, Serialize};
 
 #[derive(Clone, Debug, Default, Deserialize, Serialize)]
@@ -6,6 +7,27 @@ use serde::{Deserialize, Serialize};
 pub struct PageExtension {
   pub comments_enabled: bool,
   pub sensitive: bool,
+  pub stickied: bool,
 }
 
-impl<T> Extension<T> for PageExtension where T: Base {}
+impl<U> UnparsedExtension<U> for PageExtension
+where
+  U: UnparsedMutExt,
+{
+  type Error = serde_json::Error;
+
+  fn try_from_unparsed(unparsed_mut: &mut U) -> Result<Self, Self::Error> {
+    Ok(PageExtension {
+      comments_enabled: unparsed_mut.remove("commentsEnabled")?,
+      sensitive: unparsed_mut.remove("sensitive")?,
+      stickied: unparsed_mut.remove("stickied")?,
+    })
+  }
+
+  fn try_into_unparsed(self, unparsed_mut: &mut U) -> Result<(), Self::Error> {
+    unparsed_mut.insert("commentsEnabled", self.comments_enabled)?;
+    unparsed_mut.insert("sensitive", self.sensitive)?;
+    unparsed_mut.insert("stickied", self.stickied)?;
+    Ok(())
+  }
+}
index 1c930a958ca3c8fc3d22bebae18d5dca4e41b932..96063d5e0bdd2bbe34823ce020bdab2b8006e4e3 100644 (file)
@@ -1,10 +1,13 @@
 use crate::{apub::ActorType, LemmyError};
-use activitystreams::ext::Extension;
+use activitystreams::unparsed::UnparsedMutExt;
+use activitystreams_ext::UnparsedExtension;
 use actix_web::{client::ClientRequest, HttpRequest};
+use anyhow::{anyhow, Context};
 use http_signature_normalization_actix::{
   digest::{DigestClient, SignExt},
   Config,
 };
+use lemmy_utils::location_info;
 use log::debug;
 use openssl::{
   hash::MessageDigest,
@@ -24,8 +27,8 @@ pub async fn sign(
   actor: &dyn ActorType,
   activity: String,
 ) -> Result<DigestClient<String>, LemmyError> {
-  let signing_key_id = format!("{}#main-key", actor.actor_id());
-  let private_key = actor.private_key();
+  let signing_key_id = format!("{}#main-key", actor.actor_id()?);
+  let private_key = actor.private_key().context(location_info!())?;
 
   let digest_client = request
     .signature_with_digest(
@@ -35,8 +38,8 @@ pub async fn sign(
       activity,
       move |signing_string| {
         let private_key = PKey::private_key_from_pem(private_key.as_bytes())?;
-        let mut signer = Signer::new(MessageDigest::sha256(), &private_key).unwrap();
-        signer.update(signing_string.as_bytes()).unwrap();
+        let mut signer = Signer::new(MessageDigest::sha256(), &private_key)?;
+        signer.update(signing_string.as_bytes())?;
 
         Ok(base64::encode(signer.sign_to_vec()?)) as Result<_, LemmyError>
       },
@@ -47,6 +50,7 @@ pub async fn sign(
 }
 
 pub fn verify(request: &HttpRequest, actor: &dyn ActorType) -> Result<(), LemmyError> {
+  let public_key = actor.public_key().context(location_info!())?;
   let verified = HTTP_SIG_CONFIG
     .begin_verify(
       request.method(),
@@ -56,12 +60,11 @@ pub fn verify(request: &HttpRequest, actor: &dyn ActorType) -> Result<(), LemmyE
     .verify(|signature, signing_string| -> Result<bool, LemmyError> {
       debug!(
         "Verifying with key {}, message {}",
-        &actor.public_key(),
-        &signing_string
+        &public_key, &signing_string
       );
-      let public_key = PKey::public_key_from_pem(actor.public_key().as_bytes())?;
-      let mut verifier = Verifier::new(MessageDigest::sha256(), &public_key).unwrap();
-      verifier.update(&signing_string.as_bytes()).unwrap();
+      let public_key = PKey::public_key_from_pem(public_key.as_bytes())?;
+      let mut verifier = Verifier::new(MessageDigest::sha256(), &public_key)?;
+      verifier.update(&signing_string.as_bytes())?;
       Ok(verifier.verify(&base64::decode(signature)?)?)
     })?;
 
@@ -69,7 +72,7 @@ pub fn verify(request: &HttpRequest, actor: &dyn ActorType) -> Result<(), LemmyE
     debug!("verified signature for {}", &request.uri());
     Ok(())
   } else {
-    Err(format_err!("Invalid signature on request: {}", &request.uri()).into())
+    Err(anyhow!("Invalid signature on request: {}", &request.uri()).into())
   }
 }
 
@@ -98,4 +101,20 @@ impl PublicKey {
   }
 }
 
-impl<T> Extension<T> for PublicKeyExtension where T: activitystreams::Actor {}
+impl<U> UnparsedExtension<U> for PublicKeyExtension
+where
+  U: UnparsedMutExt,
+{
+  type Error = serde_json::Error;
+
+  fn try_from_unparsed(unparsed_mut: &mut U) -> Result<Self, Self::Error> {
+    Ok(PublicKeyExtension {
+      public_key: unparsed_mut.remove("publicKey")?,
+    })
+  }
+
+  fn try_into_unparsed(self, unparsed_mut: &mut U) -> Result<(), Self::Error> {
+    unparsed_mut.insert("publicKey", self.public_key)?;
+    Ok(())
+  }
+}
index f20c9eabe85882e402d4f596b5e0c9488c8e3877..0c42aa14e4b8bf57f222d2cbbec4b57d31edf03c 100644 (file)
@@ -1,17 +1,24 @@
 use crate::{
   api::site::SearchResponse,
-  apub::{is_apub_id_valid, FromApub, GroupExt, PageExt, PersonExt, APUB_JSON_CONTENT_TYPE},
+  apub::{
+    check_is_apub_id_valid,
+    ActorType,
+    FromApub,
+    GroupExt,
+    PageExt,
+    PersonExt,
+    APUB_JSON_CONTENT_TYPE,
+  },
   blocking,
   request::{retry, RecvError},
-  routes::nodeinfo::{NodeInfo, NodeInfoWellKnown},
-  DbPool,
+  LemmyContext,
   LemmyError,
 };
-use activitystreams::object::Note;
-use activitystreams_new::{base::BaseExt, prelude::*, primitives::XsdAnyUri};
+use activitystreams::{base::BaseExt, collection::OrderedCollection, object::Note, prelude::*};
 use actix_web::client::Client;
+use anyhow::{anyhow, Context};
 use chrono::NaiveDateTime;
-use diesel::{result::Error::NotFound, PgConnection};
+use diesel::result::Error::NotFound;
 use lemmy_db::{
   comment::{Comment, CommentForm},
   comment_view::CommentView,
@@ -26,27 +33,14 @@ use lemmy_db::{
   Joinable,
   SearchType,
 };
-use lemmy_utils::get_apub_protocol_string;
+use lemmy_utils::{get_apub_protocol_string, location_info};
 use log::debug;
 use serde::Deserialize;
 use std::{fmt::Debug, time::Duration};
 use url::Url;
 
 static ACTOR_REFETCH_INTERVAL_SECONDS: i64 = 24 * 60 * 60;
-
-// Fetch nodeinfo metadata from a remote instance.
-async fn _fetch_node_info(client: &Client, domain: &str) -> Result<NodeInfo, LemmyError> {
-  let well_known_uri = Url::parse(&format!(
-    "{}://{}/.well-known/nodeinfo",
-    get_apub_protocol_string(),
-    domain
-  ))?;
-
-  let well_known = fetch_remote_object::<NodeInfoWellKnown>(client, &well_known_uri).await?;
-  let nodeinfo = fetch_remote_object::<NodeInfo>(client, &well_known.links.href).await?;
-
-  Ok(nodeinfo)
-}
+static ACTOR_REFETCH_INTERVAL_SECONDS_DEBUG: i64 = 10;
 
 /// Fetch any type of ActivityPub object, handling things like HTTP headers, deserialisation,
 /// timeouts etc.
@@ -57,9 +51,7 @@ pub async fn fetch_remote_object<Response>(
 where
   Response: for<'de> Deserialize<'de>,
 {
-  if !is_apub_id_valid(&url) {
-    return Err(format_err!("Activitypub uri invalid or blocked: {}", url).into());
-  }
+  check_is_apub_id_valid(&url)?;
 
   let timeout = Duration::from_secs(60);
 
@@ -100,8 +92,7 @@ pub enum SearchAcceptedObjects {
 /// http://lemmy_alpha:8540/comment/2
 pub async fn search_by_apub_id(
   query: &str,
-  client: &Client,
-  pool: &DbPool,
+  context: &LemmyContext,
 ) -> Result<SearchResponse, LemmyError> {
   // Parse the shorthand query url
   let query_url = if query.contains('@') {
@@ -117,10 +108,10 @@ pub async fn search_by_apub_id(
         let split2 = split[0].split('!').collect::<Vec<&str>>();
         (format!("/c/{}", split2[1]), split[1])
       } else {
-        return Err(format_err!("Invalid search query: {}", query).into());
+        return Err(anyhow!("Invalid search query: {}", query).into());
       }
     } else {
-      return Err(format_err!("Invalid search query: {}", query).into());
+      return Err(anyhow!("Invalid search query: {}", query).into());
     };
 
     let url = format!("{}://{}{}", get_apub_protocol_string(), instance, name);
@@ -137,76 +128,87 @@ pub async fn search_by_apub_id(
     users: vec![],
   };
 
-  let response = match fetch_remote_object::<SearchAcceptedObjects>(client, &query_url).await? {
-    SearchAcceptedObjects::Person(p) => {
-      let user_uri = p.inner.id().unwrap().to_string();
+  let domain = query_url.domain().context("url has no domain")?;
+  let response =
+    match fetch_remote_object::<SearchAcceptedObjects>(context.client(), &query_url).await? {
+      SearchAcceptedObjects::Person(p) => {
+        let user_uri = p.inner.id(domain)?.context("person has no id")?;
 
-      let user = get_or_fetch_and_upsert_remote_user(&user_uri, client, pool).await?;
+        let user = get_or_fetch_and_upsert_user(&user_uri, context).await?;
 
-      response.users = vec![blocking(pool, move |conn| UserView::read(conn, user.id)).await??];
+        response.users = vec![
+          blocking(context.pool(), move |conn| {
+            UserView::get_user_secure(conn, user.id)
+          })
+          .await??,
+        ];
 
-      response
-    }
-    SearchAcceptedObjects::Group(g) => {
-      let community_uri = g.inner.id().unwrap().to_string();
+        response
+      }
+      SearchAcceptedObjects::Group(g) => {
+        let community_uri = g.inner.id(domain)?.context("group has no id")?;
 
-      let community =
-        get_or_fetch_and_upsert_remote_community(&community_uri, client, pool).await?;
+        let community = get_or_fetch_and_upsert_community(community_uri, context).await?;
 
-      // TODO Maybe at some point in the future, fetch all the history of a community
-      // fetch_community_outbox(&c, conn)?;
-      response.communities = vec![
-        blocking(pool, move |conn| {
-          CommunityView::read(conn, community.id, None)
-        })
-        .await??,
-      ];
+        response.communities = vec![
+          blocking(context.pool(), move |conn| {
+            CommunityView::read(conn, community.id, None)
+          })
+          .await??,
+        ];
 
-      response
-    }
-    SearchAcceptedObjects::Page(p) => {
-      let post_form = PostForm::from_apub(&p, client, pool).await?;
+        response
+      }
+      SearchAcceptedObjects::Page(p) => {
+        let post_form = PostForm::from_apub(&p, context, Some(query_url)).await?;
 
-      let p = blocking(pool, move |conn| upsert_post(&post_form, conn)).await??;
-      response.posts = vec![blocking(pool, move |conn| PostView::read(conn, p.id, None)).await??];
+        let p = blocking(context.pool(), move |conn| Post::upsert(conn, &post_form)).await??;
+        response.posts =
+          vec![blocking(context.pool(), move |conn| PostView::read(conn, p.id, None)).await??];
 
-      response
-    }
-    SearchAcceptedObjects::Comment(c) => {
-      let post_url = c
-        .object_props
-        .get_many_in_reply_to_xsd_any_uris()
-        .unwrap()
-        .next()
-        .unwrap()
-        .to_string();
-
-      // TODO: also fetch parent comments if any
-      let post = fetch_remote_object(client, &Url::parse(&post_url)?).await?;
-      let post_form = PostForm::from_apub(&post, client, pool).await?;
-      let comment_form = CommentForm::from_apub(&c, client, pool).await?;
-
-      blocking(pool, move |conn| upsert_post(&post_form, conn)).await??;
-      let c = blocking(pool, move |conn| upsert_comment(&comment_form, conn)).await??;
-      response.comments =
-        vec![blocking(pool, move |conn| CommentView::read(conn, c.id, None)).await??];
-
-      response
-    }
-  };
+        response
+      }
+      SearchAcceptedObjects::Comment(c) => {
+        let comment_form = CommentForm::from_apub(&c, context, Some(query_url)).await?;
+
+        let c = blocking(context.pool(), move |conn| {
+          Comment::upsert(conn, &comment_form)
+        })
+        .await??;
+        response.comments = vec![
+          blocking(context.pool(), move |conn| {
+            CommentView::read(conn, c.id, None)
+          })
+          .await??,
+        ];
+
+        response
+      }
+    };
 
   Ok(response)
 }
 
+pub async fn get_or_fetch_and_upsert_actor(
+  apub_id: &Url,
+  context: &LemmyContext,
+) -> Result<Box<dyn ActorType>, LemmyError> {
+  let user = get_or_fetch_and_upsert_user(apub_id, context).await;
+  let actor: Box<dyn ActorType> = match user {
+    Ok(u) => Box::new(u),
+    Err(_) => Box::new(get_or_fetch_and_upsert_community(apub_id, context).await?),
+  };
+  Ok(actor)
+}
+
 /// Check if a remote user exists, create if not found, if its too old update it.Fetch a user, insert/update it in the database and return the user.
-pub async fn get_or_fetch_and_upsert_remote_user(
-  apub_id: &str,
-  client: &Client,
-  pool: &DbPool,
+pub async fn get_or_fetch_and_upsert_user(
+  apub_id: &Url,
+  context: &LemmyContext,
 ) -> Result<User_, LemmyError> {
   let apub_id_owned = apub_id.to_owned();
-  let user = blocking(pool, move |conn| {
-    User_::read_from_actor_id(conn, &apub_id_owned)
+  let user = blocking(context.pool(), move |conn| {
+    User_::read_from_actor_id(conn, apub_id_owned.as_ref())
   })
   .await?;
 
@@ -214,21 +216,21 @@ pub async fn get_or_fetch_and_upsert_remote_user(
     // If its older than a day, re-fetch it
     Ok(u) if !u.local && should_refetch_actor(u.last_refreshed_at) => {
       debug!("Fetching and updating from remote user: {}", apub_id);
-      let person = fetch_remote_object::<PersonExt>(client, &Url::parse(apub_id)?).await?;
+      let person = fetch_remote_object::<PersonExt>(context.client(), apub_id).await?;
 
-      let mut uf = UserForm::from_apub(&person, client, pool).await?;
+      let mut uf = UserForm::from_apub(&person, context, Some(apub_id.to_owned())).await?;
       uf.last_refreshed_at = Some(naive_now());
-      let user = blocking(pool, move |conn| User_::update(conn, u.id, &uf)).await??;
+      let user = blocking(context.pool(), move |conn| User_::update(conn, u.id, &uf)).await??;
 
       Ok(user)
     }
     Ok(u) => Ok(u),
     Err(NotFound {}) => {
       debug!("Fetching and creating remote user: {}", apub_id);
-      let person = fetch_remote_object::<PersonExt>(client, &Url::parse(apub_id)?).await?;
+      let person = fetch_remote_object::<PersonExt>(context.client(), apub_id).await?;
 
-      let uf = UserForm::from_apub(&person, client, pool).await?;
-      let user = blocking(pool, move |conn| User_::create(conn, &uf)).await??;
+      let uf = UserForm::from_apub(&person, context, Some(apub_id.to_owned())).await?;
+      let user = blocking(context.pool(), move |conn| User_::create(conn, &uf)).await??;
 
       Ok(user)
     }
@@ -242,99 +244,126 @@ pub async fn get_or_fetch_and_upsert_remote_user(
 /// TODO it won't pick up new avatars, summaries etc until a day after.
 /// Actors need an "update" activity pushed to other servers to fix this.
 fn should_refetch_actor(last_refreshed: NaiveDateTime) -> bool {
-  if cfg!(debug_assertions) {
-    true
+  let update_interval = if cfg!(debug_assertions) {
+    // avoid infinite loop when fetching community outbox
+    chrono::Duration::seconds(ACTOR_REFETCH_INTERVAL_SECONDS_DEBUG)
   } else {
-    let update_interval = chrono::Duration::seconds(ACTOR_REFETCH_INTERVAL_SECONDS);
-    last_refreshed.lt(&(naive_now() - update_interval))
-  }
+    chrono::Duration::seconds(ACTOR_REFETCH_INTERVAL_SECONDS)
+  };
+  last_refreshed.lt(&(naive_now() - update_interval))
 }
 
 /// Check if a remote community exists, create if not found, if its too old update it.Fetch a community, insert/update it in the database and return the community.
-pub async fn get_or_fetch_and_upsert_remote_community(
-  apub_id: &str,
-  client: &Client,
-  pool: &DbPool,
+pub async fn get_or_fetch_and_upsert_community(
+  apub_id: &Url,
+  context: &LemmyContext,
 ) -> Result<Community, LemmyError> {
   let apub_id_owned = apub_id.to_owned();
-  let community = blocking(pool, move |conn| {
-    Community::read_from_actor_id(conn, &apub_id_owned)
+  let community = blocking(context.pool(), move |conn| {
+    Community::read_from_actor_id(conn, apub_id_owned.as_str())
   })
   .await?;
 
   match community {
     Ok(c) if !c.local && should_refetch_actor(c.last_refreshed_at) => {
       debug!("Fetching and updating from remote community: {}", apub_id);
-      let group = fetch_remote_object::<GroupExt>(client, &Url::parse(apub_id)?).await?;
-
-      let mut cf = CommunityForm::from_apub(&group, client, pool).await?;
-      cf.last_refreshed_at = Some(naive_now());
-      let community = blocking(pool, move |conn| Community::update(conn, c.id, &cf)).await??;
-
-      Ok(community)
+      fetch_remote_community(apub_id, context, Some(c.id)).await
     }
     Ok(c) => Ok(c),
     Err(NotFound {}) => {
       debug!("Fetching and creating remote community: {}", apub_id);
-      let group = fetch_remote_object::<GroupExt>(client, &Url::parse(apub_id)?).await?;
+      fetch_remote_community(apub_id, context, None).await
+    }
+    Err(e) => Err(e.into()),
+  }
+}
 
-      let cf = CommunityForm::from_apub(&group, client, pool).await?;
-      let community = blocking(pool, move |conn| Community::create(conn, &cf)).await??;
+async fn fetch_remote_community(
+  apub_id: &Url,
+  context: &LemmyContext,
+  community_id: Option<i32>,
+) -> Result<Community, LemmyError> {
+  let group = fetch_remote_object::<GroupExt>(context.client(), apub_id).await?;
 
-      // Also add the community moderators too
-      let attributed_to = group.inner.attributed_to().unwrap();
-      let creator_and_moderator_uris: Vec<&XsdAnyUri> = attributed_to
-        .as_many()
-        .unwrap()
-        .iter()
-        .map(|a| a.as_xsd_any_uri().unwrap())
-        .collect();
+  let cf = CommunityForm::from_apub(&group, context, Some(apub_id.to_owned())).await?;
+  let community = blocking(context.pool(), move |conn| {
+    if let Some(cid) = community_id {
+      Community::update(conn, cid, &cf)
+    } else {
+      Community::create(conn, &cf)
+    }
+  })
+  .await??;
 
-      let mut creator_and_moderators = Vec::new();
+  // Also add the community moderators too
+  let attributed_to = group.inner.attributed_to().context(location_info!())?;
+  let creator_and_moderator_uris: Vec<&Url> = attributed_to
+    .as_many()
+    .context(location_info!())?
+    .iter()
+    .map(|a| a.as_xsd_any_uri().context(""))
+    .collect::<Result<Vec<&Url>, anyhow::Error>>()?;
 
-      for uri in creator_and_moderator_uris {
-        let c_or_m = get_or_fetch_and_upsert_remote_user(uri.as_str(), client, pool).await?;
+  let mut creator_and_moderators = Vec::new();
 
-        creator_and_moderators.push(c_or_m);
-      }
+  for uri in creator_and_moderator_uris {
+    let c_or_m = get_or_fetch_and_upsert_user(uri, context).await?;
 
-      let community_id = community.id;
-      blocking(pool, move |conn| {
-        for mod_ in creator_and_moderators {
-          let community_moderator_form = CommunityModeratorForm {
-            community_id,
-            user_id: mod_.id,
-          };
-
-          CommunityModerator::join(conn, &community_moderator_form)?;
-        }
-        Ok(()) as Result<(), LemmyError>
-      })
-      .await??;
+    creator_and_moderators.push(c_or_m);
+  }
 
-      Ok(community)
-    }
-    Err(e) => Err(e.into()),
+  // TODO: need to make this work to update mods of existing communities
+  if community_id.is_none() {
+    let community_id = community.id;
+    blocking(context.pool(), move |conn| {
+      for mod_ in creator_and_moderators {
+        let community_moderator_form = CommunityModeratorForm {
+          community_id,
+          user_id: mod_.id,
+        };
+
+        CommunityModerator::join(conn, &community_moderator_form)?;
+      }
+      Ok(()) as Result<(), LemmyError>
+    })
+    .await??;
   }
-}
 
-fn upsert_post(post_form: &PostForm, conn: &PgConnection) -> Result<Post, LemmyError> {
-  let existing = Post::read_from_apub_id(conn, &post_form.ap_id);
-  match existing {
-    Err(NotFound {}) => Ok(Post::create(conn, &post_form)?),
-    Ok(p) => Ok(Post::update(conn, p.id, &post_form)?),
-    Err(e) => Err(e.into()),
+  // fetch outbox (maybe make this conditional)
+  let outbox =
+    fetch_remote_object::<OrderedCollection>(context.client(), &community.get_outbox_url()?)
+      .await?;
+  let outbox_items = outbox.items().context(location_info!())?.clone();
+  let mut outbox_items = outbox_items.many().context(location_info!())?;
+  if outbox_items.len() > 20 {
+    outbox_items = outbox_items[0..20].to_vec();
+  }
+  for o in outbox_items {
+    let page = PageExt::from_any_base(o)?.context(location_info!())?;
+    let post = PostForm::from_apub(&page, context, None).await?;
+    let post_ap_id = post.ap_id.clone();
+    // Check whether the post already exists in the local db
+    let existing = blocking(context.pool(), move |conn| {
+      Post::read_from_apub_id(conn, &post_ap_id)
+    })
+    .await?;
+    match existing {
+      Ok(e) => blocking(context.pool(), move |conn| Post::update(conn, e.id, &post)).await??,
+      Err(_) => blocking(context.pool(), move |conn| Post::create(conn, &post)).await??,
+    };
+    // TODO: we need to send a websocket update here
   }
+
+  Ok(community)
 }
 
-pub async fn get_or_fetch_and_insert_remote_post(
-  post_ap_id: &str,
-  client: &Client,
-  pool: &DbPool,
+pub async fn get_or_fetch_and_insert_post(
+  post_ap_id: &Url,
+  context: &LemmyContext,
 ) -> Result<Post, LemmyError> {
   let post_ap_id_owned = post_ap_id.to_owned();
-  let post = blocking(pool, move |conn| {
-    Post::read_from_apub_id(conn, &post_ap_id_owned)
+  let post = blocking(context.pool(), move |conn| {
+    Post::read_from_apub_id(conn, post_ap_id_owned.as_str())
   })
   .await?;
 
@@ -342,10 +371,10 @@ pub async fn get_or_fetch_and_insert_remote_post(
     Ok(p) => Ok(p),
     Err(NotFound {}) => {
       debug!("Fetching and creating remote post: {}", post_ap_id);
-      let post = fetch_remote_object::<PageExt>(client, &Url::parse(post_ap_id)?).await?;
-      let post_form = PostForm::from_apub(&post, client, pool).await?;
+      let post = fetch_remote_object::<PageExt>(context.client(), post_ap_id).await?;
+      let post_form = PostForm::from_apub(&post, context, Some(post_ap_id.to_owned())).await?;
 
-      let post = blocking(pool, move |conn| Post::create(conn, &post_form)).await??;
+      let post = blocking(context.pool(), move |conn| Post::create(conn, &post_form)).await??;
 
       Ok(post)
     }
@@ -353,23 +382,13 @@ pub async fn get_or_fetch_and_insert_remote_post(
   }
 }
 
-fn upsert_comment(comment_form: &CommentForm, conn: &PgConnection) -> Result<Comment, LemmyError> {
-  let existing = Comment::read_from_apub_id(conn, &comment_form.ap_id);
-  match existing {
-    Err(NotFound {}) => Ok(Comment::create(conn, &comment_form)?),
-    Ok(p) => Ok(Comment::update(conn, p.id, &comment_form)?),
-    Err(e) => Err(e.into()),
-  }
-}
-
-pub async fn get_or_fetch_and_insert_remote_comment(
-  comment_ap_id: &str,
-  client: &Client,
-  pool: &DbPool,
+pub async fn get_or_fetch_and_insert_comment(
+  comment_ap_id: &Url,
+  context: &LemmyContext,
 ) -> Result<Comment, LemmyError> {
   let comment_ap_id_owned = comment_ap_id.to_owned();
-  let comment = blocking(pool, move |conn| {
-    Comment::read_from_apub_id(conn, &comment_ap_id_owned)
+  let comment = blocking(context.pool(), move |conn| {
+    Comment::read_from_apub_id(conn, comment_ap_id_owned.as_str())
   })
   .await?;
 
@@ -380,36 +399,17 @@ pub async fn get_or_fetch_and_insert_remote_comment(
         "Fetching and creating remote comment and its parents: {}",
         comment_ap_id
       );
-      let comment = fetch_remote_object::<Note>(client, &Url::parse(comment_ap_id)?).await?;
-      let comment_form = CommentForm::from_apub(&comment, client, pool).await?;
+      let comment = fetch_remote_object::<Note>(context.client(), comment_ap_id).await?;
+      let comment_form =
+        CommentForm::from_apub(&comment, context, Some(comment_ap_id.to_owned())).await?;
 
-      let comment = blocking(pool, move |conn| Comment::create(conn, &comment_form)).await??;
+      let comment = blocking(context.pool(), move |conn| {
+        Comment::create(conn, &comment_form)
+      })
+      .await??;
 
       Ok(comment)
     }
     Err(e) => Err(e.into()),
   }
 }
-
-// TODO It should not be fetching data from a community outbox.
-// All posts, comments, comment likes, etc should be posts to our community_inbox
-// The only data we should be periodically fetching (if it hasn't been fetched in the last day
-// maybe), is community and user actors
-// and user actors
-// Fetch all posts in the outbox of the given user, and insert them into the database.
-// fn fetch_community_outbox(community: &Community, conn: &PgConnection) -> Result<Vec<Post>, LemmyError> {
-//   let outbox_url = Url::parse(&community.get_outbox_url())?;
-//   let outbox = fetch_remote_object::<OrderedCollection>(&outbox_url)?;
-//   let items = outbox.collection_props.get_many_items_base_boxes();
-
-//   Ok(
-//     items
-//       .unwrap()
-//       .map(|obox: &BaseBox| -> Result<PostForm, LemmyError> {
-//         let page = obox.clone().to_concrete::<Page>()?;
-//         PostForm::from_page(&page, conn)
-//       })
-//       .map(|pf| upsert_post(&pf?, conn))
-//       .collect::<Result<Vec<Post>, LemmyError>>()?,
-//   )
-// }
diff --git a/server/src/apub/inbox/activities/announce.rs b/server/src/apub/inbox/activities/announce.rs
new file mode 100644 (file)
index 0000000..e0fb806
--- /dev/null
@@ -0,0 +1,49 @@
+use crate::{
+  apub::inbox::{
+    activities::{
+      create::receive_create,
+      delete::receive_delete,
+      dislike::receive_dislike,
+      like::receive_like,
+      remove::receive_remove,
+      undo::receive_undo,
+      update::receive_update,
+    },
+    shared_inbox::{get_community_id_from_activity, receive_unhandled_activity},
+  },
+  LemmyContext,
+  LemmyError,
+};
+use activitystreams::{
+  activity::*,
+  base::{AnyBase, BaseExt},
+  prelude::ExtendsExt,
+};
+use actix_web::HttpResponse;
+use anyhow::Context;
+use lemmy_utils::location_info;
+
+pub async fn receive_announce(
+  activity: AnyBase,
+  context: &LemmyContext,
+) -> Result<HttpResponse, LemmyError> {
+  let announce = Announce::from_any_base(activity)?.context(location_info!())?;
+
+  // ensure that announce and community come from the same instance
+  let community = get_community_id_from_activity(&announce)?;
+  announce.id(community.domain().context(location_info!())?)?;
+
+  let kind = announce.object().as_single_kind_str();
+  let object = announce.object();
+  let object2 = object.clone().one().context(location_info!())?;
+  match kind {
+    Some("Create") => receive_create(object2, context).await,
+    Some("Update") => receive_update(object2, context).await,
+    Some("Like") => receive_like(object2, context).await,
+    Some("Dislike") => receive_dislike(object2, context).await,
+    Some("Delete") => receive_delete(object2, context).await,
+    Some("Remove") => receive_remove(object2, context).await,
+    Some("Undo") => receive_undo(object2, context).await,
+    _ => receive_unhandled_activity(announce),
+  }
+}
diff --git a/server/src/apub/inbox/activities/create.rs b/server/src/apub/inbox/activities/create.rs
new file mode 100644 (file)
index 0000000..caba560
--- /dev/null
@@ -0,0 +1,136 @@
+use crate::{
+  api::{
+    comment::{send_local_notifs, CommentResponse},
+    post::PostResponse,
+  },
+  apub::{
+    inbox::shared_inbox::{
+      announce_if_community_is_local,
+      get_user_from_activity,
+      receive_unhandled_activity,
+    },
+    ActorType,
+    FromApub,
+    PageExt,
+  },
+  blocking,
+  websocket::{
+    server::{SendComment, SendPost},
+    UserOperation,
+  },
+  LemmyContext,
+  LemmyError,
+};
+use activitystreams::{activity::Create, base::AnyBase, object::Note, prelude::*};
+use actix_web::HttpResponse;
+use anyhow::Context;
+use lemmy_db::{
+  comment::{Comment, CommentForm},
+  comment_view::CommentView,
+  post::{Post, PostForm},
+  post_view::PostView,
+};
+use lemmy_utils::{location_info, scrape_text_for_mentions};
+
+pub async fn receive_create(
+  activity: AnyBase,
+  context: &LemmyContext,
+) -> Result<HttpResponse, LemmyError> {
+  let create = Create::from_any_base(activity)?.context(location_info!())?;
+
+  // ensure that create and actor come from the same instance
+  let user = get_user_from_activity(&create, context).await?;
+  create.id(user.actor_id()?.domain().context(location_info!())?)?;
+
+  match create.object().as_single_kind_str() {
+    Some("Page") => receive_create_post(create, context).await,
+    Some("Note") => receive_create_comment(create, context).await,
+    _ => receive_unhandled_activity(create),
+  }
+}
+
+async fn receive_create_post(
+  create: Create,
+  context: &LemmyContext,
+) -> Result<HttpResponse, LemmyError> {
+  let user = get_user_from_activity(&create, context).await?;
+  let page = PageExt::from_any_base(create.object().to_owned().one().context(location_info!())?)?
+    .context(location_info!())?;
+
+  let post = PostForm::from_apub(&page, context, Some(user.actor_id()?)).await?;
+
+  // Using an upsert, since likes (which fetch the post), sometimes come in before the create
+  // resulting in double posts.
+  let inserted_post = blocking(context.pool(), move |conn| Post::upsert(conn, &post)).await??;
+
+  // Refetch the view
+  let inserted_post_id = inserted_post.id;
+  let post_view = blocking(context.pool(), move |conn| {
+    PostView::read(conn, inserted_post_id, None)
+  })
+  .await??;
+
+  let res = PostResponse { post: post_view };
+
+  context.chat_server().do_send(SendPost {
+    op: UserOperation::CreatePost,
+    post: res,
+    websocket_id: None,
+  });
+
+  announce_if_community_is_local(create, &user, context).await?;
+  Ok(HttpResponse::Ok().finish())
+}
+
+async fn receive_create_comment(
+  create: Create,
+  context: &LemmyContext,
+) -> Result<HttpResponse, LemmyError> {
+  let user = get_user_from_activity(&create, context).await?;
+  let note = Note::from_any_base(create.object().to_owned().one().context(location_info!())?)?
+    .context(location_info!())?;
+
+  let comment = CommentForm::from_apub(&note, context, Some(user.actor_id()?)).await?;
+
+  let inserted_comment =
+    blocking(context.pool(), move |conn| Comment::upsert(conn, &comment)).await??;
+
+  let post_id = inserted_comment.post_id;
+  let post = blocking(context.pool(), move |conn| Post::read(conn, post_id)).await??;
+
+  // Note:
+  // Although mentions could be gotten from the post tags (they are included there), or the ccs,
+  // Its much easier to scrape them from the comment body, since the API has to do that
+  // anyway.
+  let mentions = scrape_text_for_mentions(&inserted_comment.content);
+  let recipient_ids = send_local_notifs(
+    mentions,
+    inserted_comment.clone(),
+    &user,
+    post,
+    context.pool(),
+    true,
+  )
+  .await?;
+
+  // Refetch the view
+  let comment_view = blocking(context.pool(), move |conn| {
+    CommentView::read(conn, inserted_comment.id, None)
+  })
+  .await??;
+
+  let res = CommentResponse {
+    comment: comment_view,
+    recipient_ids,
+    form_id: None,
+  };
+
+  context.chat_server().do_send(SendComment {
+    op: UserOperation::CreateComment,
+    comment: res,
+    websocket_id: None,
+  });
+
+  announce_if_community_is_local(create, &user, context).await?;
+  Ok(HttpResponse::Ok().finish())
+}
diff --git a/server/src/apub/inbox/activities/delete.rs b/server/src/apub/inbox/activities/delete.rs
new file mode 100644 (file)
index 0000000..2a6689d
--- /dev/null
@@ -0,0 +1,230 @@
+use crate::{
+  api::{comment::CommentResponse, community::CommunityResponse, post::PostResponse},
+  apub::{
+    fetcher::{get_or_fetch_and_insert_comment, get_or_fetch_and_insert_post},
+    inbox::shared_inbox::{
+      announce_if_community_is_local,
+      get_user_from_activity,
+      receive_unhandled_activity,
+    },
+    ActorType,
+    FromApub,
+    GroupExt,
+    PageExt,
+  },
+  blocking,
+  websocket::{
+    server::{SendComment, SendCommunityRoomMessage, SendPost},
+    UserOperation,
+  },
+  LemmyContext,
+  LemmyError,
+};
+use activitystreams::{activity::Delete, base::AnyBase, object::Note, prelude::*};
+use actix_web::HttpResponse;
+use anyhow::Context;
+use lemmy_db::{
+  comment::{Comment, CommentForm},
+  comment_view::CommentView,
+  community::{Community, CommunityForm},
+  community_view::CommunityView,
+  naive_now,
+  post::{Post, PostForm},
+  post_view::PostView,
+  Crud,
+};
+use lemmy_utils::location_info;
+
+pub async fn receive_delete(
+  activity: AnyBase,
+  context: &LemmyContext,
+) -> Result<HttpResponse, LemmyError> {
+  let delete = Delete::from_any_base(activity)?.context(location_info!())?;
+  match delete.object().as_single_kind_str() {
+    Some("Page") => receive_delete_post(delete, context).await,
+    Some("Note") => receive_delete_comment(delete, context).await,
+    Some("Group") => receive_delete_community(delete, context).await,
+    _ => receive_unhandled_activity(delete),
+  }
+}
+
+async fn receive_delete_post(
+  delete: Delete,
+  context: &LemmyContext,
+) -> Result<HttpResponse, LemmyError> {
+  let user = get_user_from_activity(&delete, context).await?;
+  let page = PageExt::from_any_base(delete.object().to_owned().one().context(location_info!())?)?
+    .context(location_info!())?;
+
+  let post_ap_id = PostForm::from_apub(&page, context, Some(user.actor_id()?))
+    .await?
+    .get_ap_id()?;
+
+  let post = get_or_fetch_and_insert_post(&post_ap_id, context).await?;
+
+  let post_form = PostForm {
+    name: post.name.to_owned(),
+    url: post.url.to_owned(),
+    body: post.body.to_owned(),
+    creator_id: post.creator_id.to_owned(),
+    community_id: post.community_id,
+    removed: None,
+    deleted: Some(true),
+    nsfw: post.nsfw,
+    locked: None,
+    stickied: None,
+    updated: Some(naive_now()),
+    embed_title: post.embed_title,
+    embed_description: post.embed_description,
+    embed_html: post.embed_html,
+    thumbnail_url: post.thumbnail_url,
+    ap_id: post.ap_id,
+    local: post.local,
+    published: None,
+  };
+  let post_id = post.id;
+  blocking(context.pool(), move |conn| {
+    Post::update(conn, post_id, &post_form)
+  })
+  .await??;
+
+  // Refetch the view
+  let post_id = post.id;
+  let post_view = blocking(context.pool(), move |conn| {
+    PostView::read(conn, post_id, None)
+  })
+  .await??;
+
+  let res = PostResponse { post: post_view };
+
+  context.chat_server().do_send(SendPost {
+    op: UserOperation::EditPost,
+    post: res,
+    websocket_id: None,
+  });
+
+  announce_if_community_is_local(delete, &user, context).await?;
+  Ok(HttpResponse::Ok().finish())
+}
+
+async fn receive_delete_comment(
+  delete: Delete,
+  context: &LemmyContext,
+) -> Result<HttpResponse, LemmyError> {
+  let user = get_user_from_activity(&delete, context).await?;
+  let note = Note::from_any_base(delete.object().to_owned().one().context(location_info!())?)?
+    .context(location_info!())?;
+
+  let comment_ap_id = CommentForm::from_apub(&note, context, Some(user.actor_id()?))
+    .await?
+    .get_ap_id()?;
+
+  let comment = get_or_fetch_and_insert_comment(&comment_ap_id, context).await?;
+
+  let comment_form = CommentForm {
+    content: comment.content.to_owned(),
+    parent_id: comment.parent_id,
+    post_id: comment.post_id,
+    creator_id: comment.creator_id,
+    removed: None,
+    deleted: Some(true),
+    read: None,
+    published: None,
+    updated: Some(naive_now()),
+    ap_id: comment.ap_id,
+    local: comment.local,
+  };
+  let comment_id = comment.id;
+  blocking(context.pool(), move |conn| {
+    Comment::update(conn, comment_id, &comment_form)
+  })
+  .await??;
+
+  // Refetch the view
+  let comment_id = comment.id;
+  let comment_view = blocking(context.pool(), move |conn| {
+    CommentView::read(conn, comment_id, None)
+  })
+  .await??;
+
+  // TODO get those recipient actor ids from somewhere
+  let recipient_ids = vec![];
+  let res = CommentResponse {
+    comment: comment_view,
+    recipient_ids,
+    form_id: None,
+  };
+
+  context.chat_server().do_send(SendComment {
+    op: UserOperation::EditComment,
+    comment: res,
+    websocket_id: None,
+  });
+
+  announce_if_community_is_local(delete, &user, context).await?;
+  Ok(HttpResponse::Ok().finish())
+}
+
+async fn receive_delete_community(
+  delete: Delete,
+  context: &LemmyContext,
+) -> Result<HttpResponse, LemmyError> {
+  let group = GroupExt::from_any_base(delete.object().to_owned().one().context(location_info!())?)?
+    .context(location_info!())?;
+  let user = get_user_from_activity(&delete, context).await?;
+
+  let community_actor_id = CommunityForm::from_apub(&group, context, Some(user.actor_id()?))
+    .await?
+    .actor_id;
+
+  let community = blocking(context.pool(), move |conn| {
+    Community::read_from_actor_id(conn, &community_actor_id)
+  })
+  .await??;
+
+  let community_form = CommunityForm {
+    name: community.name.to_owned(),
+    title: community.title.to_owned(),
+    description: community.description.to_owned(),
+    category_id: community.category_id, // Note: need to keep this due to foreign key constraint
+    creator_id: community.creator_id,   // Note: need to keep this due to foreign key constraint
+    removed: None,
+    published: None,
+    updated: Some(naive_now()),
+    deleted: Some(true),
+    nsfw: community.nsfw,
+    actor_id: community.actor_id,
+    local: community.local,
+    private_key: community.private_key,
+    public_key: community.public_key,
+    last_refreshed_at: None,
+    icon: Some(community.icon.to_owned()),
+    banner: Some(community.banner.to_owned()),
+  };
+
+  let community_id = community.id;
+  blocking(context.pool(), move |conn| {
+    Community::update(conn, community_id, &community_form)
+  })
+  .await??;
+
+  let community_id = community.id;
+  let res = CommunityResponse {
+    community: blocking(context.pool(), move |conn| {
+      CommunityView::read(conn, community_id, None)
+    })
+    .await??,
+  };
+
+  let community_id = res.community.id;
+
+  context.chat_server().do_send(SendCommunityRoomMessage {
+    op: UserOperation::EditCommunity,
+    response: res,
+    community_id,
+    websocket_id: None,
+  });
+
+  announce_if_community_is_local(delete, &user, context).await?;
+  Ok(HttpResponse::Ok().finish())
+}
diff --git a/server/src/apub/inbox/activities/dislike.rs b/server/src/apub/inbox/activities/dislike.rs
new file mode 100644 (file)
index 0000000..4d59dd4
--- /dev/null
@@ -0,0 +1,150 @@
+use crate::{
+  api::{comment::CommentResponse, post::PostResponse},
+  apub::{
+    fetcher::{get_or_fetch_and_insert_comment, get_or_fetch_and_insert_post},
+    inbox::shared_inbox::{
+      announce_if_community_is_local,
+      get_user_from_activity,
+      receive_unhandled_activity,
+    },
+    FromApub,
+    PageExt,
+  },
+  blocking,
+  websocket::{
+    server::{SendComment, SendPost},
+    UserOperation,
+  },
+  LemmyContext,
+  LemmyError,
+};
+use activitystreams::{activity::Dislike, base::AnyBase, object::Note, prelude::*};
+use actix_web::HttpResponse;
+use anyhow::Context;
+use lemmy_db::{
+  comment::{CommentForm, CommentLike, CommentLikeForm},
+  comment_view::CommentView,
+  post::{PostForm, PostLike, PostLikeForm},
+  post_view::PostView,
+  Likeable,
+};
+use lemmy_utils::location_info;
+
+pub async fn receive_dislike(
+  activity: AnyBase,
+  context: &LemmyContext,
+) -> Result<HttpResponse, LemmyError> {
+  let dislike = Dislike::from_any_base(activity)?.context(location_info!())?;
+  match dislike.object().as_single_kind_str() {
+    Some("Page") => receive_dislike_post(dislike, context).await,
+    Some("Note") => receive_dislike_comment(dislike, context).await,
+    _ => receive_unhandled_activity(dislike),
+  }
+}
+
+async fn receive_dislike_post(
+  dislike: Dislike,
+  context: &LemmyContext,
+) -> Result<HttpResponse, LemmyError> {
+  let user = get_user_from_activity(&dislike, context).await?;
+  let page = PageExt::from_any_base(
+    dislike
+      .object()
+      .to_owned()
+      .one()
+      .context(location_info!())?,
+  )?
+  .context(location_info!())?;
+
+  let post = PostForm::from_apub(&page, context, None).await?;
+
+  let post_id = get_or_fetch_and_insert_post(&post.get_ap_id()?, context)
+    .await?
+    .id;
+
+  let like_form = PostLikeForm {
+    post_id,
+    user_id: user.id,
+    score: -1,
+  };
+  let user_id = user.id;
+  blocking(context.pool(), move |conn| {
+    PostLike::remove(conn, user_id, post_id)?;
+    PostLike::like(conn, &like_form)
+  })
+  .await??;
+
+  // Refetch the view
+  let post_view = blocking(context.pool(), move |conn| {
+    PostView::read(conn, post_id, None)
+  })
+  .await??;
+
+  let res = PostResponse { post: post_view };
+
+  context.chat_server().do_send(SendPost {
+    op: UserOperation::CreatePostLike,
+    post: res,
+    websocket_id: None,
+  });
+
+  announce_if_community_is_local(dislike, &user, context).await?;
+  Ok(HttpResponse::Ok().finish())
+}
+
+async fn receive_dislike_comment(
+  dislike: Dislike,
+  context: &LemmyContext,
+) -> Result<HttpResponse, LemmyError> {
+  let note = Note::from_any_base(
+    dislike
+      .object()
+      .to_owned()
+      .one()
+      .context(location_info!())?,
+  )?
+  .context(location_info!())?;
+  let user = get_user_from_activity(&dislike, context).await?;
+
+  let comment = CommentForm::from_apub(&note, context, None).await?;
+
+  let comment_id = get_or_fetch_and_insert_comment(&comment.get_ap_id()?, context)
+    .await?
+    .id;
+
+  let like_form = CommentLikeForm {
+    comment_id,
+    post_id: comment.post_id,
+    user_id: user.id,
+    score: -1,
+  };
+  let user_id = user.id;
+  blocking(context.pool(), move |conn| {
+    CommentLike::remove(conn, user_id, comment_id)?;
+    CommentLike::like(conn, &like_form)
+  })
+  .await??;
+
+  // Refetch the view
+  let comment_view = blocking(context.pool(), move |conn| {
+    CommentView::read(conn, comment_id, None)
+  })
+  .await??;
+
+  // TODO get those recipient actor ids from somewhere
+  let recipient_ids = vec![];
+  let res = CommentResponse {
+    comment: comment_view,
+    recipient_ids,
+    form_id: None,
+  };
+
+  context.chat_server().do_send(SendComment {
+    op: UserOperation::CreateCommentLike,
+    comment: res,
+    websocket_id: None,
+  });
+
+  announce_if_community_is_local(dislike, &user, context).await?;
+  Ok(HttpResponse::Ok().finish())
+}
diff --git a/server/src/apub/inbox/activities/like.rs b/server/src/apub/inbox/activities/like.rs
new file mode 100644 (file)
index 0000000..a3f19b3
--- /dev/null
@@ -0,0 +1,135 @@
+use crate::{
+  api::{comment::CommentResponse, post::PostResponse},
+  apub::{
+    fetcher::{get_or_fetch_and_insert_comment, get_or_fetch_and_insert_post},
+    inbox::shared_inbox::{
+      announce_if_community_is_local,
+      get_user_from_activity,
+      receive_unhandled_activity,
+    },
+    FromApub,
+    PageExt,
+  },
+  blocking,
+  websocket::{
+    server::{SendComment, SendPost},
+    UserOperation,
+  },
+  LemmyContext,
+  LemmyError,
+};
+use activitystreams::{activity::Like, base::AnyBase, object::Note, prelude::*};
+use actix_web::HttpResponse;
+use anyhow::Context;
+use lemmy_db::{
+  comment::{CommentForm, CommentLike, CommentLikeForm},
+  comment_view::CommentView,
+  post::{PostForm, PostLike, PostLikeForm},
+  post_view::PostView,
+  Likeable,
+};
+use lemmy_utils::location_info;
+
+pub async fn receive_like(
+  activity: AnyBase,
+  context: &LemmyContext,
+) -> Result<HttpResponse, LemmyError> {
+  let like = Like::from_any_base(activity)?.context(location_info!())?;
+  match like.object().as_single_kind_str() {
+    Some("Page") => receive_like_post(like, context).await,
+    Some("Note") => receive_like_comment(like, context).await,
+    _ => receive_unhandled_activity(like),
+  }
+}
+
+async fn receive_like_post(like: Like, context: &LemmyContext) -> Result<HttpResponse, LemmyError> {
+  let user = get_user_from_activity(&like, context).await?;
+  let page = PageExt::from_any_base(like.object().to_owned().one().context(location_info!())?)?
+    .context(location_info!())?;
+
+  let post = PostForm::from_apub(&page, context, None).await?;
+
+  let post_id = get_or_fetch_and_insert_post(&post.get_ap_id()?, context)
+    .await?
+    .id;
+
+  let like_form = PostLikeForm {
+    post_id,
+    user_id: user.id,
+    score: 1,
+  };
+  let user_id = user.id;
+  blocking(context.pool(), move |conn| {
+    PostLike::remove(conn, user_id, post_id)?;
+    PostLike::like(conn, &like_form)
+  })
+  .await??;
+
+  // Refetch the view
+  let post_view = blocking(context.pool(), move |conn| {
+    PostView::read(conn, post_id, None)
+  })
+  .await??;
+
+  let res = PostResponse { post: post_view };
+
+  context.chat_server().do_send(SendPost {
+    op: UserOperation::CreatePostLike,
+    post: res,
+    websocket_id: None,
+  });
+
+  announce_if_community_is_local(like, &user, context).await?;
+  Ok(HttpResponse::Ok().finish())
+}
+
+async fn receive_like_comment(
+  like: Like,
+  context: &LemmyContext,
+) -> Result<HttpResponse, LemmyError> {
+  let note = Note::from_any_base(like.object().to_owned().one().context(location_info!())?)?
+    .context(location_info!())?;
+  let user = get_user_from_activity(&like, context).await?;
+
+  let comment = CommentForm::from_apub(&note, context, None).await?;
+
+  let comment_id = get_or_fetch_and_insert_comment(&comment.get_ap_id()?, context)
+    .await?
+    .id;
+
+  let like_form = CommentLikeForm {
+    comment_id,
+    post_id: comment.post_id,
+    user_id: user.id,
+    score: 1,
+  };
+  let user_id = user.id;
+  blocking(context.pool(), move |conn| {
+    CommentLike::remove(conn, user_id, comment_id)?;
+    CommentLike::like(conn, &like_form)
+  })
+  .await??;
+
+  // Refetch the view
+  let comment_view = blocking(context.pool(), move |conn| {
+    CommentView::read(conn, comment_id, None)
+  })
+  .await??;
+
+  // TODO get those recipient actor ids from somewhere
+  let recipient_ids = vec![];
+  let res = CommentResponse {
+    comment: comment_view,
+    recipient_ids,
+    form_id: None,
+  };
+
+  context.chat_server().do_send(SendComment {
+    op: UserOperation::CreateCommentLike,
+    comment: res,
+    websocket_id: None,
+  });
+
+  announce_if_community_is_local(like, &user, context).await?;
+  Ok(HttpResponse::Ok().finish())
+}
diff --git a/server/src/apub/inbox/activities/mod.rs b/server/src/apub/inbox/activities/mod.rs
new file mode 100644 (file)
index 0000000..aa50cd1
--- /dev/null
@@ -0,0 +1,8 @@
+pub mod announce;
+pub mod create;
+pub mod delete;
+pub mod dislike;
+pub mod like;
+pub mod remove;
+pub mod undo;
+pub mod update;
diff --git a/server/src/apub/inbox/activities/remove.rs b/server/src/apub/inbox/activities/remove.rs
new file mode 100644 (file)
index 0000000..83a7484
--- /dev/null
@@ -0,0 +1,237 @@
+use crate::{
+  api::{comment::CommentResponse, community::CommunityResponse, post::PostResponse},
+  apub::{
+    fetcher::{get_or_fetch_and_insert_comment, get_or_fetch_and_insert_post},
+    inbox::shared_inbox::{
+      announce_if_community_is_local,
+      get_community_id_from_activity,
+      get_user_from_activity,
+      receive_unhandled_activity,
+    },
+    ActorType,
+    FromApub,
+    GroupExt,
+    PageExt,
+  },
+  blocking,
+  websocket::{
+    server::{SendComment, SendCommunityRoomMessage, SendPost},
+    UserOperation,
+  },
+  LemmyContext,
+  LemmyError,
+};
+use activitystreams::{activity::Remove, base::AnyBase, object::Note, prelude::*};
+use actix_web::HttpResponse;
+use anyhow::{anyhow, Context};
+use lemmy_db::{
+  comment::{Comment, CommentForm},
+  comment_view::CommentView,
+  community::{Community, CommunityForm},
+  community_view::CommunityView,
+  naive_now,
+  post::{Post, PostForm},
+  post_view::PostView,
+  Crud,
+};
+use lemmy_utils::location_info;
+
+pub async fn receive_remove(
+  activity: AnyBase,
+  context: &LemmyContext,
+) -> Result<HttpResponse, LemmyError> {
+  let remove = Remove::from_any_base(activity)?.context(location_info!())?;
+  let actor = get_user_from_activity(&remove, context).await?;
+  let community = get_community_id_from_activity(&remove)?;
+  if actor.actor_id()?.domain() != community.domain() {
+    return Err(anyhow!("Remove activities are only allowed on local objects").into());
+  }
+
+  match remove.object().as_single_kind_str() {
+    Some("Page") => receive_remove_post(remove, context).await,
+    Some("Note") => receive_remove_comment(remove, context).await,
+    Some("Group") => receive_remove_community(remove, context).await,
+    _ => receive_unhandled_activity(remove),
+  }
+}
+
+async fn receive_remove_post(
+  remove: Remove,
+  context: &LemmyContext,
+) -> Result<HttpResponse, LemmyError> {
+  let mod_ = get_user_from_activity(&remove, context).await?;
+  let page = PageExt::from_any_base(remove.object().to_owned().one().context(location_info!())?)?
+    .context(location_info!())?;
+
+  let post_ap_id = PostForm::from_apub(&page, context, None)
+    .await?
+    .get_ap_id()?;
+
+  let post = get_or_fetch_and_insert_post(&post_ap_id, context).await?;
+
+  let post_form = PostForm {
+    name: post.name.to_owned(),
+    url: post.url.to_owned(),
+    body: post.body.to_owned(),
+    creator_id: post.creator_id.to_owned(),
+    community_id: post.community_id,
+    removed: Some(true),
+    deleted: None,
+    nsfw: post.nsfw,
+    locked: None,
+    stickied: None,
+    updated: Some(naive_now()),
+    embed_title: post.embed_title,
+    embed_description: post.embed_description,
+    embed_html: post.embed_html,
+    thumbnail_url: post.thumbnail_url,
+    ap_id: post.ap_id,
+    local: post.local,
+    published: None,
+  };
+  let post_id = post.id;
+  blocking(context.pool(), move |conn| {
+    Post::update(conn, post_id, &post_form)
+  })
+  .await??;
+
+  // Refetch the view
+  let post_id = post.id;
+  let post_view = blocking(context.pool(), move |conn| {
+    PostView::read(conn, post_id, None)
+  })
+  .await??;
+
+  let res = PostResponse { post: post_view };
+
+  context.chat_server().do_send(SendPost {
+    op: UserOperation::EditPost,
+    post: res,
+    websocket_id: None,
+  });
+
+  announce_if_community_is_local(remove, &mod_, context).await?;
+  Ok(HttpResponse::Ok().finish())
+}
+
+async fn receive_remove_comment(
+  remove: Remove,
+  context: &LemmyContext,
+) -> Result<HttpResponse, LemmyError> {
+  let mod_ = get_user_from_activity(&remove, context).await?;
+  let note = Note::from_any_base(remove.object().to_owned().one().context(location_info!())?)?
+    .context(location_info!())?;
+
+  let comment_ap_id = CommentForm::from_apub(&note, context, None)
+    .await?
+    .get_ap_id()?;
+
+  let comment = get_or_fetch_and_insert_comment(&comment_ap_id, context).await?;
+
+  let comment_form = CommentForm {
+    content: comment.content.to_owned(),
+    parent_id: comment.parent_id,
+    post_id: comment.post_id,
+    creator_id: comment.creator_id,
+    removed: Some(true),
+    deleted: None,
+    read: None,
+    published: None,
+    updated: Some(naive_now()),
+    ap_id: comment.ap_id,
+    local: comment.local,
+  };
+  let comment_id = comment.id;
+  blocking(context.pool(), move |conn| {
+    Comment::update(conn, comment_id, &comment_form)
+  })
+  .await??;
+
+  // Refetch the view
+  let comment_id = comment.id;
+  let comment_view = blocking(context.pool(), move |conn| {
+    CommentView::read(conn, comment_id, None)
+  })
+  .await??;
+
+  // TODO get those recipient actor ids from somewhere
+  let recipient_ids = vec![];
+  let res = CommentResponse {
+    comment: comment_view,
+    recipient_ids,
+    form_id: None,
+  };
+
+  context.chat_server().do_send(SendComment {
+    op: UserOperation::EditComment,
+    comment: res,
+    websocket_id: None,
+  });
+
+  announce_if_community_is_local(remove, &mod_, context).await?;
+  Ok(HttpResponse::Ok().finish())
+}
+
+async fn receive_remove_community(
+  remove: Remove,
+  context: &LemmyContext,
+) -> Result<HttpResponse, LemmyError> {
+  let mod_ = get_user_from_activity(&remove, context).await?;
+  let group = GroupExt::from_any_base(remove.object().to_owned().one().context(location_info!())?)?
+    .context(location_info!())?;
+
+  let community_actor_id = CommunityForm::from_apub(&group, context, Some(mod_.actor_id()?))
+    .await?
+    .actor_id;
+
+  let community = blocking(context.pool(), move |conn| {
+    Community::read_from_actor_id(conn, &community_actor_id)
+  })
+  .await??;
+
+  let community_form = CommunityForm {
+    name: community.name.to_owned(),
+    title: community.title.to_owned(),
+    description: community.description.to_owned(),
+    category_id: community.category_id, // Note: need to keep this due to foreign key constraint
+    creator_id: community.creator_id,   // Note: need to keep this due to foreign key constraint
+    removed: Some(true),
+    published: None,
+    updated: Some(naive_now()),
+    deleted: None,
+    nsfw: community.nsfw,
+    actor_id: community.actor_id,
+    local: community.local,
+    private_key: community.private_key,
+    public_key: community.public_key,
+    last_refreshed_at: None,
+    icon: Some(community.icon.to_owned()),
+    banner: Some(community.banner.to_owned()),
+  };
+
+  let community_id = community.id;
+  blocking(context.pool(), move |conn| {
+    Community::update(conn, community_id, &community_form)
+  })
+  .await??;
+
+  let community_id = community.id;
+  let res = CommunityResponse {
+    community: blocking(context.pool(), move |conn| {
+      CommunityView::read(conn, community_id, None)
+    })
+    .await??,
+  };
+
+  let community_id = res.community.id;
+
+  context.chat_server().do_send(SendCommunityRoomMessage {
+    op: UserOperation::EditCommunity,
+    response: res,
+    community_id,
+    websocket_id: None,
+  });
+
+  announce_if_community_is_local(remove, &mod_, context).await?;
+  Ok(HttpResponse::Ok().finish())
+}
diff --git a/server/src/apub/inbox/activities/undo.rs b/server/src/apub/inbox/activities/undo.rs
new file mode 100644 (file)
index 0000000..f356c91
--- /dev/null
@@ -0,0 +1,699 @@
+use crate::{
+  api::{comment::CommentResponse, community::CommunityResponse, post::PostResponse},
+  apub::{
+    fetcher::{get_or_fetch_and_insert_comment, get_or_fetch_and_insert_post},
+    inbox::shared_inbox::{
+      announce_if_community_is_local,
+      get_user_from_activity,
+      receive_unhandled_activity,
+    },
+    ActorType,
+    FromApub,
+    GroupExt,
+    PageExt,
+  },
+  blocking,
+  websocket::{
+    server::{SendComment, SendCommunityRoomMessage, SendPost},
+    UserOperation,
+  },
+  LemmyContext,
+  LemmyError,
+};
+use activitystreams::{
+  activity::*,
+  base::{AnyBase, AsBase},
+  object::Note,
+  prelude::*,
+};
+use actix_web::HttpResponse;
+use anyhow::{anyhow, Context};
+use lemmy_db::{
+  comment::{Comment, CommentForm, CommentLike},
+  comment_view::CommentView,
+  community::{Community, CommunityForm},
+  community_view::CommunityView,
+  naive_now,
+  post::{Post, PostForm, PostLike},
+  post_view::PostView,
+  Crud,
+  Likeable,
+};
+use lemmy_utils::location_info;
+
+pub async fn receive_undo(
+  activity: AnyBase,
+  context: &LemmyContext,
+) -> Result<HttpResponse, LemmyError> {
+  let undo = Undo::from_any_base(activity)?.context(location_info!())?;
+  match undo.object().as_single_kind_str() {
+    Some("Delete") => receive_undo_delete(undo, context).await,
+    Some("Remove") => receive_undo_remove(undo, context).await,
+    Some("Like") => receive_undo_like(undo, context).await,
+    Some("Dislike") => receive_undo_dislike(undo, context).await,
+    _ => receive_unhandled_activity(undo),
+  }
+}
+
+fn check_is_undo_valid<T, A>(outer_activity: &Undo, inner_activity: &T) -> Result<(), LemmyError>
+where
+  T: AsBase<A> + ActorAndObjectRef,
+{
+  let outer_actor = outer_activity.actor()?;
+  let outer_actor_uri = outer_actor
+    .as_single_xsd_any_uri()
+    .context(location_info!())?;
+
+  let inner_actor = inner_activity.actor()?;
+  let inner_actor_uri = inner_actor
+    .as_single_xsd_any_uri()
+    .context(location_info!())?;
+
+  if outer_actor_uri.domain() != inner_actor_uri.domain() {
+    Err(anyhow!("Cant undo activities from a different instance").into())
+  } else {
+    Ok(())
+  }
+}
+
+async fn receive_undo_delete(
+  undo: Undo,
+  context: &LemmyContext,
+) -> Result<HttpResponse, LemmyError> {
+  let delete = Delete::from_any_base(undo.object().to_owned().one().context(location_info!())?)?
+    .context(location_info!())?;
+  check_is_undo_valid(&undo, &delete)?;
+  let type_ = delete
+    .object()
+    .as_single_kind_str()
+    .context(location_info!())?;
+  match type_ {
+    "Note" => receive_undo_delete_comment(undo, &delete, context).await,
+    "Page" => receive_undo_delete_post(undo, &delete, context).await,
+    "Group" => receive_undo_delete_community(undo, &delete, context).await,
+    d => Err(anyhow!("Undo Delete type {} not supported", d).into()),
+  }
+}
+
+async fn receive_undo_remove(
+  undo: Undo,
+  context: &LemmyContext,
+) -> Result<HttpResponse, LemmyError> {
+  let remove = Remove::from_any_base(undo.object().to_owned().one().context(location_info!())?)?
+    .context(location_info!())?;
+  check_is_undo_valid(&undo, &remove)?;
+
+  let type_ = remove
+    .object()
+    .as_single_kind_str()
+    .context(location_info!())?;
+  match type_ {
+    "Note" => receive_undo_remove_comment(undo, &remove, context).await,
+    "Page" => receive_undo_remove_post(undo, &remove, context).await,
+    "Group" => receive_undo_remove_community(undo, &remove, context).await,
+    d => Err(anyhow!("Undo Delete type {} not supported", d).into()),
+  }
+}
+
+async fn receive_undo_like(undo: Undo, context: &LemmyContext) -> Result<HttpResponse, LemmyError> {
+  let like = Like::from_any_base(undo.object().to_owned().one().context(location_info!())?)?
+    .context(location_info!())?;
+  check_is_undo_valid(&undo, &like)?;
+
+  let type_ = like
+    .object()
+    .as_single_kind_str()
+    .context(location_info!())?;
+  match type_ {
+    "Note" => receive_undo_like_comment(undo, &like, context).await,
+    "Page" => receive_undo_like_post(undo, &like, context).await,
+    d => Err(anyhow!("Undo Delete type {} not supported", d).into()),
+  }
+}
+
+async fn receive_undo_dislike(
+  undo: Undo,
+  context: &LemmyContext,
+) -> Result<HttpResponse, LemmyError> {
+  let dislike = Dislike::from_any_base(undo.object().to_owned().one().context(location_info!())?)?
+    .context(location_info!())?;
+  check_is_undo_valid(&undo, &dislike)?;
+
+  let type_ = dislike
+    .object()
+    .as_single_kind_str()
+    .context(location_info!())?;
+  match type_ {
+    "Note" => receive_undo_dislike_comment(undo, &dislike, context).await,
+    "Page" => receive_undo_dislike_post(undo, &dislike, context).await,
+    d => Err(anyhow!("Undo Delete type {} not supported", d).into()),
+  }
+}
+
+async fn receive_undo_delete_comment(
+  undo: Undo,
+  delete: &Delete,
+  context: &LemmyContext,
+) -> Result<HttpResponse, LemmyError> {
+  let user = get_user_from_activity(delete, context).await?;
+  let note = Note::from_any_base(delete.object().to_owned().one().context(location_info!())?)?
+    .context(location_info!())?;
+
+  let comment_ap_id = CommentForm::from_apub(&note, context, Some(user.actor_id()?))
+    .await?
+    .get_ap_id()?;
+
+  let comment = get_or_fetch_and_insert_comment(&comment_ap_id, context).await?;
+
+  let comment_form = CommentForm {
+    content: comment.content.to_owned(),
+    parent_id: comment.parent_id,
+    post_id: comment.post_id,
+    creator_id: comment.creator_id,
+    removed: None,
+    deleted: Some(false),
+    read: None,
+    published: None,
+    updated: Some(naive_now()),
+    ap_id: comment.ap_id,
+    local: comment.local,
+  };
+  let comment_id = comment.id;
+  blocking(context.pool(), move |conn| {
+    Comment::update(conn, comment_id, &comment_form)
+  })
+  .await??;
+
+  // Refetch the view
+  let comment_id = comment.id;
+  let comment_view = blocking(context.pool(), move |conn| {
+    CommentView::read(conn, comment_id, None)
+  })
+  .await??;
+
+  // TODO get those recipient actor ids from somewhere
+  let recipient_ids = vec![];
+  let res = CommentResponse {
+    comment: comment_view,
+    recipient_ids,
+    form_id: None,
+  };
+
+  context.chat_server().do_send(SendComment {
+    op: UserOperation::EditComment,
+    comment: res,
+    websocket_id: None,
+  });
+
+  announce_if_community_is_local(undo, &user, context).await?;
+  Ok(HttpResponse::Ok().finish())
+}
+
+async fn receive_undo_remove_comment(
+  undo: Undo,
+  remove: &Remove,
+  context: &LemmyContext,
+) -> Result<HttpResponse, LemmyError> {
+  let mod_ = get_user_from_activity(remove, context).await?;
+  let note = Note::from_any_base(remove.object().to_owned().one().context(location_info!())?)?
+    .context(location_info!())?;
+
+  let comment_ap_id = CommentForm::from_apub(&note, context, None)
+    .await?
+    .get_ap_id()?;
+
+  let comment = get_or_fetch_and_insert_comment(&comment_ap_id, context).await?;
+
+  let comment_form = CommentForm {
+    content: comment.content.to_owned(),
+    parent_id: comment.parent_id,
+    post_id: comment.post_id,
+    creator_id: comment.creator_id,
+    removed: Some(false),
+    deleted: None,
+    read: None,
+    published: None,
+    updated: Some(naive_now()),
+    ap_id: comment.ap_id,
+    local: comment.local,
+  };
+  let comment_id = comment.id;
+  blocking(context.pool(), move |conn| {
+    Comment::update(conn, comment_id, &comment_form)
+  })
+  .await??;
+
+  // Refetch the view
+  let comment_id = comment.id;
+  let comment_view = blocking(context.pool(), move |conn| {
+    CommentView::read(conn, comment_id, None)
+  })
+  .await??;
+
+  // TODO get those recipient actor ids from somewhere
+  let recipient_ids = vec![];
+  let res = CommentResponse {
+    comment: comment_view,
+    recipient_ids,
+    form_id: None,
+  };
+
+  context.chat_server().do_send(SendComment {
+    op: UserOperation::EditComment,
+    comment: res,
+    websocket_id: None,
+  });
+
+  announce_if_community_is_local(undo, &mod_, context).await?;
+  Ok(HttpResponse::Ok().finish())
+}
+
+async fn receive_undo_delete_post(
+  undo: Undo,
+  delete: &Delete,
+  context: &LemmyContext,
+) -> Result<HttpResponse, LemmyError> {
+  let user = get_user_from_activity(delete, context).await?;
+  let page = PageExt::from_any_base(delete.object().to_owned().one().context(location_info!())?)?
+    .context(location_info!())?;
+
+  let post_ap_id = PostForm::from_apub(&page, context, Some(user.actor_id()?))
+    .await?
+    .get_ap_id()?;
+
+  let post = get_or_fetch_and_insert_post(&post_ap_id, context).await?;
+
+  let post_form = PostForm {
+    name: post.name.to_owned(),
+    url: post.url.to_owned(),
+    body: post.body.to_owned(),
+    creator_id: post.creator_id.to_owned(),
+    community_id: post.community_id,
+    removed: None,
+    deleted: Some(false),
+    nsfw: post.nsfw,
+    locked: None,
+    stickied: None,
+    updated: Some(naive_now()),
+    embed_title: post.embed_title,
+    embed_description: post.embed_description,
+    embed_html: post.embed_html,
+    thumbnail_url: post.thumbnail_url,
+    ap_id: post.ap_id,
+    local: post.local,
+    published: None,
+  };
+  let post_id = post.id;
+  blocking(context.pool(), move |conn| {
+    Post::update(conn, post_id, &post_form)
+  })
+  .await??;
+
+  // Refetch the view
+  let post_id = post.id;
+  let post_view = blocking(context.pool(), move |conn| {
+    PostView::read(conn, post_id, None)
+  })
+  .await??;
+
+  let res = PostResponse { post: post_view };
+
+  context.chat_server().do_send(SendPost {
+    op: UserOperation::EditPost,
+    post: res,
+    websocket_id: None,
+  });
+
+  announce_if_community_is_local(undo, &user, context).await?;
+  Ok(HttpResponse::Ok().finish())
+}
+
+async fn receive_undo_remove_post(
+  undo: Undo,
+  remove: &Remove,
+  context: &LemmyContext,
+) -> Result<HttpResponse, LemmyError> {
+  let mod_ = get_user_from_activity(remove, context).await?;
+  let page = PageExt::from_any_base(remove.object().to_owned().one().context(location_info!())?)?
+    .context(location_info!())?;
+
+  let post_ap_id = PostForm::from_apub(&page, context, None)
+    .await?
+    .get_ap_id()?;
+
+  let post = get_or_fetch_and_insert_post(&post_ap_id, context).await?;
+
+  let post_form = PostForm {
+    name: post.name.to_owned(),
+    url: post.url.to_owned(),
+    body: post.body.to_owned(),
+    creator_id: post.creator_id.to_owned(),
+    community_id: post.community_id,
+    removed: Some(false),
+    deleted: None,
+    nsfw: post.nsfw,
+    locked: None,
+    stickied: None,
+    updated: Some(naive_now()),
+    embed_title: post.embed_title,
+    embed_description: post.embed_description,
+    embed_html: post.embed_html,
+    thumbnail_url: post.thumbnail_url,
+    ap_id: post.ap_id,
+    local: post.local,
+    published: None,
+  };
+  let post_id = post.id;
+  blocking(context.pool(), move |conn| {
+    Post::update(conn, post_id, &post_form)
+  })
+  .await??;
+
+  // Refetch the view
+  let post_id = post.id;
+  let post_view = blocking(context.pool(), move |conn| {
+    PostView::read(conn, post_id, None)
+  })
+  .await??;
+
+  let res = PostResponse { post: post_view };
+
+  context.chat_server().do_send(SendPost {
+    op: UserOperation::EditPost,
+    post: res,
+    websocket_id: None,
+  });
+
+  announce_if_community_is_local(undo, &mod_, context).await?;
+  Ok(HttpResponse::Ok().finish())
+}
+
+async fn receive_undo_delete_community(
+  undo: Undo,
+  delete: &Delete,
+  context: &LemmyContext,
+) -> Result<HttpResponse, LemmyError> {
+  let user = get_user_from_activity(delete, context).await?;
+  let group = GroupExt::from_any_base(delete.object().to_owned().one().context(location_info!())?)?
+    .context(location_info!())?;
+
+  let community_actor_id = CommunityForm::from_apub(&group, context, Some(user.actor_id()?))
+    .await?
+    .actor_id;
+
+  let community = blocking(context.pool(), move |conn| {
+    Community::read_from_actor_id(conn, &community_actor_id)
+  })
+  .await??;
+
+  let community_form = CommunityForm {
+    name: community.name.to_owned(),
+    title: community.title.to_owned(),
+    description: community.description.to_owned(),
+    category_id: community.category_id, // Note: need to keep this due to foreign key constraint
+    creator_id: community.creator_id,   // Note: need to keep this due to foreign key constraint
+    removed: None,
+    published: None,
+    updated: Some(naive_now()),
+    deleted: Some(false),
+    nsfw: community.nsfw,
+    actor_id: community.actor_id,
+    local: community.local,
+    private_key: community.private_key,
+    public_key: community.public_key,
+    last_refreshed_at: None,
+    icon: Some(community.icon.to_owned()),
+    banner: Some(community.banner.to_owned()),
+  };
+
+  let community_id = community.id;
+  blocking(context.pool(), move |conn| {
+    Community::update(conn, community_id, &community_form)
+  })
+  .await??;
+
+  let community_id = community.id;
+  let res = CommunityResponse {
+    community: blocking(context.pool(), move |conn| {
+      CommunityView::read(conn, community_id, None)
+    })
+    .await??,
+  };
+
+  let community_id = res.community.id;
+
+  context.chat_server().do_send(SendCommunityRoomMessage {
+    op: UserOperation::EditCommunity,
+    response: res,
+    community_id,
+    websocket_id: None,
+  });
+
+  announce_if_community_is_local(undo, &user, context).await?;
+  Ok(HttpResponse::Ok().finish())
+}
+
+async fn receive_undo_remove_community(
+  undo: Undo,
+  remove: &Remove,
+  context: &LemmyContext,
+) -> Result<HttpResponse, LemmyError> {
+  let mod_ = get_user_from_activity(remove, context).await?;
+  let group = GroupExt::from_any_base(remove.object().to_owned().one().context(location_info!())?)?
+    .context(location_info!())?;
+
+  let community_actor_id = CommunityForm::from_apub(&group, context, Some(mod_.actor_id()?))
+    .await?
+    .actor_id;
+
+  let community = blocking(context.pool(), move |conn| {
+    Community::read_from_actor_id(conn, &community_actor_id)
+  })
+  .await??;
+
+  let community_form = CommunityForm {
+    name: community.name.to_owned(),
+    title: community.title.to_owned(),
+    description: community.description.to_owned(),
+    category_id: community.category_id, // Note: need to keep this due to foreign key constraint
+    creator_id: community.creator_id,   // Note: need to keep this due to foreign key constraint
+    removed: Some(false),
+    published: None,
+    updated: Some(naive_now()),
+    deleted: None,
+    nsfw: community.nsfw,
+    actor_id: community.actor_id,
+    local: community.local,
+    private_key: community.private_key,
+    public_key: community.public_key,
+    last_refreshed_at: None,
+    icon: Some(community.icon.to_owned()),
+    banner: Some(community.banner.to_owned()),
+  };
+
+  let community_id = community.id;
+  blocking(context.pool(), move |conn| {
+    Community::update(conn, community_id, &community_form)
+  })
+  .await??;
+
+  let community_id = community.id;
+  let res = CommunityResponse {
+    community: blocking(context.pool(), move |conn| {
+      CommunityView::read(conn, community_id, None)
+    })
+    .await??,
+  };
+
+  let community_id = res.community.id;
+
+  context.chat_server().do_send(SendCommunityRoomMessage {
+    op: UserOperation::EditCommunity,
+    response: res,
+    community_id,
+    websocket_id: None,
+  });
+
+  announce_if_community_is_local(undo, &mod_, context).await?;
+  Ok(HttpResponse::Ok().finish())
+}
+
+async fn receive_undo_like_comment(
+  undo: Undo,
+  like: &Like,
+  context: &LemmyContext,
+) -> Result<HttpResponse, LemmyError> {
+  let user = get_user_from_activity(like, context).await?;
+  let note = Note::from_any_base(like.object().to_owned().one().context(location_info!())?)?
+    .context(location_info!())?;
+
+  let comment = CommentForm::from_apub(&note, context, None).await?;
+
+  let comment_id = get_or_fetch_and_insert_comment(&comment.get_ap_id()?, context)
+    .await?
+    .id;
+
+  let user_id = user.id;
+  blocking(context.pool(), move |conn| {
+    CommentLike::remove(conn, user_id, comment_id)
+  })
+  .await??;
+
+  // Refetch the view
+  let comment_view = blocking(context.pool(), move |conn| {
+    CommentView::read(conn, comment_id, None)
+  })
+  .await??;
+
+  // TODO get those recipient actor ids from somewhere
+  let recipient_ids = vec![];
+  let res = CommentResponse {
+    comment: comment_view,
+    recipient_ids,
+    form_id: None,
+  };
+
+  context.chat_server().do_send(SendComment {
+    op: UserOperation::CreateCommentLike,
+    comment: res,
+    websocket_id: None,
+  });
+
+  announce_if_community_is_local(undo, &user, context).await?;
+  Ok(HttpResponse::Ok().finish())
+}
+
+async fn receive_undo_like_post(
+  undo: Undo,
+  like: &Like,
+  context: &LemmyContext,
+) -> Result<HttpResponse, LemmyError> {
+  let user = get_user_from_activity(like, context).await?;
+  let page = PageExt::from_any_base(like.object().to_owned().one().context(location_info!())?)?
+    .context(location_info!())?;
+
+  let post = PostForm::from_apub(&page, context, None).await?;
+
+  let post_id = get_or_fetch_and_insert_post(&post.get_ap_id()?, context)
+    .await?
+    .id;
+
+  let user_id = user.id;
+  blocking(context.pool(), move |conn| {
+    PostLike::remove(conn, user_id, post_id)
+  })
+  .await??;
+
+  // Refetch the view
+  let post_view = blocking(context.pool(), move |conn| {
+    PostView::read(conn, post_id, None)
+  })
+  .await??;
+
+  let res = PostResponse { post: post_view };
+
+  context.chat_server().do_send(SendPost {
+    op: UserOperation::CreatePostLike,
+    post: res,
+    websocket_id: None,
+  });
+
+  announce_if_community_is_local(undo, &user, context).await?;
+  Ok(HttpResponse::Ok().finish())
+}
+
+async fn receive_undo_dislike_comment(
+  undo: Undo,
+  dislike: &Dislike,
+  context: &LemmyContext,
+) -> Result<HttpResponse, LemmyError> {
+  let user = get_user_from_activity(dislike, context).await?;
+  let note = Note::from_any_base(
+    dislike
+      .object()
+      .to_owned()
+      .one()
+      .context(location_info!())?,
+  )?
+  .context(location_info!())?;
+
+  let comment = CommentForm::from_apub(&note, context, None).await?;
+
+  let comment_id = get_or_fetch_and_insert_comment(&comment.get_ap_id()?, context)
+    .await?
+    .id;
+
+  let user_id = user.id;
+  blocking(context.pool(), move |conn| {
+    CommentLike::remove(conn, user_id, comment_id)
+  })
+  .await??;
+
+  // Refetch the view
+  let comment_view = blocking(context.pool(), move |conn| {
+    CommentView::read(conn, comment_id, None)
+  })
+  .await??;
+
+  // TODO get those recipient actor ids from somewhere
+  let recipient_ids = vec![];
+  let res = CommentResponse {
+    comment: comment_view,
+    recipient_ids,
+    form_id: None,
+  };
+
+  context.chat_server().do_send(SendComment {
+    op: UserOperation::CreateCommentLike,
+    comment: res,
+    websocket_id: None,
+  });
+
+  announce_if_community_is_local(undo, &user, context).await?;
+  Ok(HttpResponse::Ok().finish())
+}
+
+async fn receive_undo_dislike_post(
+  undo: Undo,
+  dislike: &Dislike,
+  context: &LemmyContext,
+) -> Result<HttpResponse, LemmyError> {
+  let user = get_user_from_activity(dislike, context).await?;
+  let page = PageExt::from_any_base(
+    dislike
+      .object()
+      .to_owned()
+      .one()
+      .context(location_info!())?,
+  )?
+  .context(location_info!())?;
+
+  let post = PostForm::from_apub(&page, context, None).await?;
+
+  let post_id = get_or_fetch_and_insert_post(&post.get_ap_id()?, context)
+    .await?
+    .id;
+
+  let user_id = user.id;
+  blocking(context.pool(), move |conn| {
+    PostLike::remove(conn, user_id, post_id)
+  })
+  .await??;
+
+  // Refetch the view
+  let post_view = blocking(context.pool(), move |conn| {
+    PostView::read(conn, post_id, None)
+  })
+  .await??;
+
+  let res = PostResponse { post: post_view };
+
+  context.chat_server().do_send(SendPost {
+    op: UserOperation::CreatePostLike,
+    post: res,
+    websocket_id: None,
+  });
+
+  announce_if_community_is_local(undo, &user, context).await?;
+  Ok(HttpResponse::Ok().finish())
+}
diff --git a/server/src/apub/inbox/activities/update.rs b/server/src/apub/inbox/activities/update.rs
new file mode 100644 (file)
index 0000000..f8e6603
--- /dev/null
@@ -0,0 +1,144 @@
+use crate::{
+  api::{
+    comment::{send_local_notifs, CommentResponse},
+    post::PostResponse,
+  },
+  apub::{
+    fetcher::{get_or_fetch_and_insert_comment, get_or_fetch_and_insert_post},
+    inbox::shared_inbox::{
+      announce_if_community_is_local,
+      get_user_from_activity,
+      receive_unhandled_activity,
+    },
+    ActorType,
+    FromApub,
+    PageExt,
+  },
+  blocking,
+  websocket::{
+    server::{SendComment, SendPost},
+    UserOperation,
+  },
+  LemmyContext,
+  LemmyError,
+};
+use activitystreams::{activity::Update, base::AnyBase, object::Note, prelude::*};
+use actix_web::HttpResponse;
+use anyhow::Context;
+use lemmy_db::{
+  comment::{Comment, CommentForm},
+  comment_view::CommentView,
+  post::{Post, PostForm},
+  post_view::PostView,
+  Crud,
+};
+use lemmy_utils::{location_info, scrape_text_for_mentions};
+
+pub async fn receive_update(
+  activity: AnyBase,
+  context: &LemmyContext,
+) -> Result<HttpResponse, LemmyError> {
+  let update = Update::from_any_base(activity)?.context(location_info!())?;
+
+  // ensure that update and actor come from the same instance
+  let user = get_user_from_activity(&update, context).await?;
+  update.id(user.actor_id()?.domain().context(location_info!())?)?;
+
+  match update.object().as_single_kind_str() {
+    Some("Page") => receive_update_post(update, context).await,
+    Some("Note") => receive_update_comment(update, context).await,
+    _ => receive_unhandled_activity(update),
+  }
+}
+
+async fn receive_update_post(
+  update: Update,
+  context: &LemmyContext,
+) -> Result<HttpResponse, LemmyError> {
+  let user = get_user_from_activity(&update, context).await?;
+  let page = PageExt::from_any_base(update.object().to_owned().one().context(location_info!())?)?
+    .context(location_info!())?;
+
+  let post = PostForm::from_apub(&page, context, Some(user.actor_id()?)).await?;
+
+  let original_post_id = get_or_fetch_and_insert_post(&post.get_ap_id()?, context)
+    .await?
+    .id;
+
+  blocking(context.pool(), move |conn| {
+    Post::update(conn, original_post_id, &post)
+  })
+  .await??;
+
+  // Refetch the view
+  let post_view = blocking(context.pool(), move |conn| {
+    PostView::read(conn, original_post_id, None)
+  })
+  .await??;
+
+  let res = PostResponse { post: post_view };
+
+  context.chat_server().do_send(SendPost {
+    op: UserOperation::EditPost,
+    post: res,
+    websocket_id: None,
+  });
+
+  announce_if_community_is_local(update, &user, context).await?;
+  Ok(HttpResponse::Ok().finish())
+}
+
+async fn receive_update_comment(
+  update: Update,
+  context: &LemmyContext,
+) -> Result<HttpResponse, LemmyError> {
+  let note = Note::from_any_base(update.object().to_owned().one().context(location_info!())?)?
+    .context(location_info!())?;
+  let user = get_user_from_activity(&update, context).await?;
+
+  let comment = CommentForm::from_apub(&note, context, Some(user.actor_id()?)).await?;
+
+  let original_comment_id = get_or_fetch_and_insert_comment(&comment.get_ap_id()?, context)
+    .await?
+    .id;
+
+  let updated_comment = blocking(context.pool(), move |conn| {
+    Comment::update(conn, original_comment_id, &comment)
+  })
+  .await??;
+
+  let post_id = updated_comment.post_id;
+  let post = blocking(context.pool(), move |conn| Post::read(conn, post_id)).await??;
+
+  let mentions = scrape_text_for_mentions(&updated_comment.content);
+  let recipient_ids = send_local_notifs(
+    mentions,
+    updated_comment,
+    &user,
+    post,
+    context.pool(),
+    false,
+  )
+  .await?;
+
+  // Refetch the view
+  let comment_view = blocking(context.pool(), move |conn| {
+    CommentView::read(conn, original_comment_id, None)
+  })
+  .await??;
+
+  let res = CommentResponse {
+    comment: comment_view,
+    recipient_ids,
+    form_id: None,
+  };
+
+  context.chat_server().do_send(SendComment {
+    op: UserOperation::EditComment,
+    comment: res,
+    websocket_id: None,
+  });
+
+  announce_if_community_is_local(update, &user, context).await?;
+  Ok(HttpResponse::Ok().finish())
+}
diff --git a/server/src/apub/inbox/community_inbox.rs b/server/src/apub/inbox/community_inbox.rs
new file mode 100644 (file)
index 0000000..929b851
--- /dev/null
@@ -0,0 +1,134 @@
+use crate::{
+  apub::{
+    check_is_apub_id_valid,
+    extensions::signatures::verify,
+    fetcher::get_or_fetch_and_upsert_user,
+    insert_activity,
+    ActorType,
+  },
+  blocking,
+  LemmyContext,
+  LemmyError,
+};
+use activitystreams::{
+  activity::{ActorAndObject, Follow, Undo},
+  base::AnyBase,
+  prelude::*,
+};
+use actix_web::{web, HttpRequest, HttpResponse};
+use anyhow::{anyhow, Context};
+use lemmy_db::{
+  community::{Community, CommunityFollower, CommunityFollowerForm},
+  user::User_,
+  Followable,
+};
+use lemmy_utils::location_info;
+use log::debug;
+use serde::{Deserialize, Serialize};
+use std::fmt::Debug;
+
+#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd, Deserialize, Serialize)]
+#[serde(rename_all = "PascalCase")]
+pub enum ValidTypes {
+  Follow,
+  Undo,
+}
+
+pub type AcceptedActivities = ActorAndObject<ValidTypes>;
+
+/// Handler for all incoming activities to community inboxes.
+pub async fn community_inbox(
+  request: HttpRequest,
+  input: web::Json<AcceptedActivities>,
+  path: web::Path<String>,
+  context: web::Data<LemmyContext>,
+) -> Result<HttpResponse, LemmyError> {
+  let activity = input.into_inner();
+
+  let path = path.into_inner();
+  let community = blocking(&context.pool(), move |conn| {
+    Community::read_from_name(&conn, &path)
+  })
+  .await??;
+
+  if !community.local {
+    return Err(
+      anyhow!(
+        "Received activity is addressed to remote community {}",
+        &community.actor_id
+      )
+      .into(),
+    );
+  }
+  debug!(
+    "Community {} received activity {:?}",
+    &community.name, &activity
+  );
+  let user_uri = activity
+    .actor()?
+    .as_single_xsd_any_uri()
+    .context(location_info!())?;
+  check_is_apub_id_valid(user_uri)?;
+
+  let user = get_or_fetch_and_upsert_user(&user_uri, &context).await?;
+
+  verify(&request, &user)?;
+
+  let any_base = activity.clone().into_any_base()?;
+  let kind = activity.kind().context(location_info!())?;
+  let user_id = user.id;
+  let res = match kind {
+    ValidTypes::Follow => handle_follow(any_base, user, community, &context).await,
+    ValidTypes::Undo => handle_undo_follow(any_base, user, community, &context).await,
+  };
+
+  insert_activity(user_id, activity.clone(), false, context.pool()).await?;
+  res
+}
+
+/// Handle a follow request from a remote user, adding it to the local database and returning an
+/// Accept activity.
+async fn handle_follow(
+  activity: AnyBase,
+  user: User_,
+  community: Community,
+  context: &LemmyContext,
+) -> Result<HttpResponse, LemmyError> {
+  let follow = Follow::from_any_base(activity)?.context(location_info!())?;
+  let community_follower_form = CommunityFollowerForm {
+    community_id: community.id,
+    user_id: user.id,
+  };
+
+  // This will fail if they're already a follower, but ignore the error.
+  blocking(&context.pool(), move |conn| {
+    CommunityFollower::follow(&conn, &community_follower_form).ok()
+  })
+  .await?;
+
+  community.send_accept_follow(follow, context).await?;
+
+  Ok(HttpResponse::Ok().finish())
+}
+
+async fn handle_undo_follow(
+  activity: AnyBase,
+  user: User_,
+  community: Community,
+  context: &LemmyContext,
+) -> Result<HttpResponse, LemmyError> {
+  let _undo = Undo::from_any_base(activity)?.context(location_info!())?;
+
+  let community_follower_form = CommunityFollowerForm {
+    community_id: community.id,
+    user_id: user.id,
+  };
+
+  // This will fail if they aren't a follower, but ignore the error.
+  blocking(&context.pool(), move |conn| {
+    CommunityFollower::unfollow(&conn, &community_follower_form).ok()
+  })
+  .await?;
+
+  Ok(HttpResponse::Ok().finish())
+}
diff --git a/server/src/apub/inbox/mod.rs b/server/src/apub/inbox/mod.rs
new file mode 100644 (file)
index 0000000..b9db61f
--- /dev/null
@@ -0,0 +1,4 @@
+pub mod activities;
+pub mod community_inbox;
+pub mod shared_inbox;
+pub mod user_inbox;
diff --git a/server/src/apub/inbox/shared_inbox.rs b/server/src/apub/inbox/shared_inbox.rs
new file mode 100644 (file)
index 0000000..c9f9324
--- /dev/null
@@ -0,0 +1,166 @@
+use crate::{
+  apub::{
+    check_is_apub_id_valid,
+    community::do_announce,
+    extensions::signatures::verify,
+    fetcher::{
+      get_or_fetch_and_upsert_actor,
+      get_or_fetch_and_upsert_community,
+      get_or_fetch_and_upsert_user,
+    },
+    inbox::activities::{
+      announce::receive_announce,
+      create::receive_create,
+      delete::receive_delete,
+      dislike::receive_dislike,
+      like::receive_like,
+      remove::receive_remove,
+      undo::receive_undo,
+      update::receive_update,
+    },
+    insert_activity,
+  },
+  LemmyContext,
+  LemmyError,
+};
+use activitystreams::{
+  activity::{ActorAndObject, ActorAndObjectRef},
+  base::{AsBase, Extends},
+  object::AsObject,
+  prelude::*,
+};
+use actix_web::{web, HttpRequest, HttpResponse};
+use anyhow::Context;
+use lemmy_db::user::User_;
+use lemmy_utils::location_info;
+use log::debug;
+use serde::{Deserialize, Serialize};
+use std::fmt::Debug;
+use url::Url;
+
+#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd, Deserialize, Serialize)]
+#[serde(rename_all = "PascalCase")]
+pub enum ValidTypes {
+  Create,
+  Update,
+  Like,
+  Dislike,
+  Delete,
+  Undo,
+  Remove,
+  Announce,
+}
+
+// TODO: this isnt entirely correct, cause some of these activities are not ActorAndObject,
+//       but it might still work due to the anybase conversion
+pub type AcceptedActivities = ActorAndObject<ValidTypes>;
+
+/// Handler for all incoming activities to user inboxes.
+pub async fn shared_inbox(
+  request: HttpRequest,
+  input: web::Json<AcceptedActivities>,
+  context: web::Data<LemmyContext>,
+) -> Result<HttpResponse, LemmyError> {
+  let activity = input.into_inner();
+
+  let json = serde_json::to_string(&activity)?;
+  debug!("Shared inbox received activity: {}", json);
+
+  let sender = &activity
+    .actor()?
+    .to_owned()
+    .single_xsd_any_uri()
+    .context(location_info!())?;
+  let community = get_community_id_from_activity(&activity)?;
+
+  check_is_apub_id_valid(sender)?;
+  check_is_apub_id_valid(&community)?;
+
+  let actor = get_or_fetch_and_upsert_actor(sender, &context).await?;
+  verify(&request, actor.as_ref())?;
+
+  let any_base = activity.clone().into_any_base()?;
+  let kind = activity.kind().context(location_info!())?;
+  let res = match kind {
+    ValidTypes::Announce => receive_announce(any_base, &context).await,
+    ValidTypes::Create => receive_create(any_base, &context).await,
+    ValidTypes::Update => receive_update(any_base, &context).await,
+    ValidTypes::Like => receive_like(any_base, &context).await,
+    ValidTypes::Dislike => receive_dislike(any_base, &context).await,
+    ValidTypes::Remove => receive_remove(any_base, &context).await,
+    ValidTypes::Delete => receive_delete(any_base, &context).await,
+    ValidTypes::Undo => receive_undo(any_base, &context).await,
+  };
+
+  insert_activity(actor.user_id(), activity.clone(), false, context.pool()).await?;
+  res
+}
+
+pub(in crate::apub::inbox) fn receive_unhandled_activity<A>(
+  activity: A,
+) -> Result<HttpResponse, LemmyError>
+where
+  A: Debug,
+{
+  debug!("received unhandled activity type: {:?}", activity);
+  Ok(HttpResponse::NotImplemented().finish())
+}
+
+pub(in crate::apub::inbox) async fn get_user_from_activity<T, A>(
+  activity: &T,
+  context: &LemmyContext,
+) -> Result<User_, LemmyError>
+where
+  T: AsBase<A> + ActorAndObjectRef,
+{
+  let actor = activity.actor()?;
+  let user_uri = actor.as_single_xsd_any_uri().context(location_info!())?;
+  get_or_fetch_and_upsert_user(&user_uri, context).await
+}
+
+pub(in crate::apub::inbox) fn get_community_id_from_activity<T, A>(
+  activity: &T,
+) -> Result<Url, LemmyError>
+where
+  T: AsBase<A> + ActorAndObjectRef + AsObject<A>,
+{
+  let cc = activity.cc().context(location_info!())?;
+  let cc = cc.as_many().context(location_info!())?;
+  Ok(
+    cc.first()
+      .context(location_info!())?
+      .as_xsd_any_uri()
+      .context(location_info!())?
+      .to_owned(),
+  )
+}
+
+pub(in crate::apub::inbox) async fn announce_if_community_is_local<T, Kind>(
+  activity: T,
+  user: &User_,
+  context: &LemmyContext,
+) -> Result<(), LemmyError>
+where
+  T: AsObject<Kind>,
+  T: Extends<Kind>,
+  Kind: Serialize,
+  <T as Extends<Kind>>::Error: From<serde_json::Error> + Send + Sync + 'static,
+{
+  let cc = activity.cc().context(location_info!())?;
+  let cc = cc.as_many().context(location_info!())?;
+  let community_followers_uri = cc
+    .first()
+    .context(location_info!())?
+    .as_xsd_any_uri()
+    .context(location_info!())?;
+  // TODO: this is hacky but seems to be the only way to get the community ID
+  let community_uri = community_followers_uri
+    .to_string()
+    .replace("/followers", "");
+  let community = get_or_fetch_and_upsert_community(&Url::parse(&community_uri)?, context).await?;
+
+  if community.local {
+    do_announce(activity.into_any_base()?, &community, &user, context).await?;
+  }
+  Ok(())
+}
diff --git a/server/src/apub/inbox/user_inbox.rs b/server/src/apub/inbox/user_inbox.rs
new file mode 100644 (file)
index 0000000..103fd92
--- /dev/null
@@ -0,0 +1,330 @@
+use crate::{
+  api::user::PrivateMessageResponse,
+  apub::{
+    check_is_apub_id_valid,
+    extensions::signatures::verify,
+    fetcher::{get_or_fetch_and_upsert_actor, get_or_fetch_and_upsert_community},
+    insert_activity,
+    FromApub,
+  },
+  blocking,
+  websocket::{server::SendUserRoomMessage, UserOperation},
+  LemmyContext,
+  LemmyError,
+};
+use activitystreams::{
+  activity::{Accept, ActorAndObject, Create, Delete, Undo, Update},
+  base::AnyBase,
+  object::Note,
+  prelude::*,
+};
+use actix_web::{web, HttpRequest, HttpResponse};
+use anyhow::Context;
+use lemmy_db::{
+  community::{CommunityFollower, CommunityFollowerForm},
+  naive_now,
+  private_message::{PrivateMessage, PrivateMessageForm},
+  private_message_view::PrivateMessageView,
+  user::User_,
+  Crud,
+  Followable,
+};
+use lemmy_utils::location_info;
+use log::debug;
+use serde::{Deserialize, Serialize};
+use std::fmt::Debug;
+
+#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd, Deserialize, Serialize)]
+#[serde(rename_all = "PascalCase")]
+pub enum ValidTypes {
+  Accept,
+  Create,
+  Update,
+  Delete,
+  Undo,
+}
+
+pub type AcceptedActivities = ActorAndObject<ValidTypes>;
+
+/// Handler for all incoming activities to user inboxes.
+pub async fn user_inbox(
+  request: HttpRequest,
+  input: web::Json<AcceptedActivities>,
+  path: web::Path<String>,
+  context: web::Data<LemmyContext>,
+) -> Result<HttpResponse, LemmyError> {
+  let activity = input.into_inner();
+  let username = path.into_inner();
+  debug!("User {} received activity: {:?}", &username, &activity);
+
+  let actor_uri = activity
+    .actor()?
+    .as_single_xsd_any_uri()
+    .context(location_info!())?;
+
+  check_is_apub_id_valid(actor_uri)?;
+
+  let actor = get_or_fetch_and_upsert_actor(actor_uri, &context).await?;
+  verify(&request, actor.as_ref())?;
+
+  let any_base = activity.clone().into_any_base()?;
+  let kind = activity.kind().context(location_info!())?;
+  let res = match kind {
+    ValidTypes::Accept => receive_accept(any_base, username, &context).await,
+    ValidTypes::Create => receive_create_private_message(any_base, &context).await,
+    ValidTypes::Update => receive_update_private_message(any_base, &context).await,
+    ValidTypes::Delete => receive_delete_private_message(any_base, &context).await,
+    ValidTypes::Undo => receive_undo_delete_private_message(any_base, &context).await,
+  };
+
+  insert_activity(actor.user_id(), activity.clone(), false, context.pool()).await?;
+  res
+}
+
+/// Handle accepted follows.
+async fn receive_accept(
+  activity: AnyBase,
+  username: String,
+  context: &LemmyContext,
+) -> Result<HttpResponse, LemmyError> {
+  let accept = Accept::from_any_base(activity)?.context(location_info!())?;
+  let community_uri = accept
+    .actor()?
+    .to_owned()
+    .single_xsd_any_uri()
+    .context(location_info!())?;
+
+  let community = get_or_fetch_and_upsert_community(&community_uri, context).await?;
+
+  let user = blocking(&context.pool(), move |conn| {
+    User_::read_from_name(conn, &username)
+  })
+  .await??;
+
+  // Now you need to add this to the community follower
+  let community_follower_form = CommunityFollowerForm {
+    community_id: community.id,
+    user_id: user.id,
+  };
+
+  // This will fail if they're already a follower
+  blocking(&context.pool(), move |conn| {
+    CommunityFollower::follow(conn, &community_follower_form).ok()
+  })
+  .await?;
+
+  // TODO: make sure that we actually requested a follow
+  Ok(HttpResponse::Ok().finish())
+}
+
+async fn receive_create_private_message(
+  activity: AnyBase,
+  context: &LemmyContext,
+) -> Result<HttpResponse, LemmyError> {
+  let create = Create::from_any_base(activity)?.context(location_info!())?;
+  let note = Note::from_any_base(
+    create
+      .object()
+      .as_one()
+      .context(location_info!())?
+      .to_owned(),
+  )?
+  .context(location_info!())?;
+
+  let domain = Some(create.id_unchecked().context(location_info!())?.to_owned());
+  let private_message = PrivateMessageForm::from_apub(&note, context, domain).await?;
+
+  let inserted_private_message = blocking(&context.pool(), move |conn| {
+    PrivateMessage::create(conn, &private_message)
+  })
+  .await??;
+
+  let message = blocking(&context.pool(), move |conn| {
+    PrivateMessageView::read(conn, inserted_private_message.id)
+  })
+  .await??;
+
+  let res = PrivateMessageResponse { message };
+
+  let recipient_id = res.message.recipient_id;
+
+  context.chat_server().do_send(SendUserRoomMessage {
+    op: UserOperation::CreatePrivateMessage,
+    response: res,
+    recipient_id,
+    websocket_id: None,
+  });
+
+  Ok(HttpResponse::Ok().finish())
+}
+
+async fn receive_update_private_message(
+  activity: AnyBase,
+  context: &LemmyContext,
+) -> Result<HttpResponse, LemmyError> {
+  let update = Update::from_any_base(activity)?.context(location_info!())?;
+  let note = Note::from_any_base(
+    update
+      .object()
+      .as_one()
+      .context(location_info!())?
+      .to_owned(),
+  )?
+  .context(location_info!())?;
+
+  let domain = Some(update.id_unchecked().context(location_info!())?.to_owned());
+  let private_message_form = PrivateMessageForm::from_apub(&note, context, domain).await?;
+
+  let private_message_ap_id = private_message_form.ap_id.clone();
+  let private_message = blocking(&context.pool(), move |conn| {
+    PrivateMessage::read_from_apub_id(conn, &private_message_ap_id)
+  })
+  .await??;
+
+  let private_message_id = private_message.id;
+  blocking(&context.pool(), move |conn| {
+    PrivateMessage::update(conn, private_message_id, &private_message_form)
+  })
+  .await??;
+
+  let private_message_id = private_message.id;
+  let message = blocking(&context.pool(), move |conn| {
+    PrivateMessageView::read(conn, private_message_id)
+  })
+  .await??;
+
+  let res = PrivateMessageResponse { message };
+
+  let recipient_id = res.message.recipient_id;
+
+  context.chat_server().do_send(SendUserRoomMessage {
+    op: UserOperation::EditPrivateMessage,
+    response: res,
+    recipient_id,
+    websocket_id: None,
+  });
+
+  Ok(HttpResponse::Ok().finish())
+}
+
+async fn receive_delete_private_message(
+  activity: AnyBase,
+  context: &LemmyContext,
+) -> Result<HttpResponse, LemmyError> {
+  let delete = Delete::from_any_base(activity)?.context(location_info!())?;
+  let note = Note::from_any_base(
+    delete
+      .object()
+      .as_one()
+      .context(location_info!())?
+      .to_owned(),
+  )?
+  .context(location_info!())?;
+
+  let domain = Some(delete.id_unchecked().context(location_info!())?.to_owned());
+  let private_message_form = PrivateMessageForm::from_apub(&note, context, domain).await?;
+
+  let private_message_ap_id = private_message_form.ap_id;
+  let private_message = blocking(&context.pool(), move |conn| {
+    PrivateMessage::read_from_apub_id(conn, &private_message_ap_id)
+  })
+  .await??;
+
+  let private_message_form = PrivateMessageForm {
+    content: private_message_form.content,
+    recipient_id: private_message.recipient_id,
+    creator_id: private_message.creator_id,
+    deleted: Some(true),
+    read: None,
+    ap_id: private_message.ap_id,
+    local: private_message.local,
+    published: None,
+    updated: Some(naive_now()),
+  };
+
+  let private_message_id = private_message.id;
+  blocking(&context.pool(), move |conn| {
+    PrivateMessage::update(conn, private_message_id, &private_message_form)
+  })
+  .await??;
+
+  let private_message_id = private_message.id;
+  let message = blocking(&context.pool(), move |conn| {
+    PrivateMessageView::read(&conn, private_message_id)
+  })
+  .await??;
+
+  let res = PrivateMessageResponse { message };
+
+  let recipient_id = res.message.recipient_id;
+
+  context.chat_server().do_send(SendUserRoomMessage {
+    op: UserOperation::EditPrivateMessage,
+    response: res,
+    recipient_id,
+    websocket_id: None,
+  });
+
+  Ok(HttpResponse::Ok().finish())
+}
+
+async fn receive_undo_delete_private_message(
+  activity: AnyBase,
+  context: &LemmyContext,
+) -> Result<HttpResponse, LemmyError> {
+  let undo = Undo::from_any_base(activity)?.context(location_info!())?;
+  let delete = Delete::from_any_base(undo.object().as_one().context(location_info!())?.to_owned())?
+    .context(location_info!())?;
+  let note = Note::from_any_base(
+    delete
+      .object()
+      .as_one()
+      .context(location_info!())?
+      .to_owned(),
+  )?
+  .context(location_info!())?;
+
+  let domain = Some(undo.id_unchecked().context(location_info!())?.to_owned());
+  let private_message = PrivateMessageForm::from_apub(&note, context, domain).await?;
+
+  let private_message_ap_id = private_message.ap_id.clone();
+  let private_message_id = blocking(&context.pool(), move |conn| {
+    PrivateMessage::read_from_apub_id(conn, &private_message_ap_id).map(|pm| pm.id)
+  })
+  .await??;
+
+  let private_message_form = PrivateMessageForm {
+    content: private_message.content,
+    recipient_id: private_message.recipient_id,
+    creator_id: private_message.creator_id,
+    deleted: Some(false),
+    read: None,
+    ap_id: private_message.ap_id,
+    local: private_message.local,
+    published: None,
+    updated: Some(naive_now()),
+  };
+
+  blocking(&context.pool(), move |conn| {
+    PrivateMessage::update(conn, private_message_id, &private_message_form)
+  })
+  .await??;
+
+  let message = blocking(&context.pool(), move |conn| {
+    PrivateMessageView::read(&conn, private_message_id)
+  })
+  .await??;
+
+  let res = PrivateMessageResponse { message };
+
+  let recipient_id = res.message.recipient_id;
+
+  context.chat_server().do_send(SendUserRoomMessage {
+    op: UserOperation::EditPrivateMessage,
+    response: res,
+    recipient_id,
+    websocket_id: None,
+  });
+
+  Ok(HttpResponse::Ok().finish())
+}
index cfb539fb169756241016ef955a0be18b8909c24f..dddbd7e04d64eaa3f0930ec6c0450ff3158f7a15 100644 (file)
@@ -1,14 +1,12 @@
 pub mod activities;
 pub mod comment;
 pub mod community;
-pub mod community_inbox;
 pub mod extensions;
 pub mod fetcher;
+pub mod inbox;
 pub mod post;
 pub mod private_message;
-pub mod shared_inbox;
 pub mod user;
-pub mod user_inbox;
 
 use crate::{
   apub::extensions::{
@@ -20,24 +18,32 @@ use crate::{
   request::{retry, RecvError},
   routes::webfinger::WebFingerResponse,
   DbPool,
+  LemmyContext,
   LemmyError,
 };
-use activitystreams::object::Page;
-use activitystreams_ext::{Ext1, Ext2};
-use activitystreams_new::{
+use activitystreams::{
   activity::Follow,
   actor::{ApActor, Group, Person},
-  object::Tombstone,
+  base::AsBase,
+  markers::Base,
+  object::{Page, Tombstone},
   prelude::*,
 };
+use activitystreams_ext::{Ext1, Ext2};
 use actix_web::{body::Body, client::Client, HttpResponse};
+use anyhow::{anyhow, Context};
 use chrono::NaiveDateTime;
-use failure::_core::fmt::Debug;
 use lemmy_db::{activity::do_insert_activity, user::User_};
-use lemmy_utils::{convert_datetime, get_apub_protocol_string, settings::Settings, MentionData};
+use lemmy_utils::{
+  convert_datetime,
+  get_apub_protocol_string,
+  location_info,
+  settings::Settings,
+  MentionData,
+};
 use log::debug;
 use serde::Serialize;
-use url::Url;
+use url::{ParseError, Url};
 
 type GroupExt = Ext2<ApActor<Group>, GroupExtension, PublicKeyExtension>;
 type PersonExt = Ext1<ApActor<Person>, PublicKeyExtension>;
@@ -66,33 +72,56 @@ where
 }
 
 // Checks if the ID has a valid format, correct scheme, and is in the allowed instance list.
-fn is_apub_id_valid(apub_id: &Url) -> bool {
-  debug!("Checking {}", apub_id);
+fn check_is_apub_id_valid(apub_id: &Url) -> Result<(), LemmyError> {
+  let settings = Settings::get();
+  let domain = apub_id.domain().context(location_info!())?.to_string();
+  let local_instance = settings
+    .hostname
+    .split(':')
+    .collect::<Vec<&str>>()
+    .first()
+    .context(location_info!())?
+    .to_string();
+
+  if !settings.federation.enabled {
+    return if domain == local_instance {
+      Ok(())
+    } else {
+      Err(
+        anyhow!(
+          "Trying to connect with {}, but federation is disabled",
+          domain
+        )
+        .into(),
+      )
+    };
+  }
+
   if apub_id.scheme() != get_apub_protocol_string() {
-    debug!("invalid scheme: {:?}", apub_id.scheme());
-    return false;
+    return Err(anyhow!("invalid apub id scheme: {:?}", apub_id.scheme()).into());
   }
 
-  let allowed_instances: Vec<String> = Settings::get()
-    .federation
-    .allowed_instances
-    .split(',')
-    .map(|d| d.to_string())
-    .collect();
-  match apub_id.domain() {
-    Some(d) => {
-      let contains = allowed_instances.contains(&d.to_owned());
-
-      if !contains {
-        debug!("{} not in {:?}", d, allowed_instances);
-      }
+  let mut allowed_instances = Settings::get().get_allowed_instances();
+  let blocked_instances = Settings::get().get_blocked_instances();
 
-      contains
+  if !allowed_instances.is_empty() {
+    // need to allow this explicitly because apub activities might contain objects from our local
+    // instance. split is needed to remove the port in our federation test setup.
+    allowed_instances.push(local_instance);
+
+    if allowed_instances.contains(&domain) {
+      Ok(())
+    } else {
+      Err(anyhow!("{} not in federation allowlist", domain).into())
     }
-    None => {
-      debug!("missing domain");
-      false
+  } else if !blocked_instances.is_empty() {
+    if blocked_instances.contains(&domain) {
+      Err(anyhow!("{} is in federation blocklist", domain).into())
+    } else {
+      Ok(())
     }
+  } else {
+    panic!("Invalid config, both allowed_instances and blocked_instances are specified");
   }
 }
 
@@ -104,34 +133,44 @@ pub trait ToApub {
 }
 
 /// Updated is actually the deletion time
-fn create_tombstone(
+fn create_tombstone<T>(
   deleted: bool,
   object_id: &str,
   updated: Option<NaiveDateTime>,
-  former_type: String,
-) -> Result<Tombstone, LemmyError> {
+  former_type: T,
+) -> Result<Tombstone, LemmyError>
+where
+  T: ToString,
+{
   if deleted {
     if let Some(updated) = updated {
       let mut tombstone = Tombstone::new();
       tombstone.set_id(object_id.parse()?);
-      tombstone.set_former_type(former_type);
-      tombstone.set_deleted(convert_datetime(updated).into());
+      tombstone.set_former_type(former_type.to_string());
+      tombstone.set_deleted(convert_datetime(updated));
       Ok(tombstone)
     } else {
-      Err(format_err!("Cant convert to tombstone because updated time was None.").into())
+      Err(anyhow!("Cant convert to tombstone because updated time was None.").into())
     }
   } else {
-    Err(format_err!("Cant convert object to tombstone if it wasnt deleted").into())
+    Err(anyhow!("Cant convert object to tombstone if it wasnt deleted").into())
   }
 }
 
 #[async_trait::async_trait(?Send)]
 pub trait FromApub {
   type ApubType;
+  /// Converts an object from ActivityPub type to Lemmy internal type.
+  ///
+  /// * `apub` The object to read from
+  /// * `context` LemmyContext which holds DB pool, HTTP client etc
+  /// * `expected_domain` If present, ensure that the apub object comes from the same domain as
+  ///                     this URL
+  ///
   async fn from_apub(
     apub: &Self::ApubType,
-    client: &Client,
-    pool: &DbPool,
+    context: &LemmyContext,
+    expected_domain: Option<Url>,
   ) -> Result<Self, LemmyError>
   where
     Self: Sized;
@@ -139,179 +178,145 @@ pub trait FromApub {
 
 #[async_trait::async_trait(?Send)]
 pub trait ApubObjectType {
-  async fn send_create(
-    &self,
-    creator: &User_,
-    client: &Client,
-    pool: &DbPool,
-  ) -> Result<(), LemmyError>;
-  async fn send_update(
-    &self,
-    creator: &User_,
-    client: &Client,
-    pool: &DbPool,
-  ) -> Result<(), LemmyError>;
-  async fn send_delete(
-    &self,
-    creator: &User_,
-    client: &Client,
-    pool: &DbPool,
-  ) -> Result<(), LemmyError>;
+  async fn send_create(&self, creator: &User_, context: &LemmyContext) -> Result<(), LemmyError>;
+  async fn send_update(&self, creator: &User_, context: &LemmyContext) -> Result<(), LemmyError>;
+  async fn send_delete(&self, creator: &User_, context: &LemmyContext) -> Result<(), LemmyError>;
   async fn send_undo_delete(
     &self,
     creator: &User_,
-    client: &Client,
-    pool: &DbPool,
-  ) -> Result<(), LemmyError>;
-  async fn send_remove(
-    &self,
-    mod_: &User_,
-    client: &Client,
-    pool: &DbPool,
-  ) -> Result<(), LemmyError>;
-  async fn send_undo_remove(
-    &self,
-    mod_: &User_,
-    client: &Client,
-    pool: &DbPool,
+    context: &LemmyContext,
   ) -> Result<(), LemmyError>;
+  async fn send_remove(&self, mod_: &User_, context: &LemmyContext) -> Result<(), LemmyError>;
+  async fn send_undo_remove(&self, mod_: &User_, context: &LemmyContext) -> Result<(), LemmyError>;
 }
 
-#[async_trait::async_trait(?Send)]
-pub trait ApubLikeableType {
-  async fn send_like(
-    &self,
-    creator: &User_,
-    client: &Client,
-    pool: &DbPool,
-  ) -> Result<(), LemmyError>;
-  async fn send_dislike(
-    &self,
-    creator: &User_,
-    client: &Client,
-    pool: &DbPool,
-  ) -> Result<(), LemmyError>;
-  async fn send_undo_like(
-    &self,
-    creator: &User_,
-    client: &Client,
-    pool: &DbPool,
-  ) -> Result<(), LemmyError>;
+pub(in crate::apub) fn check_actor_domain<T, Kind>(
+  apub: &T,
+  expected_domain: Option<Url>,
+) -> Result<String, LemmyError>
+where
+  T: Base + AsBase<Kind>,
+{
+  let actor_id = if let Some(url) = expected_domain {
+    let domain = url.domain().context(location_info!())?;
+    apub.id(domain)?.context(location_info!())?
+  } else {
+    let actor_id = apub.id_unchecked().context(location_info!())?;
+    check_is_apub_id_valid(&actor_id)?;
+    actor_id
+  };
+  Ok(actor_id.to_string())
 }
 
-pub fn get_shared_inbox(actor_id: &str) -> String {
-  let url = Url::parse(actor_id).unwrap();
-  format!(
-    "{}://{}{}/inbox",
-    &url.scheme(),
-    &url.host_str().unwrap(),
-    if let Some(port) = url.port() {
-      format!(":{}", port)
-    } else {
-      "".to_string()
-    },
-  )
+#[async_trait::async_trait(?Send)]
+pub trait ApubLikeableType {
+  async fn send_like(&self, creator: &User_, context: &LemmyContext) -> Result<(), LemmyError>;
+  async fn send_dislike(&self, creator: &User_, context: &LemmyContext) -> Result<(), LemmyError>;
+  async fn send_undo_like(&self, creator: &User_, context: &LemmyContext)
+    -> Result<(), LemmyError>;
 }
 
 #[async_trait::async_trait(?Send)]
 pub trait ActorType {
-  fn actor_id(&self) -> String;
+  fn actor_id_str(&self) -> String;
+
+  // TODO: every actor should have a public key, so this shouldnt be an option (needs to be fixed in db)
+  fn public_key(&self) -> Option<String>;
+  fn private_key(&self) -> Option<String>;
 
-  fn public_key(&self) -> String;
-  fn private_key(&self) -> String;
+  /// numeric id in the database, used for insert_activity
+  fn user_id(&self) -> i32;
 
   // These two have default impls, since currently a community can't follow anything,
   // and a user can't be followed (yet)
   #[allow(unused_variables)]
   async fn send_follow(
     &self,
-    follow_actor_id: &str,
-    client: &Client,
-    pool: &DbPool,
+    follow_actor_id: &Url,
+    context: &LemmyContext,
   ) -> Result<(), LemmyError>;
   async fn send_unfollow(
     &self,
-    follow_actor_id: &str,
-    client: &Client,
-    pool: &DbPool,
+    follow_actor_id: &Url,
+    context: &LemmyContext,
   ) -> Result<(), LemmyError>;
 
   #[allow(unused_variables)]
   async fn send_accept_follow(
     &self,
-    follow: &Follow,
-    client: &Client,
-    pool: &DbPool,
+    follow: Follow,
+    context: &LemmyContext,
   ) -> Result<(), LemmyError>;
 
-  async fn send_delete(
-    &self,
-    creator: &User_,
-    client: &Client,
-    pool: &DbPool,
-  ) -> Result<(), LemmyError>;
+  async fn send_delete(&self, creator: &User_, context: &LemmyContext) -> Result<(), LemmyError>;
   async fn send_undo_delete(
     &self,
     creator: &User_,
-    client: &Client,
-    pool: &DbPool,
+    context: &LemmyContext,
   ) -> Result<(), LemmyError>;
 
-  async fn send_remove(
-    &self,
-    mod_: &User_,
-    client: &Client,
-    pool: &DbPool,
-  ) -> Result<(), LemmyError>;
-  async fn send_undo_remove(
-    &self,
-    mod_: &User_,
-    client: &Client,
-    pool: &DbPool,
-  ) -> Result<(), LemmyError>;
+  async fn send_remove(&self, mod_: &User_, context: &LemmyContext) -> Result<(), LemmyError>;
+  async fn send_undo_remove(&self, mod_: &User_, context: &LemmyContext) -> Result<(), LemmyError>;
 
   /// For a given community, returns the inboxes of all followers.
-  async fn get_follower_inboxes(&self, pool: &DbPool) -> Result<Vec<String>, LemmyError>;
+  async fn get_follower_inboxes(&self, pool: &DbPool) -> Result<Vec<Url>, LemmyError>;
+
+  fn actor_id(&self) -> Result<Url, ParseError> {
+    Url::parse(&self.actor_id_str())
+  }
 
   // TODO move these to the db rows
-  fn get_inbox_url(&self) -> String {
-    format!("{}/inbox", &self.actor_id())
+  fn get_inbox_url(&self) -> Result<Url, ParseError> {
+    Url::parse(&format!("{}/inbox", &self.actor_id_str()))
   }
 
-  fn get_shared_inbox_url(&self) -> String {
-    get_shared_inbox(&self.actor_id())
+  fn get_shared_inbox_url(&self) -> Result<Url, LemmyError> {
+    let actor_id = self.actor_id()?;
+    let url = format!(
+      "{}://{}{}/inbox",
+      &actor_id.scheme(),
+      &actor_id.host_str().context(location_info!())?,
+      if let Some(port) = actor_id.port() {
+        format!(":{}", port)
+      } else {
+        "".to_string()
+      },
+    );
+    Ok(Url::parse(&url)?)
   }
 
-  fn get_outbox_url(&self) -> String {
-    format!("{}/outbox", &self.actor_id())
+  fn get_outbox_url(&self) -> Result<Url, ParseError> {
+    Url::parse(&format!("{}/outbox", &self.actor_id_str()))
   }
 
-  fn get_followers_url(&self) -> String {
-    format!("{}/followers", &self.actor_id())
+  fn get_followers_url(&self) -> Result<Url, ParseError> {
+    Url::parse(&format!("{}/followers", &self.actor_id_str()))
   }
 
   fn get_following_url(&self) -> String {
-    format!("{}/following", &self.actor_id())
+    format!("{}/following", &self.actor_id_str())
   }
 
   fn get_liked_url(&self) -> String {
-    format!("{}/liked", &self.actor_id())
+    format!("{}/liked", &self.actor_id_str())
   }
 
-  fn get_public_key_ext(&self) -> PublicKeyExtension {
-    PublicKey {
-      id: format!("{}#main-key", self.actor_id()),
-      owner: self.actor_id(),
-      public_key_pem: self.public_key(),
-    }
-    .to_ext()
+  fn get_public_key_ext(&self) -> Result<PublicKeyExtension, LemmyError> {
+    Ok(
+      PublicKey {
+        id: format!("{}#main-key", self.actor_id_str()),
+        owner: self.actor_id_str(),
+        public_key_pem: self.public_key().context(location_info!())?,
+      }
+      .to_ext(),
+    )
   }
 }
 
 pub async fn fetch_webfinger_url(
   mention: &MentionData,
   client: &Client,
-) -> Result<String, LemmyError> {
+) -> Result<Url, LemmyError> {
   let fetch_url = format!(
     "{}://{}/.well-known/webfinger?resource=acct:{}@{}",
     get_apub_protocol_string(),
@@ -332,11 +337,13 @@ pub async fn fetch_webfinger_url(
     .links
     .iter()
     .find(|l| l.type_.eq(&Some("application/activity+json".to_string())))
-    .ok_or_else(|| format_err!("No application/activity+json link found."))?;
+    .ok_or_else(|| anyhow!("No application/activity+json link found."))?;
   link
     .href
     .to_owned()
-    .ok_or_else(|| format_err!("No href found.").into())
+    .map(|u| Url::parse(&u))
+    .transpose()?
+    .ok_or_else(|| anyhow!("No href found.").into())
 }
 
 pub async fn insert_activity<T>(
@@ -346,7 +353,7 @@ pub async fn insert_activity<T>(
   pool: &DbPool,
 ) -> Result<(), LemmyError>
 where
-  T: Serialize + Debug + Send + 'static,
+  T: Serialize + std::fmt::Debug + Send + 'static,
 {
   blocking(pool, move |conn| {
     do_insert_activity(conn, user_id, &data, local)
index 36922e4f0cc30592159a5a3620f9dc595c23f597..4f2a831552a25e06f0ea3b6c8ba3a7f2dd2df263 100644 (file)
@@ -1,11 +1,13 @@
 use crate::{
+  api::check_slurs,
   apub::{
-    activities::{populate_object_props, send_activity_to_community},
+    activities::{generate_activity_id, send_activity_to_community},
+    check_actor_domain,
     create_apub_response,
     create_apub_tombstone_response,
     create_tombstone,
     extensions::page_extension::PageExtension,
-    fetcher::{get_or_fetch_and_upsert_remote_community, get_or_fetch_and_upsert_remote_user},
+    fetcher::{get_or_fetch_and_upsert_community, get_or_fetch_and_upsert_user},
     ActorType,
     ApubLikeableType,
     ApubObjectType,
@@ -14,27 +16,37 @@ use crate::{
     ToApub,
   },
   blocking,
-  routes::DbPoolParam,
   DbPool,
+  LemmyContext,
   LemmyError,
 };
 use activitystreams::{
-  activity::{Create, Delete, Dislike, Like, Remove, Undo, Update},
-  context,
-  object::{kind::PageType, properties::ObjectProperties, AnyImage, Image, Page},
-  BaseBox,
+  activity::{
+    kind::{CreateType, DeleteType, DislikeType, LikeType, RemoveType, UndoType, UpdateType},
+    Create,
+    Delete,
+    Dislike,
+    Like,
+    Remove,
+    Undo,
+    Update,
+  },
+  object::{kind::PageType, Image, Object, Page, Tombstone},
+  prelude::*,
+  public,
 };
 use activitystreams_ext::Ext1;
-use activitystreams_new::object::Tombstone;
-use actix_web::{body::Body, client::Client, web, HttpResponse};
+use actix_web::{body::Body, web, HttpResponse};
+use anyhow::Context;
 use lemmy_db::{
   community::Community,
   post::{Post, PostForm},
   user::User_,
   Crud,
 };
-use lemmy_utils::{convert_datetime, get_apub_protocol_string, settings::Settings};
+use lemmy_utils::{convert_datetime, location_info, remove_slurs};
 use serde::Deserialize;
+use url::Url;
 
 #[derive(Deserialize)]
 pub struct PostQuery {
@@ -44,13 +56,13 @@ pub struct PostQuery {
 /// Return the post json over HTTP.
 pub async fn get_apub_post(
   info: web::Path<PostQuery>,
-  db: DbPoolParam,
+  context: web::Data<LemmyContext>,
 ) -> Result<HttpResponse<Body>, LemmyError> {
   let id = info.post_id.parse::<i32>()?;
-  let post = blocking(&db, move |conn| Post::read(conn, id)).await??;
+  let post = blocking(context.pool(), move |conn| Post::read(conn, id)).await??;
 
   if !post.deleted {
-    Ok(create_apub_response(&post.to_apub(&db).await?))
+    Ok(create_apub_response(&post.to_apub(context.pool()).await?))
   } else {
     Ok(create_apub_tombstone_response(&post.to_tombstone()?))
   }
@@ -62,8 +74,7 @@ impl ToApub for Post {
 
   // Turn a Lemmy post into an ActivityPub page that can be sent out over the network.
   async fn to_apub(&self, pool: &DbPool) -> Result<PageExt, LemmyError> {
-    let mut page = Page::default();
-    let oprops: &mut ObjectProperties = page.as_mut();
+    let mut page = Page::new();
 
     let creator_id = self.creator_id;
     let creator = blocking(pool, move |conn| User_::read(conn, creator_id)).await??;
@@ -71,88 +82,112 @@ impl ToApub for Post {
     let community_id = self.community_id;
     let community = blocking(pool, move |conn| Community::read(conn, community_id)).await??;
 
-    oprops
+    page
       // Not needed when the Post is embedded in a collection (like for community outbox)
       // TODO: need to set proper context defining sensitive/commentsEnabled fields
       // https://git.asonix.dog/Aardwolf/activitystreams/issues/5
-      .set_context_xsd_any_uri(context())?
-      .set_id(self.ap_id.to_owned())?
+      .set_context(activitystreams::context())
+      .set_id(self.ap_id.parse::<Url>()?)
       // Use summary field to be consistent with mastodon content warning.
       // https://mastodon.xyz/@Louisa/103987265222901387.json
-      .set_summary_xsd_string(self.name.to_owned())?
-      .set_published(convert_datetime(self.published))?
-      .set_to_xsd_any_uri(community.actor_id)?
-      .set_attributed_to_xsd_any_uri(creator.actor_id)?;
+      .set_summary(self.name.to_owned())
+      .set_published(convert_datetime(self.published))
+      .set_to(community.actor_id)
+      .set_attributed_to(creator.actor_id);
 
     if let Some(body) = &self.body {
-      oprops.set_content_xsd_string(body.to_owned())?;
+      page.set_content(body.to_owned());
     }
 
     // TODO: hacky code because we get self.url == Some("")
     // https://github.com/LemmyNet/lemmy/issues/602
     let url = self.url.as_ref().filter(|u| !u.is_empty());
     if let Some(u) = url {
-      oprops.set_url_xsd_any_uri(u.to_owned())?;
+      page.set_url(u.to_owned());
 
       // Embeds
       let mut page_preview = Page::new();
-      page_preview
-        .object_props
-        .set_url_xsd_any_uri(u.to_owned())?;
+      page_preview.set_url(u.to_owned());
 
       if let Some(embed_title) = &self.embed_title {
-        page_preview
-          .object_props
-          .set_name_xsd_string(embed_title.to_owned())?;
+        page_preview.set_name(embed_title.to_owned());
       }
 
       if let Some(embed_description) = &self.embed_description {
-        page_preview
-          .object_props
-          .set_summary_xsd_string(embed_description.to_owned())?;
+        page_preview.set_summary(embed_description.to_owned());
       }
 
       if let Some(embed_html) = &self.embed_html {
-        page_preview
-          .object_props
-          .set_content_xsd_string(embed_html.to_owned())?;
+        page_preview.set_content(embed_html.to_owned());
       }
 
-      oprops.set_preview_base_box(page_preview)?;
+      page.set_preview(page_preview.into_any_base()?);
     }
 
     if let Some(thumbnail_url) = &self.thumbnail_url {
-      let full_url = format!(
-        "{}://{}/pictshare/{}",
-        get_apub_protocol_string(),
-        Settings::get().hostname,
-        thumbnail_url
-      );
-
       let mut image = Image::new();
-      image.object_props.set_url_xsd_any_uri(full_url)?;
-      let any_image = AnyImage::from_concrete(image)?;
-      oprops.set_image_any_image(any_image)?;
+      image.set_url(thumbnail_url.to_string());
+      page.set_image(image.into_any_base()?);
     }
 
     if let Some(u) = self.updated {
-      oprops.set_updated(convert_datetime(u))?;
+      page.set_updated(convert_datetime(u));
     }
 
     let ext = PageExtension {
       comments_enabled: !self.locked,
       sensitive: self.nsfw,
+      stickied: self.stickied,
     };
     Ok(Ext1::new(page, ext))
   }
 
   fn to_tombstone(&self) -> Result<Tombstone, LemmyError> {
-    create_tombstone(
-      self.deleted,
-      &self.ap_id,
-      self.updated,
-      PageType.to_string(),
-    )
+    create_tombstone(self.deleted, &self.ap_id, self.updated, PageType::Page)
+  }
+}
+
+struct EmbedType {
+  title: Option<String>,
+  description: Option<String>,
+  html: Option<String>,
+}
+
+fn extract_embed_from_apub(
+  page: &Ext1<Object<PageType>, PageExtension>,
+) -> Result<EmbedType, LemmyError> {
+  match page.inner.preview() {
+    Some(preview) => {
+      let preview_page = Page::from_any_base(preview.one().context(location_info!())?.to_owned())?
+        .context(location_info!())?;
+      let title = preview_page
+        .name()
+        .map(|n| n.one())
+        .flatten()
+        .map(|s| s.as_xsd_string())
+        .flatten()
+        .map(|s| s.to_string());
+      let description = preview_page
+        .summary()
+        .map(|s| s.as_single_xsd_string())
+        .flatten()
+        .map(|s| s.to_string());
+      let html = preview_page
+        .content()
+        .map(|c| c.as_single_xsd_string())
+        .flatten()
+        .map(|s| s.to_string());
+      Ok(EmbedType {
+        title,
+        description,
+        html,
+      })
+    }
+    None => Ok(EmbedType {
+      title: None,
+      description: None,
+      html: None,
+    }),
   }
 }
 
@@ -163,73 +198,98 @@ impl FromApub for PostForm {
   /// Parse an ActivityPub page received from another instance into a Lemmy post.
   async fn from_apub(
     page: &PageExt,
-    client: &Client,
-    pool: &DbPool,
+    context: &LemmyContext,
+    expected_domain: Option<Url>,
   ) -> Result<PostForm, LemmyError> {
     let ext = &page.ext_one;
-    let oprops = &page.inner.object_props;
-    let creator_actor_id = &oprops.get_attributed_to_xsd_any_uri().unwrap().to_string();
-
-    let creator = get_or_fetch_and_upsert_remote_user(&creator_actor_id, client, pool).await?;
-
-    let community_actor_id = &oprops.get_to_xsd_any_uri().unwrap().to_string();
-
-    let community =
-      get_or_fetch_and_upsert_remote_community(&community_actor_id, client, pool).await?;
-
-    let thumbnail_url = match oprops.get_image_any_image() {
-      Some(any_image) => any_image
-        .to_owned()
-        .into_concrete::<Image>()?
-        .object_props
-        .get_url_xsd_any_uri()
-        .map(|u| u.to_string()),
+    let creator_actor_id = page
+      .inner
+      .attributed_to()
+      .as_ref()
+      .context(location_info!())?
+      .as_single_xsd_any_uri()
+      .context(location_info!())?;
+
+    let creator = get_or_fetch_and_upsert_user(creator_actor_id, context).await?;
+
+    let community_actor_id = page
+      .inner
+      .to()
+      .as_ref()
+      .context(location_info!())?
+      .as_single_xsd_any_uri()
+      .context(location_info!())?;
+
+    let community = get_or_fetch_and_upsert_community(community_actor_id, context).await?;
+
+    let thumbnail_url = match &page.inner.image() {
+      Some(any_image) => Image::from_any_base(
+        any_image
+          .to_owned()
+          .as_one()
+          .context(location_info!())?
+          .to_owned(),
+      )?
+      .context(location_info!())?
+      .url()
+      .context(location_info!())?
+      .as_single_xsd_any_uri()
+      .map(|u| u.to_string()),
       None => None,
     };
 
-    let url = oprops.get_url_xsd_any_uri().map(|u| u.to_string());
-    let (embed_title, embed_description, embed_html) = match oprops.get_preview_base_box() {
-      Some(preview) => {
-        let preview_page = preview.to_owned().into_concrete::<Page>()?;
-        let name = preview_page
-          .object_props
-          .get_name_xsd_string()
-          .map(|n| n.to_string());
-        let summary = preview_page
-          .object_props
-          .get_summary_xsd_string()
-          .map(|s| s.to_string());
-        let content = preview_page
-          .object_props
-          .get_content_xsd_string()
-          .map(|c| c.to_string());
-        (name, summary, content)
-      }
-      None => (None, None, None),
-    };
-
+    let embed = extract_embed_from_apub(page)?;
+
+    let name = page
+      .inner
+      .summary()
+      .as_ref()
+      .context(location_info!())?
+      .as_single_xsd_string()
+      .context(location_info!())?
+      .to_string();
+    let url = page
+      .inner
+      .url()
+      .as_ref()
+      .map(|u| u.as_single_xsd_string())
+      .flatten()
+      .map(|s| s.to_string());
+    let body = page
+      .inner
+      .content()
+      .as_ref()
+      .map(|c| c.as_single_xsd_string())
+      .flatten()
+      .map(|s| s.to_string());
+    check_slurs(&name)?;
+    let body_slurs_removed = body.map(|b| remove_slurs(&b));
     Ok(PostForm {
-      name: oprops.get_summary_xsd_string().unwrap().to_string(),
+      name,
       url,
-      body: oprops.get_content_xsd_string().map(|c| c.to_string()),
+      body: body_slurs_removed,
       creator_id: creator.id,
       community_id: community.id,
       removed: None,
       locked: Some(!ext.comments_enabled),
-      published: oprops
-        .get_published()
-        .map(|u| u.as_ref().to_owned().naive_local()),
-      updated: oprops
-        .get_updated()
-        .map(|u| u.as_ref().to_owned().naive_local()),
+      published: page
+        .inner
+        .published()
+        .as_ref()
+        .map(|u| u.to_owned().naive_local()),
+      updated: page
+        .inner
+        .updated()
+        .as_ref()
+        .map(|u| u.to_owned().naive_local()),
       deleted: None,
       nsfw: ext.sensitive,
-      stickied: None, // -> put it in "featured" collection of the community
-      embed_title,
-      embed_description,
-      embed_html,
+      stickied: Some(ext.stickied),
+      embed_title: embed.title,
+      embed_description: embed.description,
+      embed_html: embed.html,
       thumbnail_url,
-      ap_id: oprops.get_id().unwrap().to_string(),
+      ap_id: check_actor_domain(page, expected_domain)?,
       local: false,
     })
   }
@@ -238,111 +298,83 @@ impl FromApub for PostForm {
 #[async_trait::async_trait(?Send)]
 impl ApubObjectType for Post {
   /// Send out information about a newly created post, to the followers of the community.
-  async fn send_create(
-    &self,
-    creator: &User_,
-    client: &Client,
-    pool: &DbPool,
-  ) -> Result<(), LemmyError> {
-    let page = self.to_apub(pool).await?;
+  async fn send_create(&self, creator: &User_, context: &LemmyContext) -> Result<(), LemmyError> {
+    let page = self.to_apub(context.pool()).await?;
 
     let community_id = self.community_id;
-    let community = blocking(pool, move |conn| Community::read(conn, community_id)).await??;
-
-    let id = format!("{}/create/{}", self.ap_id, uuid::Uuid::new_v4());
+    let community = blocking(context.pool(), move |conn| {
+      Community::read(conn, community_id)
+    })
+    .await??;
 
-    let mut create = Create::new();
-    populate_object_props(
-      &mut create.object_props,
-      vec![community.get_followers_url()],
-      &id,
-    )?;
+    let mut create = Create::new(creator.actor_id.to_owned(), page.into_any_base()?);
     create
-      .create_props
-      .set_actor_xsd_any_uri(creator.actor_id.to_owned())?
-      .set_object_base_box(BaseBox::from_concrete(page)?)?;
+      .set_context(activitystreams::context())
+      .set_id(generate_activity_id(CreateType::Create)?)
+      .set_to(public())
+      .set_many_ccs(vec![community.get_followers_url()?]);
 
     send_activity_to_community(
       creator,
       &community,
-      vec![community.get_shared_inbox_url()],
-      create,
-      client,
-      pool,
+      vec![community.get_shared_inbox_url()?],
+      create.into_any_base()?,
+      context,
     )
     .await?;
     Ok(())
   }
 
   /// Send out information about an edited post, to the followers of the community.
-  async fn send_update(
-    &self,
-    creator: &User_,
-    client: &Client,
-    pool: &DbPool,
-  ) -> Result<(), LemmyError> {
-    let page = self.to_apub(pool).await?;
+  async fn send_update(&self, creator: &User_, context: &LemmyContext) -> Result<(), LemmyError> {
+    let page = self.to_apub(context.pool()).await?;
 
     let community_id = self.community_id;
-    let community = blocking(pool, move |conn| Community::read(conn, community_id)).await??;
-
-    let id = format!("{}/update/{}", self.ap_id, uuid::Uuid::new_v4());
+    let community = blocking(context.pool(), move |conn| {
+      Community::read(conn, community_id)
+    })
+    .await??;
 
-    let mut update = Update::new();
-    populate_object_props(
-      &mut update.object_props,
-      vec![community.get_followers_url()],
-      &id,
-    )?;
+    let mut update = Update::new(creator.actor_id.to_owned(), page.into_any_base()?);
     update
-      .update_props
-      .set_actor_xsd_any_uri(creator.actor_id.to_owned())?
-      .set_object_base_box(BaseBox::from_concrete(page)?)?;
+      .set_context(activitystreams::context())
+      .set_id(generate_activity_id(UpdateType::Update)?)
+      .set_to(public())
+      .set_many_ccs(vec![community.get_followers_url()?]);
 
     send_activity_to_community(
       creator,
       &community,
-      vec![community.get_shared_inbox_url()],
-      update,
-      client,
-      pool,
+      vec![community.get_shared_inbox_url()?],
+      update.into_any_base()?,
+      context,
     )
     .await?;
     Ok(())
   }
 
-  async fn send_delete(
-    &self,
-    creator: &User_,
-    client: &Client,
-    pool: &DbPool,
-  ) -> Result<(), LemmyError> {
-    let page = self.to_apub(pool).await?;
+  async fn send_delete(&self, creator: &User_, context: &LemmyContext) -> Result<(), LemmyError> {
+    let page = self.to_apub(context.pool()).await?;
 
     let community_id = self.community_id;
-    let community = blocking(pool, move |conn| Community::read(conn, community_id)).await??;
-
-    let id = format!("{}/delete/{}", self.ap_id, uuid::Uuid::new_v4());
-    let mut delete = Delete::default();
-
-    populate_object_props(
-      &mut delete.object_props,
-      vec![community.get_followers_url()],
-      &id,
-    )?;
+    let community = blocking(context.pool(), move |conn| {
+      Community::read(conn, community_id)
+    })
+    .await??;
 
+    let mut delete = Delete::new(creator.actor_id.to_owned(), page.into_any_base()?);
     delete
-      .delete_props
-      .set_actor_xsd_any_uri(creator.actor_id.to_owned())?
-      .set_object_base_box(BaseBox::from_concrete(page)?)?;
+      .set_context(activitystreams::context())
+      .set_id(generate_activity_id(DeleteType::Delete)?)
+      .set_to(public())
+      .set_many_ccs(vec![community.get_followers_url()?]);
 
     send_activity_to_community(
       creator,
       &community,
-      vec![community.get_shared_inbox_url()],
-      delete,
-      client,
-      pool,
+      vec![community.get_shared_inbox_url()?],
+      delete.into_any_base()?,
+      context,
     )
     .await?;
     Ok(())
@@ -351,140 +383,99 @@ impl ApubObjectType for Post {
   async fn send_undo_delete(
     &self,
     creator: &User_,
-    client: &Client,
-    pool: &DbPool,
+    context: &LemmyContext,
   ) -> Result<(), LemmyError> {
-    let page = self.to_apub(pool).await?;
+    let page = self.to_apub(context.pool()).await?;
 
     let community_id = self.community_id;
-    let community = blocking(pool, move |conn| Community::read(conn, community_id)).await??;
-
-    let id = format!("{}/delete/{}", self.ap_id, uuid::Uuid::new_v4());
-    let mut delete = Delete::default();
-
-    populate_object_props(
-      &mut delete.object_props,
-      vec![community.get_followers_url()],
-      &id,
-    )?;
+    let community = blocking(context.pool(), move |conn| {
+      Community::read(conn, community_id)
+    })
+    .await??;
 
+    let mut delete = Delete::new(creator.actor_id.to_owned(), page.into_any_base()?);
     delete
-      .delete_props
-      .set_actor_xsd_any_uri(creator.actor_id.to_owned())?
-      .set_object_base_box(BaseBox::from_concrete(page)?)?;
+      .set_context(activitystreams::context())
+      .set_id(generate_activity_id(DeleteType::Delete)?)
+      .set_to(public())
+      .set_many_ccs(vec![community.get_followers_url()?]);
 
-    // TODO
     // Undo that fake activity
-    let undo_id = format!("{}/undo/delete/{}", self.ap_id, uuid::Uuid::new_v4());
-    let mut undo = Undo::default();
-
-    populate_object_props(
-      &mut undo.object_props,
-      vec![community.get_followers_url()],
-      &undo_id,
-    )?;
-
+    let mut undo = Undo::new(creator.actor_id.to_owned(), delete.into_any_base()?);
     undo
-      .undo_props
-      .set_actor_xsd_any_uri(creator.actor_id.to_owned())?
-      .set_object_base_box(delete)?;
+      .set_context(activitystreams::context())
+      .set_id(generate_activity_id(UndoType::Undo)?)
+      .set_to(public())
+      .set_many_ccs(vec![community.get_followers_url()?]);
 
     send_activity_to_community(
       creator,
       &community,
-      vec![community.get_shared_inbox_url()],
-      undo,
-      client,
-      pool,
+      vec![community.get_shared_inbox_url()?],
+      undo.into_any_base()?,
+      context,
     )
     .await?;
     Ok(())
   }
 
-  async fn send_remove(
-    &self,
-    mod_: &User_,
-    client: &Client,
-    pool: &DbPool,
-  ) -> Result<(), LemmyError> {
-    let page = self.to_apub(pool).await?;
+  async fn send_remove(&self, mod_: &User_, context: &LemmyContext) -> Result<(), LemmyError> {
+    let page = self.to_apub(context.pool()).await?;
 
     let community_id = self.community_id;
-    let community = blocking(pool, move |conn| Community::read(conn, community_id)).await??;
-
-    let id = format!("{}/remove/{}", self.ap_id, uuid::Uuid::new_v4());
-    let mut remove = Remove::default();
-
-    populate_object_props(
-      &mut remove.object_props,
-      vec![community.get_followers_url()],
-      &id,
-    )?;
+    let community = blocking(context.pool(), move |conn| {
+      Community::read(conn, community_id)
+    })
+    .await??;
 
+    let mut remove = Remove::new(mod_.actor_id.to_owned(), page.into_any_base()?);
     remove
-      .remove_props
-      .set_actor_xsd_any_uri(mod_.actor_id.to_owned())?
-      .set_object_base_box(BaseBox::from_concrete(page)?)?;
+      .set_context(activitystreams::context())
+      .set_id(generate_activity_id(RemoveType::Remove)?)
+      .set_to(public())
+      .set_many_ccs(vec![community.get_followers_url()?]);
 
     send_activity_to_community(
       mod_,
       &community,
-      vec![community.get_shared_inbox_url()],
-      remove,
-      client,
-      pool,
+      vec![community.get_shared_inbox_url()?],
+      remove.into_any_base()?,
+      context,
     )
     .await?;
     Ok(())
   }
 
-  async fn send_undo_remove(
-    &self,
-    mod_: &User_,
-    client: &Client,
-    pool: &DbPool,
-  ) -> Result<(), LemmyError> {
-    let page = self.to_apub(pool).await?;
+  async fn send_undo_remove(&self, mod_: &User_, context: &LemmyContext) -> Result<(), LemmyError> {
+    let page = self.to_apub(context.pool()).await?;
 
     let community_id = self.community_id;
-    let community = blocking(pool, move |conn| Community::read(conn, community_id)).await??;
-
-    let id = format!("{}/remove/{}", self.ap_id, uuid::Uuid::new_v4());
-    let mut remove = Remove::default();
-
-    populate_object_props(
-      &mut remove.object_props,
-      vec![community.get_followers_url()],
-      &id,
-    )?;
+    let community = blocking(context.pool(), move |conn| {
+      Community::read(conn, community_id)
+    })
+    .await??;
 
+    let mut remove = Remove::new(mod_.actor_id.to_owned(), page.into_any_base()?);
     remove
-      .remove_props
-      .set_actor_xsd_any_uri(mod_.actor_id.to_owned())?
-      .set_object_base_box(BaseBox::from_concrete(page)?)?;
+      .set_context(activitystreams::context())
+      .set_id(generate_activity_id(RemoveType::Remove)?)
+      .set_to(public())
+      .set_many_ccs(vec![community.get_followers_url()?]);
 
     // Undo that fake activity
-    let undo_id = format!("{}/undo/remove/{}", self.ap_id, uuid::Uuid::new_v4());
-    let mut undo = Undo::default();
-
-    populate_object_props(
-      &mut undo.object_props,
-      vec![community.get_followers_url()],
-      &undo_id,
-    )?;
-
+    let mut undo = Undo::new(mod_.actor_id.to_owned(), remove.into_any_base()?);
     undo
-      .undo_props
-      .set_actor_xsd_any_uri(mod_.actor_id.to_owned())?
-      .set_object_base_box(remove)?;
+      .set_context(activitystreams::context())
+      .set_id(generate_activity_id(UndoType::Undo)?)
+      .set_to(public())
+      .set_many_ccs(vec![community.get_followers_url()?]);
 
     send_activity_to_community(
       mod_,
       &community,
-      vec![community.get_shared_inbox_url()],
-      undo,
-      client,
-      pool,
+      vec![community.get_shared_inbox_url()?],
+      undo.into_any_base()?,
+      context,
     )
     .await?;
     Ok(())
@@ -493,73 +484,55 @@ impl ApubObjectType for Post {
 
 #[async_trait::async_trait(?Send)]
 impl ApubLikeableType for Post {
-  async fn send_like(
-    &self,
-    creator: &User_,
-    client: &Client,
-    pool: &DbPool,
-  ) -> Result<(), LemmyError> {
-    let page = self.to_apub(pool).await?;
+  async fn send_like(&self, creator: &User_, context: &LemmyContext) -> Result<(), LemmyError> {
+    let page = self.to_apub(context.pool()).await?;
 
     let community_id = self.community_id;
-    let community = blocking(pool, move |conn| Community::read(conn, community_id)).await??;
-
-    let id = format!("{}/like/{}", self.ap_id, uuid::Uuid::new_v4());
+    let community = blocking(context.pool(), move |conn| {
+      Community::read(conn, community_id)
+    })
+    .await??;
 
-    let mut like = Like::new();
-    populate_object_props(
-      &mut like.object_props,
-      vec![community.get_followers_url()],
-      &id,
-    )?;
+    let mut like = Like::new(creator.actor_id.to_owned(), page.into_any_base()?);
     like
-      .like_props
-      .set_actor_xsd_any_uri(creator.actor_id.to_owned())?
-      .set_object_base_box(BaseBox::from_concrete(page)?)?;
+      .set_context(activitystreams::context())
+      .set_id(generate_activity_id(LikeType::Like)?)
+      .set_to(public())
+      .set_many_ccs(vec![community.get_followers_url()?]);
 
     send_activity_to_community(
       &creator,
       &community,
-      vec![community.get_shared_inbox_url()],
-      like,
-      client,
-      pool,
+      vec![community.get_shared_inbox_url()?],
+      like.into_any_base()?,
+      context,
     )
     .await?;
     Ok(())
   }
 
-  async fn send_dislike(
-    &self,
-    creator: &User_,
-    client: &Client,
-    pool: &DbPool,
-  ) -> Result<(), LemmyError> {
-    let page = self.to_apub(pool).await?;
+  async fn send_dislike(&self, creator: &User_, context: &LemmyContext) -> Result<(), LemmyError> {
+    let page = self.to_apub(context.pool()).await?;
 
     let community_id = self.community_id;
-    let community = blocking(pool, move |conn| Community::read(conn, community_id)).await??;
-
-    let id = format!("{}/dislike/{}", self.ap_id, uuid::Uuid::new_v4());
+    let community = blocking(context.pool(), move |conn| {
+      Community::read(conn, community_id)
+    })
+    .await??;
 
-    let mut dislike = Dislike::new();
-    populate_object_props(
-      &mut dislike.object_props,
-      vec![community.get_followers_url()],
-      &id,
-    )?;
+    let mut dislike = Dislike::new(creator.actor_id.to_owned(), page.into_any_base()?);
     dislike
-      .dislike_props
-      .set_actor_xsd_any_uri(creator.actor_id.to_owned())?
-      .set_object_base_box(BaseBox::from_concrete(page)?)?;
+      .set_context(activitystreams::context())
+      .set_id(generate_activity_id(DislikeType::Dislike)?)
+      .set_to(public())
+      .set_many_ccs(vec![community.get_followers_url()?]);
 
     send_activity_to_community(
       &creator,
       &community,
-      vec![community.get_shared_inbox_url()],
-      dislike,
-      client,
-      pool,
+      vec![community.get_shared_inbox_url()?],
+      dislike.into_any_base()?,
+      context,
     )
     .await?;
     Ok(())
@@ -568,50 +541,37 @@ impl ApubLikeableType for Post {
   async fn send_undo_like(
     &self,
     creator: &User_,
-    client: &Client,
-    pool: &DbPool,
+    context: &LemmyContext,
   ) -> Result<(), LemmyError> {
-    let page = self.to_apub(pool).await?;
+    let page = self.to_apub(context.pool()).await?;
 
     let community_id = self.community_id;
-    let community = blocking(pool, move |conn| Community::read(conn, community_id)).await??;
-
-    let id = format!("{}/like/{}", self.ap_id, uuid::Uuid::new_v4());
+    let community = blocking(context.pool(), move |conn| {
+      Community::read(conn, community_id)
+    })
+    .await??;
 
-    let mut like = Like::new();
-    populate_object_props(
-      &mut like.object_props,
-      vec![community.get_followers_url()],
-      &id,
-    )?;
+    let mut like = Like::new(creator.actor_id.to_owned(), page.into_any_base()?);
     like
-      .like_props
-      .set_actor_xsd_any_uri(creator.actor_id.to_owned())?
-      .set_object_base_box(BaseBox::from_concrete(page)?)?;
+      .set_context(activitystreams::context())
+      .set_id(generate_activity_id(LikeType::Like)?)
+      .set_to(public())
+      .set_many_ccs(vec![community.get_followers_url()?]);
 
-    // TODO
     // Undo that fake activity
-    let undo_id = format!("{}/undo/like/{}", self.ap_id, uuid::Uuid::new_v4());
-    let mut undo = Undo::default();
-
-    populate_object_props(
-      &mut undo.object_props,
-      vec![community.get_followers_url()],
-      &undo_id,
-    )?;
-
+    let mut undo = Undo::new(creator.actor_id.to_owned(), like.into_any_base()?);
     undo
-      .undo_props
-      .set_actor_xsd_any_uri(creator.actor_id.to_owned())?
-      .set_object_base_box(like)?;
+      .set_context(activitystreams::context())
+      .set_id(generate_activity_id(UndoType::Undo)?)
+      .set_to(public())
+      .set_many_ccs(vec![community.get_followers_url()?]);
 
     send_activity_to_community(
       &creator,
       &community,
-      vec![community.get_shared_inbox_url()],
-      undo,
-      client,
-      pool,
+      vec![community.get_shared_inbox_url()?],
+      undo.into_any_base()?,
+      context,
     )
     .await?;
     Ok(())
index bc685b2382019d1e26cc8027ee89ab060b722d73..8e5836885c3457939d5a70850a3b7ce607ad687a 100644 (file)
@@ -1,38 +1,47 @@
 use crate::{
   apub::{
-    activities::send_activity,
+    activities::{generate_activity_id, send_activity},
+    check_actor_domain,
+    check_is_apub_id_valid,
     create_tombstone,
-    fetcher::get_or_fetch_and_upsert_remote_user,
+    fetcher::get_or_fetch_and_upsert_user,
     insert_activity,
+    ActorType,
     ApubObjectType,
     FromApub,
     ToApub,
   },
   blocking,
   DbPool,
+  LemmyContext,
   LemmyError,
 };
 use activitystreams::{
-  activity::{Create, Delete, Undo, Update},
-  context,
-  object::{kind::NoteType, properties::ObjectProperties, Note},
+  activity::{
+    kind::{CreateType, DeleteType, UndoType, UpdateType},
+    Create,
+    Delete,
+    Undo,
+    Update,
+  },
+  object::{kind::NoteType, Note, Tombstone},
+  prelude::*,
 };
-use activitystreams_new::object::Tombstone;
-use actix_web::client::Client;
+use anyhow::Context;
 use lemmy_db::{
   private_message::{PrivateMessage, PrivateMessageForm},
   user::User_,
   Crud,
 };
-use lemmy_utils::convert_datetime;
+use lemmy_utils::{convert_datetime, location_info};
+use url::Url;
 
 #[async_trait::async_trait(?Send)]
 impl ToApub for PrivateMessage {
   type Response = Note;
 
   async fn to_apub(&self, pool: &DbPool) -> Result<Note, LemmyError> {
-    let mut private_message = Note::default();
-    let oprops: &mut ObjectProperties = private_message.as_mut();
+    let mut private_message = Note::new();
 
     let creator_id = self.creator_id;
     let creator = blocking(pool, move |conn| User_::read(conn, creator_id)).await??;
@@ -40,28 +49,23 @@ impl ToApub for PrivateMessage {
     let recipient_id = self.recipient_id;
     let recipient = blocking(pool, move |conn| User_::read(conn, recipient_id)).await??;
 
-    oprops
-      .set_context_xsd_any_uri(context())?
-      .set_id(self.ap_id.to_owned())?
-      .set_published(convert_datetime(self.published))?
-      .set_content_xsd_string(self.content.to_owned())?
-      .set_to_xsd_any_uri(recipient.actor_id)?
-      .set_attributed_to_xsd_any_uri(creator.actor_id)?;
+    private_message
+      .set_context(activitystreams::context())
+      .set_id(Url::parse(&self.ap_id.to_owned())?)
+      .set_published(convert_datetime(self.published))
+      .set_content(self.content.to_owned())
+      .set_to(recipient.actor_id)
+      .set_attributed_to(creator.actor_id);
 
     if let Some(u) = self.updated {
-      oprops.set_updated(convert_datetime(u))?;
+      private_message.set_updated(convert_datetime(u));
     }
 
     Ok(private_message)
   }
 
   fn to_tombstone(&self) -> Result<Tombstone, LemmyError> {
-    create_tombstone(
-      self.deleted,
-      &self.ap_id,
-      self.updated,
-      NoteType.to_string(),
-    )
+    create_tombstone(self.deleted, &self.ap_id, self.updated, NoteType::Note)
   }
 }
 
@@ -72,34 +76,41 @@ impl FromApub for PrivateMessageForm {
   /// Parse an ActivityPub note received from another instance into a Lemmy Private message
   async fn from_apub(
     note: &Note,
-    client: &Client,
-    pool: &DbPool,
+    context: &LemmyContext,
+    expected_domain: Option<Url>,
   ) -> Result<PrivateMessageForm, LemmyError> {
-    let oprops = &note.object_props;
-    let creator_actor_id = &oprops.get_attributed_to_xsd_any_uri().unwrap().to_string();
-
-    let creator = get_or_fetch_and_upsert_remote_user(&creator_actor_id, client, pool).await?;
-
-    let recipient_actor_id = &oprops.get_to_xsd_any_uri().unwrap().to_string();
-
-    let recipient = get_or_fetch_and_upsert_remote_user(&recipient_actor_id, client, pool).await?;
+    let creator_actor_id = note
+      .attributed_to()
+      .context(location_info!())?
+      .clone()
+      .single_xsd_any_uri()
+      .context(location_info!())?;
+
+    let creator = get_or_fetch_and_upsert_user(&creator_actor_id, context).await?;
+    let recipient_actor_id = note
+      .to()
+      .context(location_info!())?
+      .clone()
+      .single_xsd_any_uri()
+      .context(location_info!())?;
+    let recipient = get_or_fetch_and_upsert_user(&recipient_actor_id, context).await?;
+    let ap_id = note.id_unchecked().context(location_info!())?.to_string();
+    check_is_apub_id_valid(&Url::parse(&ap_id)?)?;
 
     Ok(PrivateMessageForm {
       creator_id: creator.id,
       recipient_id: recipient.id,
-      content: oprops
-        .get_content_xsd_string()
-        .map(|c| c.to_string())
-        .unwrap(),
-      published: oprops
-        .get_published()
-        .map(|u| u.as_ref().to_owned().naive_local()),
-      updated: oprops
-        .get_updated()
-        .map(|u| u.as_ref().to_owned().naive_local()),
+      content: note
+        .content()
+        .context(location_info!())?
+        .as_single_xsd_string()
+        .context(location_info!())?
+        .to_string(),
+      published: note.published().map(|u| u.to_owned().naive_local()),
+      updated: note.updated().map(|u| u.to_owned().naive_local()),
       deleted: None,
       read: None,
-      ap_id: oprops.get_id().unwrap().to_string(),
+      ap_id: check_actor_domain(note, expected_domain)?,
       local: false,
     })
   }
@@ -108,156 +119,120 @@ impl FromApub for PrivateMessageForm {
 #[async_trait::async_trait(?Send)]
 impl ApubObjectType for PrivateMessage {
   /// Send out information about a newly created private message
-  async fn send_create(
-    &self,
-    creator: &User_,
-    client: &Client,
-    pool: &DbPool,
-  ) -> Result<(), LemmyError> {
-    let note = self.to_apub(pool).await?;
-    let id = format!("{}/create/{}", self.ap_id, uuid::Uuid::new_v4());
+  async fn send_create(&self, creator: &User_, context: &LemmyContext) -> Result<(), LemmyError> {
+    let note = self.to_apub(context.pool()).await?;
 
     let recipient_id = self.recipient_id;
-    let recipient = blocking(pool, move |conn| User_::read(conn, recipient_id)).await??;
+    let recipient = blocking(context.pool(), move |conn| User_::read(conn, recipient_id)).await??;
 
-    let mut create = Create::new();
+    let mut create = Create::new(creator.actor_id.to_owned(), note.into_any_base()?);
+    let to = recipient.get_inbox_url()?;
     create
-      .object_props
-      .set_context_xsd_any_uri(context())?
-      .set_id(id)?;
-    let to = format!("{}/inbox", recipient.actor_id);
+      .set_context(activitystreams::context())
+      .set_id(generate_activity_id(CreateType::Create)?)
+      .set_to(to.clone());
 
-    create
-      .create_props
-      .set_actor_xsd_any_uri(creator.actor_id.to_owned())?
-      .set_object_base_box(note)?;
-
-    insert_activity(creator.id, create.clone(), true, pool).await?;
+    insert_activity(creator.id, create.clone(), true, context.pool()).await?;
 
-    send_activity(client, &create, creator, vec![to]).await?;
+    send_activity(
+      context.client(),
+      &create.into_any_base()?,
+      creator,
+      vec![to],
+    )
+    .await?;
     Ok(())
   }
 
   /// Send out information about an edited post, to the followers of the community.
-  async fn send_update(
-    &self,
-    creator: &User_,
-    client: &Client,
-    pool: &DbPool,
-  ) -> Result<(), LemmyError> {
-    let note = self.to_apub(pool).await?;
-    let id = format!("{}/update/{}", self.ap_id, uuid::Uuid::new_v4());
+  async fn send_update(&self, creator: &User_, context: &LemmyContext) -> Result<(), LemmyError> {
+    let note = self.to_apub(context.pool()).await?;
 
     let recipient_id = self.recipient_id;
-    let recipient = blocking(pool, move |conn| User_::read(conn, recipient_id)).await??;
-
-    let mut update = Update::new();
-    update
-      .object_props
-      .set_context_xsd_any_uri(context())?
-      .set_id(id)?;
-    let to = format!("{}/inbox", recipient.actor_id);
+    let recipient = blocking(context.pool(), move |conn| User_::read(conn, recipient_id)).await??;
 
+    let mut update = Update::new(creator.actor_id.to_owned(), note.into_any_base()?);
+    let to = recipient.get_inbox_url()?;
     update
-      .update_props
-      .set_actor_xsd_any_uri(creator.actor_id.to_owned())?
-      .set_object_base_box(note)?;
+      .set_context(activitystreams::context())
+      .set_id(generate_activity_id(UpdateType::Update)?)
+      .set_to(to.clone());
 
-    insert_activity(creator.id, update.clone(), true, pool).await?;
+    insert_activity(creator.id, update.clone(), true, context.pool()).await?;
 
-    send_activity(client, &update, creator, vec![to]).await?;
+    send_activity(
+      context.client(),
+      &update.into_any_base()?,
+      creator,
+      vec![to],
+    )
+    .await?;
     Ok(())
   }
 
-  async fn send_delete(
-    &self,
-    creator: &User_,
-    client: &Client,
-    pool: &DbPool,
-  ) -> Result<(), LemmyError> {
-    let note = self.to_apub(pool).await?;
-    let id = format!("{}/delete/{}", self.ap_id, uuid::Uuid::new_v4());
+  async fn send_delete(&self, creator: &User_, context: &LemmyContext) -> Result<(), LemmyError> {
+    let note = self.to_apub(context.pool()).await?;
 
     let recipient_id = self.recipient_id;
-    let recipient = blocking(pool, move |conn| User_::read(conn, recipient_id)).await??;
+    let recipient = blocking(context.pool(), move |conn| User_::read(conn, recipient_id)).await??;
 
-    let mut delete = Delete::new();
+    let mut delete = Delete::new(creator.actor_id.to_owned(), note.into_any_base()?);
+    let to = recipient.get_inbox_url()?;
     delete
-      .object_props
-      .set_context_xsd_any_uri(context())?
-      .set_id(id)?;
-    let to = format!("{}/inbox", recipient.actor_id);
+      .set_context(activitystreams::context())
+      .set_id(generate_activity_id(DeleteType::Delete)?)
+      .set_to(to.clone());
 
-    delete
-      .delete_props
-      .set_actor_xsd_any_uri(creator.actor_id.to_owned())?
-      .set_object_base_box(note)?;
-
-    insert_activity(creator.id, delete.clone(), true, pool).await?;
+    insert_activity(creator.id, delete.clone(), true, context.pool()).await?;
 
-    send_activity(client, &delete, creator, vec![to]).await?;
+    send_activity(
+      context.client(),
+      &delete.into_any_base()?,
+      creator,
+      vec![to],
+    )
+    .await?;
     Ok(())
   }
 
   async fn send_undo_delete(
     &self,
     creator: &User_,
-    client: &Client,
-    pool: &DbPool,
+    context: &LemmyContext,
   ) -> Result<(), LemmyError> {
-    let note = self.to_apub(pool).await?;
-    let id = format!("{}/delete/{}", self.ap_id, uuid::Uuid::new_v4());
+    let note = self.to_apub(context.pool()).await?;
 
     let recipient_id = self.recipient_id;
-    let recipient = blocking(pool, move |conn| User_::read(conn, recipient_id)).await??;
+    let recipient = blocking(context.pool(), move |conn| User_::read(conn, recipient_id)).await??;
 
-    let mut delete = Delete::new();
+    let mut delete = Delete::new(creator.actor_id.to_owned(), note.into_any_base()?);
+    let to = recipient.get_inbox_url()?;
     delete
-      .object_props
-      .set_context_xsd_any_uri(context())?
-      .set_id(id)?;
-    let to = format!("{}/inbox", recipient.actor_id);
+      .set_context(activitystreams::context())
+      .set_id(generate_activity_id(DeleteType::Delete)?)
+      .set_to(to.clone());
 
-    delete
-      .delete_props
-      .set_actor_xsd_any_uri(creator.actor_id.to_owned())?
-      .set_object_base_box(note)?;
-
-    // TODO
     // Undo that fake activity
-    let undo_id = format!("{}/undo/delete/{}", self.ap_id, uuid::Uuid::new_v4());
-    let mut undo = Undo::default();
-
-    undo
-      .object_props
-      .set_context_xsd_any_uri(context())?
-      .set_id(undo_id)?;
-
+    let mut undo = Undo::new(creator.actor_id.to_owned(), delete.into_any_base()?);
     undo
-      .undo_props
-      .set_actor_xsd_any_uri(creator.actor_id.to_owned())?
-      .set_object_base_box(delete)?;
+      .set_context(activitystreams::context())
+      .set_id(generate_activity_id(UndoType::Undo)?)
+      .set_to(to.clone());
 
-    insert_activity(creator.id, undo.clone(), true, pool).await?;
+    insert_activity(creator.id, undo.clone(), true, context.pool()).await?;
 
-    send_activity(client, &undo, creator, vec![to]).await?;
+    send_activity(context.client(), &undo.into_any_base()?, creator, vec![to]).await?;
     Ok(())
   }
 
-  async fn send_remove(
-    &self,
-    _mod_: &User_,
-    _client: &Client,
-    _pool: &DbPool,
-  ) -> Result<(), LemmyError> {
+  async fn send_remove(&self, _mod_: &User_, _context: &LemmyContext) -> Result<(), LemmyError> {
     unimplemented!()
   }
 
   async fn send_undo_remove(
     &self,
     _mod_: &User_,
-    _client: &Client,
-    _pool: &DbPool,
+    _context: &LemmyContext,
   ) -> Result<(), LemmyError> {
     unimplemented!()
   }
diff --git a/server/src/apub/shared_inbox.rs b/server/src/apub/shared_inbox.rs
deleted file mode 100644 (file)
index 7319f1a..0000000
+++ /dev/null
@@ -1,1801 +0,0 @@
-use crate::{
-  api::{
-    comment::{send_local_notifs, CommentResponse},
-    community::CommunityResponse,
-    post::PostResponse,
-  },
-  apub::{
-    community::do_announce,
-    extensions::signatures::verify,
-    fetcher::{
-      get_or_fetch_and_insert_remote_comment,
-      get_or_fetch_and_insert_remote_post,
-      get_or_fetch_and_upsert_remote_community,
-      get_or_fetch_and_upsert_remote_user,
-    },
-    insert_activity,
-    FromApub,
-    GroupExt,
-    PageExt,
-  },
-  blocking,
-  routes::{ChatServerParam, DbPoolParam},
-  websocket::{
-    server::{SendComment, SendCommunityRoomMessage, SendPost},
-    UserOperation,
-  },
-  DbPool,
-  LemmyError,
-};
-use activitystreams::{
-  activity::{Announce, Create, Delete, Dislike, Like, Remove, Undo, Update},
-  object::Note,
-  Activity,
-  Base,
-  BaseBox,
-};
-use actix_web::{client::Client, web, HttpRequest, HttpResponse};
-use lemmy_db::{
-  comment::{Comment, CommentForm, CommentLike, CommentLikeForm},
-  comment_view::CommentView,
-  community::{Community, CommunityForm},
-  community_view::CommunityView,
-  naive_now,
-  post::{Post, PostForm, PostLike, PostLikeForm},
-  post_view::PostView,
-  Crud,
-  Likeable,
-};
-use lemmy_utils::scrape_text_for_mentions;
-use log::debug;
-use serde::{Deserialize, Serialize};
-use std::fmt::Debug;
-
-#[serde(untagged)]
-#[derive(Serialize, Deserialize, Debug)]
-pub enum SharedAcceptedObjects {
-  Create(Box<Create>),
-  Update(Box<Update>),
-  Like(Box<Like>),
-  Dislike(Box<Dislike>),
-  Delete(Box<Delete>),
-  Undo(Box<Undo>),
-  Remove(Box<Remove>),
-  Announce(Box<Announce>),
-}
-
-impl SharedAcceptedObjects {
-  fn object(&self) -> Option<&BaseBox> {
-    match self {
-      SharedAcceptedObjects::Create(c) => c.create_props.get_object_base_box(),
-      SharedAcceptedObjects::Update(u) => u.update_props.get_object_base_box(),
-      SharedAcceptedObjects::Like(l) => l.like_props.get_object_base_box(),
-      SharedAcceptedObjects::Dislike(d) => d.dislike_props.get_object_base_box(),
-      SharedAcceptedObjects::Delete(d) => d.delete_props.get_object_base_box(),
-      SharedAcceptedObjects::Undo(d) => d.undo_props.get_object_base_box(),
-      SharedAcceptedObjects::Remove(r) => r.remove_props.get_object_base_box(),
-      SharedAcceptedObjects::Announce(a) => a.announce_props.get_object_base_box(),
-    }
-  }
-  fn sender(&self) -> String {
-    let uri = match self {
-      SharedAcceptedObjects::Create(c) => c.create_props.get_actor_xsd_any_uri(),
-      SharedAcceptedObjects::Update(u) => u.update_props.get_actor_xsd_any_uri(),
-      SharedAcceptedObjects::Like(l) => l.like_props.get_actor_xsd_any_uri(),
-      SharedAcceptedObjects::Dislike(d) => d.dislike_props.get_actor_xsd_any_uri(),
-      SharedAcceptedObjects::Delete(d) => d.delete_props.get_actor_xsd_any_uri(),
-      SharedAcceptedObjects::Undo(d) => d.undo_props.get_actor_xsd_any_uri(),
-      SharedAcceptedObjects::Remove(r) => r.remove_props.get_actor_xsd_any_uri(),
-      SharedAcceptedObjects::Announce(a) => a.announce_props.get_actor_xsd_any_uri(),
-    };
-    uri.unwrap().clone().to_string()
-  }
-  fn cc(&self) -> String {
-    // TODO: there is probably an easier way to do this
-    let oprops = match self {
-      SharedAcceptedObjects::Create(c) => &c.object_props,
-      SharedAcceptedObjects::Update(u) => &u.object_props,
-      SharedAcceptedObjects::Like(l) => &l.object_props,
-      SharedAcceptedObjects::Dislike(d) => &d.object_props,
-      SharedAcceptedObjects::Delete(d) => &d.object_props,
-      SharedAcceptedObjects::Undo(d) => &d.object_props,
-      SharedAcceptedObjects::Remove(r) => &r.object_props,
-      SharedAcceptedObjects::Announce(a) => &a.object_props,
-    };
-    oprops
-      .get_many_cc_xsd_any_uris()
-      .unwrap()
-      .next()
-      .unwrap()
-      .to_string()
-  }
-}
-
-/// Handler for all incoming activities to user inboxes.
-pub async fn shared_inbox(
-  request: HttpRequest,
-  input: web::Json<SharedAcceptedObjects>,
-  client: web::Data<Client>,
-  pool: DbPoolParam,
-  chat_server: ChatServerParam,
-) -> Result<HttpResponse, LemmyError> {
-  let activity = input.into_inner();
-  let pool = &pool;
-  let client = &client;
-
-  let json = serde_json::to_string(&activity)?;
-  debug!("Shared inbox received activity: {}", json);
-
-  let object = activity.object().cloned().unwrap();
-  let sender = &activity.sender();
-  let cc = &activity.cc();
-  // TODO: this is hacky, we should probably send the community id directly somehow
-  let to = cc.replace("/followers", "");
-
-  // TODO: this is ugly
-  match get_or_fetch_and_upsert_remote_user(&sender.to_string(), &client, pool).await {
-    Ok(u) => verify(&request, &u)?,
-    Err(_) => {
-      let c = get_or_fetch_and_upsert_remote_community(&sender.to_string(), &client, pool).await?;
-      verify(&request, &c)?;
-    }
-  }
-
-  match (activity, object.kind()) {
-    (SharedAcceptedObjects::Create(c), Some("Page")) => {
-      receive_create_post((*c).clone(), client, pool, chat_server).await?;
-      announce_activity_if_valid::<Create>(*c, &to, sender, client, pool).await
-    }
-    (SharedAcceptedObjects::Update(u), Some("Page")) => {
-      receive_update_post((*u).clone(), client, pool, chat_server).await?;
-      announce_activity_if_valid::<Update>(*u, &to, sender, client, pool).await
-    }
-    (SharedAcceptedObjects::Like(l), Some("Page")) => {
-      receive_like_post((*l).clone(), client, pool, chat_server).await?;
-      announce_activity_if_valid::<Like>(*l, &to, sender, client, pool).await
-    }
-    (SharedAcceptedObjects::Dislike(d), Some("Page")) => {
-      receive_dislike_post((*d).clone(), client, pool, chat_server).await?;
-      announce_activity_if_valid::<Dislike>(*d, &to, sender, client, pool).await
-    }
-    (SharedAcceptedObjects::Delete(d), Some("Page")) => {
-      receive_delete_post((*d).clone(), client, pool, chat_server).await?;
-      announce_activity_if_valid::<Delete>(*d, &to, sender, client, pool).await
-    }
-    (SharedAcceptedObjects::Remove(r), Some("Page")) => {
-      receive_remove_post((*r).clone(), client, pool, chat_server).await?;
-      announce_activity_if_valid::<Remove>(*r, &to, sender, client, pool).await
-    }
-    (SharedAcceptedObjects::Create(c), Some("Note")) => {
-      receive_create_comment((*c).clone(), client, pool, chat_server).await?;
-      announce_activity_if_valid::<Create>(*c, &to, sender, client, pool).await
-    }
-    (SharedAcceptedObjects::Update(u), Some("Note")) => {
-      receive_update_comment((*u).clone(), client, pool, chat_server).await?;
-      announce_activity_if_valid::<Update>(*u, &to, sender, client, pool).await
-    }
-    (SharedAcceptedObjects::Like(l), Some("Note")) => {
-      receive_like_comment((*l).clone(), client, pool, chat_server).await?;
-      announce_activity_if_valid::<Like>(*l, &to, sender, client, pool).await
-    }
-    (SharedAcceptedObjects::Dislike(d), Some("Note")) => {
-      receive_dislike_comment((*d).clone(), client, pool, chat_server).await?;
-      announce_activity_if_valid::<Dislike>(*d, &to, sender, client, pool).await
-    }
-    (SharedAcceptedObjects::Delete(d), Some("Note")) => {
-      receive_delete_comment((*d).clone(), client, pool, chat_server).await?;
-      announce_activity_if_valid::<Delete>(*d, &to, sender, client, pool).await
-    }
-    (SharedAcceptedObjects::Remove(r), Some("Note")) => {
-      receive_remove_comment((*r).clone(), client, pool, chat_server).await?;
-      announce_activity_if_valid::<Remove>(*r, &to, sender, client, pool).await
-    }
-    (SharedAcceptedObjects::Delete(d), Some("Group")) => {
-      receive_delete_community((*d).clone(), client, pool, chat_server).await?;
-      announce_activity_if_valid::<Delete>(*d, &to, sender, client, pool).await
-    }
-    (SharedAcceptedObjects::Remove(r), Some("Group")) => {
-      receive_remove_community((*r).clone(), client, pool, chat_server).await?;
-      announce_activity_if_valid::<Remove>(*r, &to, sender, client, pool).await
-    }
-    (SharedAcceptedObjects::Undo(u), Some("Delete")) => {
-      receive_undo_delete((*u).clone(), client, pool, chat_server).await?;
-      announce_activity_if_valid::<Undo>(*u, &to, sender, client, pool).await
-    }
-    (SharedAcceptedObjects::Undo(u), Some("Remove")) => {
-      receive_undo_remove((*u).clone(), client, pool, chat_server).await?;
-      announce_activity_if_valid::<Undo>(*u, &to, sender, client, pool).await
-    }
-    (SharedAcceptedObjects::Undo(u), Some("Like")) => {
-      receive_undo_like((*u).clone(), client, pool, chat_server).await?;
-      announce_activity_if_valid::<Undo>(*u, &to, sender, client, pool).await
-    }
-    (SharedAcceptedObjects::Announce(a), _) => receive_announce(a, client, pool, chat_server).await,
-    (a, _) => receive_unhandled_activity(a),
-  }
-}
-
-// TODO: should pass in sender as ActorType, but thats a bit tricky in shared_inbox()
-async fn announce_activity_if_valid<A>(
-  activity: A,
-  community_uri: &str,
-  sender: &str,
-  client: &Client,
-  pool: &DbPool,
-) -> Result<HttpResponse, LemmyError>
-where
-  A: Activity + Base + Serialize + Debug,
-{
-  let community_uri = community_uri.to_owned();
-  let community = blocking(pool, move |conn| {
-    Community::read_from_actor_id(conn, &community_uri)
-  })
-  .await??;
-
-  if community.local {
-    let sending_user = get_or_fetch_and_upsert_remote_user(sender, client, pool).await?;
-
-    do_announce(activity, &community, &sending_user, client, pool).await
-  } else {
-    Ok(HttpResponse::NotFound().finish())
-  }
-}
-
-async fn receive_announce(
-  announce: Box<Announce>,
-  client: &Client,
-  pool: &DbPool,
-  chat_server: ChatServerParam,
-) -> Result<HttpResponse, LemmyError> {
-  let object = announce
-    .announce_props
-    .get_object_base_box()
-    .unwrap()
-    .to_owned();
-  // TODO: too much copy paste
-  match object.kind() {
-    Some("Create") => {
-      let create = object.into_concrete::<Create>()?;
-      let inner_object = create.create_props.get_object_base_box().unwrap();
-      match inner_object.kind() {
-        Some("Page") => receive_create_post(create, client, pool, chat_server).await,
-        Some("Note") => receive_create_comment(create, client, pool, chat_server).await,
-        _ => receive_unhandled_activity(announce),
-      }
-    }
-    Some("Update") => {
-      let update = object.into_concrete::<Update>()?;
-      let inner_object = update.update_props.get_object_base_box().unwrap();
-      match inner_object.kind() {
-        Some("Page") => receive_update_post(update, client, pool, chat_server).await,
-        Some("Note") => receive_update_comment(update, client, pool, chat_server).await,
-        _ => receive_unhandled_activity(announce),
-      }
-    }
-    Some("Like") => {
-      let like = object.into_concrete::<Like>()?;
-      let inner_object = like.like_props.get_object_base_box().unwrap();
-      match inner_object.kind() {
-        Some("Page") => receive_like_post(like, client, pool, chat_server).await,
-        Some("Note") => receive_like_comment(like, client, pool, chat_server).await,
-        _ => receive_unhandled_activity(announce),
-      }
-    }
-    Some("Dislike") => {
-      let dislike = object.into_concrete::<Dislike>()?;
-      let inner_object = dislike.dislike_props.get_object_base_box().unwrap();
-      match inner_object.kind() {
-        Some("Page") => receive_dislike_post(dislike, client, pool, chat_server).await,
-        Some("Note") => receive_dislike_comment(dislike, client, pool, chat_server).await,
-        _ => receive_unhandled_activity(announce),
-      }
-    }
-    Some("Delete") => {
-      let delete = object.into_concrete::<Delete>()?;
-      let inner_object = delete.delete_props.get_object_base_box().unwrap();
-      match inner_object.kind() {
-        Some("Page") => receive_delete_post(delete, client, pool, chat_server).await,
-        Some("Note") => receive_delete_comment(delete, client, pool, chat_server).await,
-        _ => receive_unhandled_activity(announce),
-      }
-    }
-    Some("Remove") => {
-      let remove = object.into_concrete::<Remove>()?;
-      let inner_object = remove.remove_props.get_object_base_box().unwrap();
-      match inner_object.kind() {
-        Some("Page") => receive_remove_post(remove, client, pool, chat_server).await,
-        Some("Note") => receive_remove_comment(remove, client, pool, chat_server).await,
-        _ => receive_unhandled_activity(announce),
-      }
-    }
-    Some("Undo") => {
-      let undo = object.into_concrete::<Undo>()?;
-      let inner_object = undo.undo_props.get_object_base_box().unwrap();
-      match inner_object.kind() {
-        Some("Delete") => receive_undo_delete(undo, client, pool, chat_server).await,
-        Some("Remove") => receive_undo_remove(undo, client, pool, chat_server).await,
-        Some("Like") => receive_undo_like(undo, client, pool, chat_server).await,
-        _ => receive_unhandled_activity(announce),
-      }
-    }
-    _ => receive_unhandled_activity(announce),
-  }
-}
-
-fn receive_unhandled_activity<A>(activity: A) -> Result<HttpResponse, LemmyError>
-where
-  A: Debug,
-{
-  debug!("received unhandled activity type: {:?}", activity);
-  Ok(HttpResponse::NotImplemented().finish())
-}
-
-async fn receive_create_post(
-  create: Create,
-  client: &Client,
-  pool: &DbPool,
-  chat_server: ChatServerParam,
-) -> Result<HttpResponse, LemmyError> {
-  let page = create
-    .create_props
-    .get_object_base_box()
-    .to_owned()
-    .unwrap()
-    .to_owned()
-    .into_concrete::<PageExt>()?;
-
-  let user_uri = create
-    .create_props
-    .get_actor_xsd_any_uri()
-    .unwrap()
-    .to_string();
-
-  let user = get_or_fetch_and_upsert_remote_user(&user_uri, client, pool).await?;
-
-  insert_activity(user.id, create, false, pool).await?;
-
-  let post = PostForm::from_apub(&page, client, pool).await?;
-
-  let inserted_post = blocking(pool, move |conn| Post::create(conn, &post)).await??;
-
-  // Refetch the view
-  let inserted_post_id = inserted_post.id;
-  let post_view = blocking(pool, move |conn| {
-    PostView::read(conn, inserted_post_id, None)
-  })
-  .await??;
-
-  let res = PostResponse { post: post_view };
-
-  chat_server.do_send(SendPost {
-    op: UserOperation::CreatePost,
-    post: res,
-    my_id: None,
-  });
-
-  Ok(HttpResponse::Ok().finish())
-}
-
-async fn receive_create_comment(
-  create: Create,
-  client: &Client,
-  pool: &DbPool,
-  chat_server: ChatServerParam,
-) -> Result<HttpResponse, LemmyError> {
-  let note = create
-    .create_props
-    .get_object_base_box()
-    .to_owned()
-    .unwrap()
-    .to_owned()
-    .into_concrete::<Note>()?;
-
-  let user_uri = create
-    .create_props
-    .get_actor_xsd_any_uri()
-    .unwrap()
-    .to_string();
-
-  let user = get_or_fetch_and_upsert_remote_user(&user_uri, client, pool).await?;
-
-  insert_activity(user.id, create, false, pool).await?;
-
-  let comment = CommentForm::from_apub(&note, client, pool).await?;
-
-  let inserted_comment = blocking(pool, move |conn| Comment::create(conn, &comment)).await??;
-
-  let post_id = inserted_comment.post_id;
-  let post = blocking(pool, move |conn| Post::read(conn, post_id)).await??;
-
-  // Note:
-  // Although mentions could be gotten from the post tags (they are included there), or the ccs,
-  // Its much easier to scrape them from the comment body, since the API has to do that
-  // anyway.
-  let mentions = scrape_text_for_mentions(&inserted_comment.content);
-  let recipient_ids =
-    send_local_notifs(mentions, inserted_comment.clone(), user, post, pool).await?;
-
-  // Refetch the view
-  let comment_view = blocking(pool, move |conn| {
-    CommentView::read(conn, inserted_comment.id, None)
-  })
-  .await??;
-
-  let res = CommentResponse {
-    comment: comment_view,
-    recipient_ids,
-  };
-
-  chat_server.do_send(SendComment {
-    op: UserOperation::CreateComment,
-    comment: res,
-    my_id: None,
-  });
-
-  Ok(HttpResponse::Ok().finish())
-}
-
-async fn receive_update_post(
-  update: Update,
-  client: &Client,
-  pool: &DbPool,
-  chat_server: ChatServerParam,
-) -> Result<HttpResponse, LemmyError> {
-  let page = update
-    .update_props
-    .get_object_base_box()
-    .to_owned()
-    .unwrap()
-    .to_owned()
-    .into_concrete::<PageExt>()?;
-
-  let user_uri = update
-    .update_props
-    .get_actor_xsd_any_uri()
-    .unwrap()
-    .to_string();
-
-  let user = get_or_fetch_and_upsert_remote_user(&user_uri, client, pool).await?;
-
-  insert_activity(user.id, update, false, pool).await?;
-
-  let post = PostForm::from_apub(&page, client, pool).await?;
-
-  let post_id = get_or_fetch_and_insert_remote_post(&post.ap_id, client, pool)
-    .await?
-    .id;
-
-  blocking(pool, move |conn| Post::update(conn, post_id, &post)).await??;
-
-  // Refetch the view
-  let post_view = blocking(pool, move |conn| PostView::read(conn, post_id, None)).await??;
-
-  let res = PostResponse { post: post_view };
-
-  chat_server.do_send(SendPost {
-    op: UserOperation::EditPost,
-    post: res,
-    my_id: None,
-  });
-
-  Ok(HttpResponse::Ok().finish())
-}
-
-async fn receive_like_post(
-  like: Like,
-  client: &Client,
-  pool: &DbPool,
-  chat_server: ChatServerParam,
-) -> Result<HttpResponse, LemmyError> {
-  let page = like
-    .like_props
-    .get_object_base_box()
-    .to_owned()
-    .unwrap()
-    .to_owned()
-    .into_concrete::<PageExt>()?;
-
-  let user_uri = like.like_props.get_actor_xsd_any_uri().unwrap().to_string();
-
-  let user = get_or_fetch_and_upsert_remote_user(&user_uri, client, pool).await?;
-
-  insert_activity(user.id, like, false, pool).await?;
-
-  let post = PostForm::from_apub(&page, client, pool).await?;
-
-  let post_id = get_or_fetch_and_insert_remote_post(&post.ap_id, client, pool)
-    .await?
-    .id;
-
-  let like_form = PostLikeForm {
-    post_id,
-    user_id: user.id,
-    score: 1,
-  };
-  blocking(pool, move |conn| {
-    PostLike::remove(conn, &like_form)?;
-    PostLike::like(conn, &like_form)
-  })
-  .await??;
-
-  // Refetch the view
-  let post_view = blocking(pool, move |conn| PostView::read(conn, post_id, None)).await??;
-
-  let res = PostResponse { post: post_view };
-
-  chat_server.do_send(SendPost {
-    op: UserOperation::CreatePostLike,
-    post: res,
-    my_id: None,
-  });
-
-  Ok(HttpResponse::Ok().finish())
-}
-
-async fn receive_dislike_post(
-  dislike: Dislike,
-  client: &Client,
-  pool: &DbPool,
-  chat_server: ChatServerParam,
-) -> Result<HttpResponse, LemmyError> {
-  let page = dislike
-    .dislike_props
-    .get_object_base_box()
-    .to_owned()
-    .unwrap()
-    .to_owned()
-    .into_concrete::<PageExt>()?;
-
-  let user_uri = dislike
-    .dislike_props
-    .get_actor_xsd_any_uri()
-    .unwrap()
-    .to_string();
-
-  let user = get_or_fetch_and_upsert_remote_user(&user_uri, client, pool).await?;
-
-  insert_activity(user.id, dislike, false, pool).await?;
-
-  let post = PostForm::from_apub(&page, client, pool).await?;
-
-  let post_id = get_or_fetch_and_insert_remote_post(&post.ap_id, client, pool)
-    .await?
-    .id;
-
-  let like_form = PostLikeForm {
-    post_id,
-    user_id: user.id,
-    score: -1,
-  };
-  blocking(pool, move |conn| {
-    PostLike::remove(conn, &like_form)?;
-    PostLike::like(conn, &like_form)
-  })
-  .await??;
-
-  // Refetch the view
-  let post_view = blocking(pool, move |conn| PostView::read(conn, post_id, None)).await??;
-
-  let res = PostResponse { post: post_view };
-
-  chat_server.do_send(SendPost {
-    op: UserOperation::CreatePostLike,
-    post: res,
-    my_id: None,
-  });
-
-  Ok(HttpResponse::Ok().finish())
-}
-
-async fn receive_update_comment(
-  update: Update,
-  client: &Client,
-  pool: &DbPool,
-  chat_server: ChatServerParam,
-) -> Result<HttpResponse, LemmyError> {
-  let note = update
-    .update_props
-    .get_object_base_box()
-    .to_owned()
-    .unwrap()
-    .to_owned()
-    .into_concrete::<Note>()?;
-
-  let user_uri = update
-    .update_props
-    .get_actor_xsd_any_uri()
-    .unwrap()
-    .to_string();
-
-  let user = get_or_fetch_and_upsert_remote_user(&user_uri, client, pool).await?;
-
-  insert_activity(user.id, update, false, pool).await?;
-
-  let comment = CommentForm::from_apub(&note, client, pool).await?;
-
-  let comment_id = get_or_fetch_and_insert_remote_comment(&comment.ap_id, client, pool)
-    .await?
-    .id;
-
-  let updated_comment = blocking(pool, move |conn| {
-    Comment::update(conn, comment_id, &comment)
-  })
-  .await??;
-
-  let post_id = updated_comment.post_id;
-  let post = blocking(pool, move |conn| Post::read(conn, post_id)).await??;
-
-  let mentions = scrape_text_for_mentions(&updated_comment.content);
-  let recipient_ids = send_local_notifs(mentions, updated_comment, user, post, pool).await?;
-
-  // Refetch the view
-  let comment_view =
-    blocking(pool, move |conn| CommentView::read(conn, comment_id, None)).await??;
-
-  let res = CommentResponse {
-    comment: comment_view,
-    recipient_ids,
-  };
-
-  chat_server.do_send(SendComment {
-    op: UserOperation::EditComment,
-    comment: res,
-    my_id: None,
-  });
-
-  Ok(HttpResponse::Ok().finish())
-}
-
-async fn receive_like_comment(
-  like: Like,
-  client: &Client,
-  pool: &DbPool,
-  chat_server: ChatServerParam,
-) -> Result<HttpResponse, LemmyError> {
-  let note = like
-    .like_props
-    .get_object_base_box()
-    .to_owned()
-    .unwrap()
-    .to_owned()
-    .into_concrete::<Note>()?;
-
-  let user_uri = like.like_props.get_actor_xsd_any_uri().unwrap().to_string();
-
-  let user = get_or_fetch_and_upsert_remote_user(&user_uri, client, pool).await?;
-
-  insert_activity(user.id, like, false, pool).await?;
-
-  let comment = CommentForm::from_apub(&note, client, pool).await?;
-
-  let comment_id = get_or_fetch_and_insert_remote_comment(&comment.ap_id, client, pool)
-    .await?
-    .id;
-
-  let like_form = CommentLikeForm {
-    comment_id,
-    post_id: comment.post_id,
-    user_id: user.id,
-    score: 1,
-  };
-  blocking(pool, move |conn| {
-    CommentLike::remove(conn, &like_form)?;
-    CommentLike::like(conn, &like_form)
-  })
-  .await??;
-
-  // Refetch the view
-  let comment_view =
-    blocking(pool, move |conn| CommentView::read(conn, comment_id, None)).await??;
-
-  // TODO get those recipient actor ids from somewhere
-  let recipient_ids = vec![];
-  let res = CommentResponse {
-    comment: comment_view,
-    recipient_ids,
-  };
-
-  chat_server.do_send(SendComment {
-    op: UserOperation::CreateCommentLike,
-    comment: res,
-    my_id: None,
-  });
-
-  Ok(HttpResponse::Ok().finish())
-}
-
-async fn receive_dislike_comment(
-  dislike: Dislike,
-  client: &Client,
-  pool: &DbPool,
-  chat_server: ChatServerParam,
-) -> Result<HttpResponse, LemmyError> {
-  let note = dislike
-    .dislike_props
-    .get_object_base_box()
-    .to_owned()
-    .unwrap()
-    .to_owned()
-    .into_concrete::<Note>()?;
-
-  let user_uri = dislike
-    .dislike_props
-    .get_actor_xsd_any_uri()
-    .unwrap()
-    .to_string();
-
-  let user = get_or_fetch_and_upsert_remote_user(&user_uri, client, pool).await?;
-
-  insert_activity(user.id, dislike, false, pool).await?;
-
-  let comment = CommentForm::from_apub(&note, client, pool).await?;
-
-  let comment_id = get_or_fetch_and_insert_remote_comment(&comment.ap_id, client, pool)
-    .await?
-    .id;
-
-  let like_form = CommentLikeForm {
-    comment_id,
-    post_id: comment.post_id,
-    user_id: user.id,
-    score: -1,
-  };
-  blocking(pool, move |conn| {
-    CommentLike::remove(conn, &like_form)?;
-    CommentLike::like(conn, &like_form)
-  })
-  .await??;
-
-  // Refetch the view
-  let comment_view =
-    blocking(pool, move |conn| CommentView::read(conn, comment_id, None)).await??;
-
-  // TODO get those recipient actor ids from somewhere
-  let recipient_ids = vec![];
-  let res = CommentResponse {
-    comment: comment_view,
-    recipient_ids,
-  };
-
-  chat_server.do_send(SendComment {
-    op: UserOperation::CreateCommentLike,
-    comment: res,
-    my_id: None,
-  });
-
-  Ok(HttpResponse::Ok().finish())
-}
-
-async fn receive_delete_community(
-  delete: Delete,
-  client: &Client,
-  pool: &DbPool,
-  chat_server: ChatServerParam,
-) -> Result<HttpResponse, LemmyError> {
-  let user_uri = delete
-    .delete_props
-    .get_actor_xsd_any_uri()
-    .unwrap()
-    .to_string();
-
-  let group = delete
-    .delete_props
-    .get_object_base_box()
-    .to_owned()
-    .unwrap()
-    .to_owned()
-    .into_concrete::<GroupExt>()?;
-
-  let user = get_or_fetch_and_upsert_remote_user(&user_uri, client, pool).await?;
-
-  insert_activity(user.id, delete, false, pool).await?;
-
-  let community_actor_id = CommunityForm::from_apub(&group, client, pool)
-    .await?
-    .actor_id;
-
-  let community = blocking(pool, move |conn| {
-    Community::read_from_actor_id(conn, &community_actor_id)
-  })
-  .await??;
-
-  let community_form = CommunityForm {
-    name: community.name.to_owned(),
-    title: community.title.to_owned(),
-    description: community.description.to_owned(),
-    category_id: community.category_id, // Note: need to keep this due to foreign key constraint
-    creator_id: community.creator_id,   // Note: need to keep this due to foreign key constraint
-    removed: None,
-    published: None,
-    updated: Some(naive_now()),
-    deleted: Some(true),
-    nsfw: community.nsfw,
-    actor_id: community.actor_id,
-    local: community.local,
-    private_key: community.private_key,
-    public_key: community.public_key,
-    last_refreshed_at: None,
-  };
-
-  let community_id = community.id;
-  blocking(pool, move |conn| {
-    Community::update(conn, community_id, &community_form)
-  })
-  .await??;
-
-  let community_id = community.id;
-  let res = CommunityResponse {
-    community: blocking(pool, move |conn| {
-      CommunityView::read(conn, community_id, None)
-    })
-    .await??,
-  };
-
-  let community_id = res.community.id;
-
-  chat_server.do_send(SendCommunityRoomMessage {
-    op: UserOperation::EditCommunity,
-    response: res,
-    community_id,
-    my_id: None,
-  });
-
-  Ok(HttpResponse::Ok().finish())
-}
-
-async fn receive_remove_community(
-  remove: Remove,
-  client: &Client,
-  pool: &DbPool,
-  chat_server: ChatServerParam,
-) -> Result<HttpResponse, LemmyError> {
-  let mod_uri = remove
-    .remove_props
-    .get_actor_xsd_any_uri()
-    .unwrap()
-    .to_string();
-
-  let group = remove
-    .remove_props
-    .get_object_base_box()
-    .to_owned()
-    .unwrap()
-    .to_owned()
-    .into_concrete::<GroupExt>()?;
-
-  let mod_ = get_or_fetch_and_upsert_remote_user(&mod_uri, client, pool).await?;
-
-  insert_activity(mod_.id, remove, false, pool).await?;
-
-  let community_actor_id = CommunityForm::from_apub(&group, client, pool)
-    .await?
-    .actor_id;
-
-  let community = blocking(pool, move |conn| {
-    Community::read_from_actor_id(conn, &community_actor_id)
-  })
-  .await??;
-
-  let community_form = CommunityForm {
-    name: community.name.to_owned(),
-    title: community.title.to_owned(),
-    description: community.description.to_owned(),
-    category_id: community.category_id, // Note: need to keep this due to foreign key constraint
-    creator_id: community.creator_id,   // Note: need to keep this due to foreign key constraint
-    removed: Some(true),
-    published: None,
-    updated: Some(naive_now()),
-    deleted: None,
-    nsfw: community.nsfw,
-    actor_id: community.actor_id,
-    local: community.local,
-    private_key: community.private_key,
-    public_key: community.public_key,
-    last_refreshed_at: None,
-  };
-
-  let community_id = community.id;
-  blocking(pool, move |conn| {
-    Community::update(conn, community_id, &community_form)
-  })
-  .await??;
-
-  let community_id = community.id;
-  let res = CommunityResponse {
-    community: blocking(pool, move |conn| {
-      CommunityView::read(conn, community_id, None)
-    })
-    .await??,
-  };
-
-  let community_id = res.community.id;
-
-  chat_server.do_send(SendCommunityRoomMessage {
-    op: UserOperation::EditCommunity,
-    response: res,
-    community_id,
-    my_id: None,
-  });
-
-  Ok(HttpResponse::Ok().finish())
-}
-
-async fn receive_delete_post(
-  delete: Delete,
-  client: &Client,
-  pool: &DbPool,
-  chat_server: ChatServerParam,
-) -> Result<HttpResponse, LemmyError> {
-  let user_uri = delete
-    .delete_props
-    .get_actor_xsd_any_uri()
-    .unwrap()
-    .to_string();
-
-  let page = delete
-    .delete_props
-    .get_object_base_box()
-    .to_owned()
-    .unwrap()
-    .to_owned()
-    .into_concrete::<PageExt>()?;
-
-  let user = get_or_fetch_and_upsert_remote_user(&user_uri, client, pool).await?;
-
-  insert_activity(user.id, delete, false, pool).await?;
-
-  let post_ap_id = PostForm::from_apub(&page, client, pool).await?.ap_id;
-
-  let post = get_or_fetch_and_insert_remote_post(&post_ap_id, client, pool).await?;
-
-  let post_form = PostForm {
-    name: post.name.to_owned(),
-    url: post.url.to_owned(),
-    body: post.body.to_owned(),
-    creator_id: post.creator_id.to_owned(),
-    community_id: post.community_id,
-    removed: None,
-    deleted: Some(true),
-    nsfw: post.nsfw,
-    locked: None,
-    stickied: None,
-    updated: Some(naive_now()),
-    embed_title: post.embed_title,
-    embed_description: post.embed_description,
-    embed_html: post.embed_html,
-    thumbnail_url: post.thumbnail_url,
-    ap_id: post.ap_id,
-    local: post.local,
-    published: None,
-  };
-  let post_id = post.id;
-  blocking(pool, move |conn| Post::update(conn, post_id, &post_form)).await??;
-
-  // Refetch the view
-  let post_id = post.id;
-  let post_view = blocking(pool, move |conn| PostView::read(conn, post_id, None)).await??;
-
-  let res = PostResponse { post: post_view };
-
-  chat_server.do_send(SendPost {
-    op: UserOperation::EditPost,
-    post: res,
-    my_id: None,
-  });
-
-  Ok(HttpResponse::Ok().finish())
-}
-
-async fn receive_remove_post(
-  remove: Remove,
-  client: &Client,
-  pool: &DbPool,
-  chat_server: ChatServerParam,
-) -> Result<HttpResponse, LemmyError> {
-  let mod_uri = remove
-    .remove_props
-    .get_actor_xsd_any_uri()
-    .unwrap()
-    .to_string();
-
-  let page = remove
-    .remove_props
-    .get_object_base_box()
-    .to_owned()
-    .unwrap()
-    .to_owned()
-    .into_concrete::<PageExt>()?;
-
-  let mod_ = get_or_fetch_and_upsert_remote_user(&mod_uri, client, pool).await?;
-
-  insert_activity(mod_.id, remove, false, pool).await?;
-
-  let post_ap_id = PostForm::from_apub(&page, client, pool).await?.ap_id;
-
-  let post = get_or_fetch_and_insert_remote_post(&post_ap_id, client, pool).await?;
-
-  let post_form = PostForm {
-    name: post.name.to_owned(),
-    url: post.url.to_owned(),
-    body: post.body.to_owned(),
-    creator_id: post.creator_id.to_owned(),
-    community_id: post.community_id,
-    removed: Some(true),
-    deleted: None,
-    nsfw: post.nsfw,
-    locked: None,
-    stickied: None,
-    updated: Some(naive_now()),
-    embed_title: post.embed_title,
-    embed_description: post.embed_description,
-    embed_html: post.embed_html,
-    thumbnail_url: post.thumbnail_url,
-    ap_id: post.ap_id,
-    local: post.local,
-    published: None,
-  };
-  let post_id = post.id;
-  blocking(pool, move |conn| Post::update(conn, post_id, &post_form)).await??;
-
-  // Refetch the view
-  let post_id = post.id;
-  let post_view = blocking(pool, move |conn| PostView::read(conn, post_id, None)).await??;
-
-  let res = PostResponse { post: post_view };
-
-  chat_server.do_send(SendPost {
-    op: UserOperation::EditPost,
-    post: res,
-    my_id: None,
-  });
-
-  Ok(HttpResponse::Ok().finish())
-}
-
-async fn receive_delete_comment(
-  delete: Delete,
-  client: &Client,
-  pool: &DbPool,
-  chat_server: ChatServerParam,
-) -> Result<HttpResponse, LemmyError> {
-  let user_uri = delete
-    .delete_props
-    .get_actor_xsd_any_uri()
-    .unwrap()
-    .to_string();
-
-  let note = delete
-    .delete_props
-    .get_object_base_box()
-    .to_owned()
-    .unwrap()
-    .to_owned()
-    .into_concrete::<Note>()?;
-
-  let user = get_or_fetch_and_upsert_remote_user(&user_uri, client, pool).await?;
-
-  insert_activity(user.id, delete, false, pool).await?;
-
-  let comment_ap_id = CommentForm::from_apub(&note, client, pool).await?.ap_id;
-
-  let comment = get_or_fetch_and_insert_remote_comment(&comment_ap_id, client, pool).await?;
-
-  let comment_form = CommentForm {
-    content: comment.content.to_owned(),
-    parent_id: comment.parent_id,
-    post_id: comment.post_id,
-    creator_id: comment.creator_id,
-    removed: None,
-    deleted: Some(true),
-    read: None,
-    published: None,
-    updated: Some(naive_now()),
-    ap_id: comment.ap_id,
-    local: comment.local,
-  };
-  let comment_id = comment.id;
-  blocking(pool, move |conn| {
-    Comment::update(conn, comment_id, &comment_form)
-  })
-  .await??;
-
-  // Refetch the view
-  let comment_id = comment.id;
-  let comment_view =
-    blocking(pool, move |conn| CommentView::read(conn, comment_id, None)).await??;
-
-  // TODO get those recipient actor ids from somewhere
-  let recipient_ids = vec![];
-  let res = CommentResponse {
-    comment: comment_view,
-    recipient_ids,
-  };
-
-  chat_server.do_send(SendComment {
-    op: UserOperation::EditComment,
-    comment: res,
-    my_id: None,
-  });
-
-  Ok(HttpResponse::Ok().finish())
-}
-
-async fn receive_remove_comment(
-  remove: Remove,
-  client: &Client,
-  pool: &DbPool,
-  chat_server: ChatServerParam,
-) -> Result<HttpResponse, LemmyError> {
-  let mod_uri = remove
-    .remove_props
-    .get_actor_xsd_any_uri()
-    .unwrap()
-    .to_string();
-
-  let note = remove
-    .remove_props
-    .get_object_base_box()
-    .to_owned()
-    .unwrap()
-    .to_owned()
-    .into_concrete::<Note>()?;
-
-  let mod_ = get_or_fetch_and_upsert_remote_user(&mod_uri, client, pool).await?;
-
-  insert_activity(mod_.id, remove, false, pool).await?;
-
-  let comment_ap_id = CommentForm::from_apub(&note, client, pool).await?.ap_id;
-
-  let comment = get_or_fetch_and_insert_remote_comment(&comment_ap_id, client, pool).await?;
-
-  let comment_form = CommentForm {
-    content: comment.content.to_owned(),
-    parent_id: comment.parent_id,
-    post_id: comment.post_id,
-    creator_id: comment.creator_id,
-    removed: Some(true),
-    deleted: None,
-    read: None,
-    published: None,
-    updated: Some(naive_now()),
-    ap_id: comment.ap_id,
-    local: comment.local,
-  };
-  let comment_id = comment.id;
-  blocking(pool, move |conn| {
-    Comment::update(conn, comment_id, &comment_form)
-  })
-  .await??;
-
-  // Refetch the view
-  let comment_id = comment.id;
-  let comment_view =
-    blocking(pool, move |conn| CommentView::read(conn, comment_id, None)).await??;
-
-  // TODO get those recipient actor ids from somewhere
-  let recipient_ids = vec![];
-  let res = CommentResponse {
-    comment: comment_view,
-    recipient_ids,
-  };
-
-  chat_server.do_send(SendComment {
-    op: UserOperation::EditComment,
-    comment: res,
-    my_id: None,
-  });
-
-  Ok(HttpResponse::Ok().finish())
-}
-
-async fn receive_undo_delete(
-  undo: Undo,
-  client: &Client,
-  pool: &DbPool,
-  chat_server: ChatServerParam,
-) -> Result<HttpResponse, LemmyError> {
-  let delete = undo
-    .undo_props
-    .get_object_base_box()
-    .to_owned()
-    .unwrap()
-    .to_owned()
-    .into_concrete::<Delete>()?;
-
-  let type_ = delete
-    .delete_props
-    .get_object_base_box()
-    .to_owned()
-    .unwrap()
-    .kind()
-    .unwrap();
-
-  match type_ {
-    "Note" => receive_undo_delete_comment(delete, client, pool, chat_server).await,
-    "Page" => receive_undo_delete_post(delete, client, pool, chat_server).await,
-    "Group" => receive_undo_delete_community(delete, client, pool, chat_server).await,
-    d => Err(format_err!("Undo Delete type {} not supported", d).into()),
-  }
-}
-
-async fn receive_undo_remove(
-  undo: Undo,
-  client: &Client,
-  pool: &DbPool,
-  chat_server: ChatServerParam,
-) -> Result<HttpResponse, LemmyError> {
-  let remove = undo
-    .undo_props
-    .get_object_base_box()
-    .to_owned()
-    .unwrap()
-    .to_owned()
-    .into_concrete::<Remove>()?;
-
-  let type_ = remove
-    .remove_props
-    .get_object_base_box()
-    .to_owned()
-    .unwrap()
-    .kind()
-    .unwrap();
-
-  match type_ {
-    "Note" => receive_undo_remove_comment(remove, client, pool, chat_server).await,
-    "Page" => receive_undo_remove_post(remove, client, pool, chat_server).await,
-    "Group" => receive_undo_remove_community(remove, client, pool, chat_server).await,
-    d => Err(format_err!("Undo Delete type {} not supported", d).into()),
-  }
-}
-
-async fn receive_undo_delete_comment(
-  delete: Delete,
-  client: &Client,
-  pool: &DbPool,
-  chat_server: ChatServerParam,
-) -> Result<HttpResponse, LemmyError> {
-  let user_uri = delete
-    .delete_props
-    .get_actor_xsd_any_uri()
-    .unwrap()
-    .to_string();
-
-  let note = delete
-    .delete_props
-    .get_object_base_box()
-    .to_owned()
-    .unwrap()
-    .to_owned()
-    .into_concrete::<Note>()?;
-
-  let user = get_or_fetch_and_upsert_remote_user(&user_uri, client, pool).await?;
-
-  insert_activity(user.id, delete, false, pool).await?;
-
-  let comment_ap_id = CommentForm::from_apub(&note, client, pool).await?.ap_id;
-
-  let comment = get_or_fetch_and_insert_remote_comment(&comment_ap_id, client, pool).await?;
-
-  let comment_form = CommentForm {
-    content: comment.content.to_owned(),
-    parent_id: comment.parent_id,
-    post_id: comment.post_id,
-    creator_id: comment.creator_id,
-    removed: None,
-    deleted: Some(false),
-    read: None,
-    published: None,
-    updated: Some(naive_now()),
-    ap_id: comment.ap_id,
-    local: comment.local,
-  };
-  let comment_id = comment.id;
-  blocking(pool, move |conn| {
-    Comment::update(conn, comment_id, &comment_form)
-  })
-  .await??;
-
-  // Refetch the view
-  let comment_id = comment.id;
-  let comment_view =
-    blocking(pool, move |conn| CommentView::read(conn, comment_id, None)).await??;
-
-  // TODO get those recipient actor ids from somewhere
-  let recipient_ids = vec![];
-  let res = CommentResponse {
-    comment: comment_view,
-    recipient_ids,
-  };
-
-  chat_server.do_send(SendComment {
-    op: UserOperation::EditComment,
-    comment: res,
-    my_id: None,
-  });
-
-  Ok(HttpResponse::Ok().finish())
-}
-
-async fn receive_undo_remove_comment(
-  remove: Remove,
-  client: &Client,
-  pool: &DbPool,
-  chat_server: ChatServerParam,
-) -> Result<HttpResponse, LemmyError> {
-  let mod_uri = remove
-    .remove_props
-    .get_actor_xsd_any_uri()
-    .unwrap()
-    .to_string();
-
-  let note = remove
-    .remove_props
-    .get_object_base_box()
-    .to_owned()
-    .unwrap()
-    .to_owned()
-    .into_concrete::<Note>()?;
-
-  let mod_ = get_or_fetch_and_upsert_remote_user(&mod_uri, client, pool).await?;
-
-  insert_activity(mod_.id, remove, false, pool).await?;
-
-  let comment_ap_id = CommentForm::from_apub(&note, client, pool).await?.ap_id;
-
-  let comment = get_or_fetch_and_insert_remote_comment(&comment_ap_id, client, pool).await?;
-
-  let comment_form = CommentForm {
-    content: comment.content.to_owned(),
-    parent_id: comment.parent_id,
-    post_id: comment.post_id,
-    creator_id: comment.creator_id,
-    removed: Some(false),
-    deleted: None,
-    read: None,
-    published: None,
-    updated: Some(naive_now()),
-    ap_id: comment.ap_id,
-    local: comment.local,
-  };
-  let comment_id = comment.id;
-  blocking(pool, move |conn| {
-    Comment::update(conn, comment_id, &comment_form)
-  })
-  .await??;
-
-  // Refetch the view
-  let comment_id = comment.id;
-  let comment_view =
-    blocking(pool, move |conn| CommentView::read(conn, comment_id, None)).await??;
-
-  // TODO get those recipient actor ids from somewhere
-  let recipient_ids = vec![];
-  let res = CommentResponse {
-    comment: comment_view,
-    recipient_ids,
-  };
-
-  chat_server.do_send(SendComment {
-    op: UserOperation::EditComment,
-    comment: res,
-    my_id: None,
-  });
-
-  Ok(HttpResponse::Ok().finish())
-}
-
-async fn receive_undo_delete_post(
-  delete: Delete,
-  client: &Client,
-  pool: &DbPool,
-  chat_server: ChatServerParam,
-) -> Result<HttpResponse, LemmyError> {
-  let user_uri = delete
-    .delete_props
-    .get_actor_xsd_any_uri()
-    .unwrap()
-    .to_string();
-
-  let page = delete
-    .delete_props
-    .get_object_base_box()
-    .to_owned()
-    .unwrap()
-    .to_owned()
-    .into_concrete::<PageExt>()?;
-
-  let user = get_or_fetch_and_upsert_remote_user(&user_uri, client, pool).await?;
-
-  insert_activity(user.id, delete, false, pool).await?;
-
-  let post_ap_id = PostForm::from_apub(&page, client, pool).await?.ap_id;
-
-  let post = get_or_fetch_and_insert_remote_post(&post_ap_id, client, pool).await?;
-
-  let post_form = PostForm {
-    name: post.name.to_owned(),
-    url: post.url.to_owned(),
-    body: post.body.to_owned(),
-    creator_id: post.creator_id.to_owned(),
-    community_id: post.community_id,
-    removed: None,
-    deleted: Some(false),
-    nsfw: post.nsfw,
-    locked: None,
-    stickied: None,
-    updated: Some(naive_now()),
-    embed_title: post.embed_title,
-    embed_description: post.embed_description,
-    embed_html: post.embed_html,
-    thumbnail_url: post.thumbnail_url,
-    ap_id: post.ap_id,
-    local: post.local,
-    published: None,
-  };
-  let post_id = post.id;
-  blocking(pool, move |conn| Post::update(conn, post_id, &post_form)).await??;
-
-  // Refetch the view
-  let post_id = post.id;
-  let post_view = blocking(pool, move |conn| PostView::read(conn, post_id, None)).await??;
-
-  let res = PostResponse { post: post_view };
-
-  chat_server.do_send(SendPost {
-    op: UserOperation::EditPost,
-    post: res,
-    my_id: None,
-  });
-
-  Ok(HttpResponse::Ok().finish())
-}
-
-async fn receive_undo_remove_post(
-  remove: Remove,
-  client: &Client,
-  pool: &DbPool,
-  chat_server: ChatServerParam,
-) -> Result<HttpResponse, LemmyError> {
-  let mod_uri = remove
-    .remove_props
-    .get_actor_xsd_any_uri()
-    .unwrap()
-    .to_string();
-
-  let page = remove
-    .remove_props
-    .get_object_base_box()
-    .to_owned()
-    .unwrap()
-    .to_owned()
-    .into_concrete::<PageExt>()?;
-
-  let mod_ = get_or_fetch_and_upsert_remote_user(&mod_uri, client, pool).await?;
-
-  insert_activity(mod_.id, remove, false, pool).await?;
-
-  let post_ap_id = PostForm::from_apub(&page, client, pool).await?.ap_id;
-
-  let post = get_or_fetch_and_insert_remote_post(&post_ap_id, client, pool).await?;
-
-  let post_form = PostForm {
-    name: post.name.to_owned(),
-    url: post.url.to_owned(),
-    body: post.body.to_owned(),
-    creator_id: post.creator_id.to_owned(),
-    community_id: post.community_id,
-    removed: Some(false),
-    deleted: None,
-    nsfw: post.nsfw,
-    locked: None,
-    stickied: None,
-    updated: Some(naive_now()),
-    embed_title: post.embed_title,
-    embed_description: post.embed_description,
-    embed_html: post.embed_html,
-    thumbnail_url: post.thumbnail_url,
-    ap_id: post.ap_id,
-    local: post.local,
-    published: None,
-  };
-  let post_id = post.id;
-  blocking(pool, move |conn| Post::update(conn, post_id, &post_form)).await??;
-
-  // Refetch the view
-  let post_id = post.id;
-  let post_view = blocking(pool, move |conn| PostView::read(conn, post_id, None)).await??;
-
-  let res = PostResponse { post: post_view };
-
-  chat_server.do_send(SendPost {
-    op: UserOperation::EditPost,
-    post: res,
-    my_id: None,
-  });
-
-  Ok(HttpResponse::Ok().finish())
-}
-
-async fn receive_undo_delete_community(
-  delete: Delete,
-  client: &Client,
-  pool: &DbPool,
-  chat_server: ChatServerParam,
-) -> Result<HttpResponse, LemmyError> {
-  let user_uri = delete
-    .delete_props
-    .get_actor_xsd_any_uri()
-    .unwrap()
-    .to_string();
-
-  let group = delete
-    .delete_props
-    .get_object_base_box()
-    .to_owned()
-    .unwrap()
-    .to_owned()
-    .into_concrete::<GroupExt>()?;
-
-  let user = get_or_fetch_and_upsert_remote_user(&user_uri, client, pool).await?;
-
-  insert_activity(user.id, delete, false, pool).await?;
-
-  let community_actor_id = CommunityForm::from_apub(&group, client, pool)
-    .await?
-    .actor_id;
-
-  let community = blocking(pool, move |conn| {
-    Community::read_from_actor_id(conn, &community_actor_id)
-  })
-  .await??;
-
-  let community_form = CommunityForm {
-    name: community.name.to_owned(),
-    title: community.title.to_owned(),
-    description: community.description.to_owned(),
-    category_id: community.category_id, // Note: need to keep this due to foreign key constraint
-    creator_id: community.creator_id,   // Note: need to keep this due to foreign key constraint
-    removed: None,
-    published: None,
-    updated: Some(naive_now()),
-    deleted: Some(false),
-    nsfw: community.nsfw,
-    actor_id: community.actor_id,
-    local: community.local,
-    private_key: community.private_key,
-    public_key: community.public_key,
-    last_refreshed_at: None,
-  };
-
-  let community_id = community.id;
-  blocking(pool, move |conn| {
-    Community::update(conn, community_id, &community_form)
-  })
-  .await??;
-
-  let community_id = community.id;
-  let res = CommunityResponse {
-    community: blocking(pool, move |conn| {
-      CommunityView::read(conn, community_id, None)
-    })
-    .await??,
-  };
-
-  let community_id = res.community.id;
-
-  chat_server.do_send(SendCommunityRoomMessage {
-    op: UserOperation::EditCommunity,
-    response: res,
-    community_id,
-    my_id: None,
-  });
-
-  Ok(HttpResponse::Ok().finish())
-}
-
-async fn receive_undo_remove_community(
-  remove: Remove,
-  client: &Client,
-  pool: &DbPool,
-  chat_server: ChatServerParam,
-) -> Result<HttpResponse, LemmyError> {
-  let mod_uri = remove
-    .remove_props
-    .get_actor_xsd_any_uri()
-    .unwrap()
-    .to_string();
-
-  let group = remove
-    .remove_props
-    .get_object_base_box()
-    .to_owned()
-    .unwrap()
-    .to_owned()
-    .into_concrete::<GroupExt>()?;
-
-  let mod_ = get_or_fetch_and_upsert_remote_user(&mod_uri, client, pool).await?;
-
-  insert_activity(mod_.id, remove, false, pool).await?;
-
-  let community_actor_id = CommunityForm::from_apub(&group, client, pool)
-    .await?
-    .actor_id;
-
-  let community = blocking(pool, move |conn| {
-    Community::read_from_actor_id(conn, &community_actor_id)
-  })
-  .await??;
-
-  let community_form = CommunityForm {
-    name: community.name.to_owned(),
-    title: community.title.to_owned(),
-    description: community.description.to_owned(),
-    category_id: community.category_id, // Note: need to keep this due to foreign key constraint
-    creator_id: community.creator_id,   // Note: need to keep this due to foreign key constraint
-    removed: Some(false),
-    published: None,
-    updated: Some(naive_now()),
-    deleted: None,
-    nsfw: community.nsfw,
-    actor_id: community.actor_id,
-    local: community.local,
-    private_key: community.private_key,
-    public_key: community.public_key,
-    last_refreshed_at: None,
-  };
-
-  let community_id = community.id;
-  blocking(pool, move |conn| {
-    Community::update(conn, community_id, &community_form)
-  })
-  .await??;
-
-  let community_id = community.id;
-  let res = CommunityResponse {
-    community: blocking(pool, move |conn| {
-      CommunityView::read(conn, community_id, None)
-    })
-    .await??,
-  };
-
-  let community_id = res.community.id;
-
-  chat_server.do_send(SendCommunityRoomMessage {
-    op: UserOperation::EditCommunity,
-    response: res,
-    community_id,
-    my_id: None,
-  });
-
-  Ok(HttpResponse::Ok().finish())
-}
-
-async fn receive_undo_like(
-  undo: Undo,
-  client: &Client,
-  pool: &DbPool,
-  chat_server: ChatServerParam,
-) -> Result<HttpResponse, LemmyError> {
-  let like = undo
-    .undo_props
-    .get_object_base_box()
-    .to_owned()
-    .unwrap()
-    .to_owned()
-    .into_concrete::<Like>()?;
-
-  let type_ = like
-    .like_props
-    .get_object_base_box()
-    .to_owned()
-    .unwrap()
-    .kind()
-    .unwrap();
-
-  match type_ {
-    "Note" => receive_undo_like_comment(like, client, pool, chat_server).await,
-    "Page" => receive_undo_like_post(like, client, pool, chat_server).await,
-    d => Err(format_err!("Undo Delete type {} not supported", d).into()),
-  }
-}
-
-async fn receive_undo_like_comment(
-  like: Like,
-  client: &Client,
-  pool: &DbPool,
-  chat_server: ChatServerParam,
-) -> Result<HttpResponse, LemmyError> {
-  let note = like
-    .like_props
-    .get_object_base_box()
-    .to_owned()
-    .unwrap()
-    .to_owned()
-    .into_concrete::<Note>()?;
-
-  let user_uri = like.like_props.get_actor_xsd_any_uri().unwrap().to_string();
-
-  let user = get_or_fetch_and_upsert_remote_user(&user_uri, client, pool).await?;
-
-  insert_activity(user.id, like, false, pool).await?;
-
-  let comment = CommentForm::from_apub(&note, client, pool).await?;
-
-  let comment_id = get_or_fetch_and_insert_remote_comment(&comment.ap_id, client, pool)
-    .await?
-    .id;
-
-  let like_form = CommentLikeForm {
-    comment_id,
-    post_id: comment.post_id,
-    user_id: user.id,
-    score: 0,
-  };
-  blocking(pool, move |conn| CommentLike::remove(conn, &like_form)).await??;
-
-  // Refetch the view
-  let comment_view =
-    blocking(pool, move |conn| CommentView::read(conn, comment_id, None)).await??;
-
-  // TODO get those recipient actor ids from somewhere
-  let recipient_ids = vec![];
-  let res = CommentResponse {
-    comment: comment_view,
-    recipient_ids,
-  };
-
-  chat_server.do_send(SendComment {
-    op: UserOperation::CreateCommentLike,
-    comment: res,
-    my_id: None,
-  });
-
-  Ok(HttpResponse::Ok().finish())
-}
-
-async fn receive_undo_like_post(
-  like: Like,
-  client: &Client,
-  pool: &DbPool,
-  chat_server: ChatServerParam,
-) -> Result<HttpResponse, LemmyError> {
-  let page = like
-    .like_props
-    .get_object_base_box()
-    .to_owned()
-    .unwrap()
-    .to_owned()
-    .into_concrete::<PageExt>()?;
-
-  let user_uri = like.like_props.get_actor_xsd_any_uri().unwrap().to_string();
-
-  let user = get_or_fetch_and_upsert_remote_user(&user_uri, client, pool).await?;
-
-  insert_activity(user.id, like, false, pool).await?;
-
-  let post = PostForm::from_apub(&page, client, pool).await?;
-
-  let post_id = get_or_fetch_and_insert_remote_post(&post.ap_id, client, pool)
-    .await?
-    .id;
-
-  let like_form = PostLikeForm {
-    post_id,
-    user_id: user.id,
-    score: 1,
-  };
-  blocking(pool, move |conn| PostLike::remove(conn, &like_form)).await??;
-
-  // Refetch the view
-  let post_view = blocking(pool, move |conn| PostView::read(conn, post_id, None)).await??;
-
-  let res = PostResponse { post: post_view };
-
-  chat_server.do_send(SendPost {
-    op: UserOperation::CreatePostLike,
-    post: res,
-    my_id: None,
-  });
-
-  Ok(HttpResponse::Ok().finish())
-}
index 2b02486d17a7a4b43962fd7932141266bdf4f619..f6225dea2f5a30df30ead134c07ad8b286b14049 100644 (file)
@@ -1,8 +1,10 @@
 use crate::{
-  api::claims::Claims,
+  api::{check_slurs, check_slurs_opt},
   apub::{
-    activities::send_activity,
+    activities::{generate_activity_id, send_activity},
+    check_actor_domain,
     create_apub_response,
+    fetcher::get_or_fetch_and_upsert_actor,
     insert_activity,
     ActorType,
     FromApub,
@@ -10,27 +12,30 @@ use crate::{
     ToApub,
   },
   blocking,
-  routes::DbPoolParam,
   DbPool,
+  LemmyContext,
   LemmyError,
 };
-use activitystreams_ext::Ext1;
-use activitystreams_new::{
-  activity::{Follow, Undo},
+use activitystreams::{
+  activity::{
+    kind::{FollowType, UndoType},
+    Follow,
+    Undo,
+  },
   actor::{ApActor, Endpoints, Person},
-  context,
   object::{Image, Tombstone},
   prelude::*,
-  primitives::{XsdAnyUri, XsdDateTime},
 };
-use actix_web::{body::Body, client::Client, web, HttpResponse};
-use failure::_core::str::FromStr;
+use activitystreams_ext::Ext1;
+use actix_web::{body::Body, web, HttpResponse};
+use anyhow::Context;
 use lemmy_db::{
   naive_now,
   user::{UserForm, User_},
 };
-use lemmy_utils::convert_datetime;
+use lemmy_utils::{convert_datetime, location_info};
 use serde::Deserialize;
+use url::Url;
 
 #[derive(Deserialize)]
 pub struct UserQuery {
@@ -46,13 +51,13 @@ impl ToApub for User_ {
     // TODO go through all these to_string and to_owned()
     let mut person = Person::new();
     person
-      .set_context(context())
-      .set_id(XsdAnyUri::from_str(&self.actor_id)?)
+      .set_context(activitystreams::context())
+      .set_id(Url::parse(&self.actor_id)?)
       .set_name(self.name.to_owned())
-      .set_published(XsdDateTime::from(convert_datetime(self.published)));
+      .set_published(convert_datetime(self.published));
 
     if let Some(u) = self.updated {
-      person.set_updated(XsdDateTime::from(convert_datetime(u)));
+      person.set_updated(convert_datetime(u));
     }
 
     if let Some(avatar_url) = &self.avatar {
@@ -61,14 +66,24 @@ impl ToApub for User_ {
       person.set_icon(image.into_any_base()?);
     }
 
-    let mut ap_actor = ApActor::new(self.get_inbox_url().parse()?, person);
+    if let Some(banner_url) = &self.banner {
+      let mut image = Image::new();
+      image.set_url(banner_url.to_owned());
+      person.set_image(image.into_any_base()?);
+    }
+
+    if let Some(bio) = &self.bio {
+      person.set_summary(bio.to_owned());
+    }
+
+    let mut ap_actor = ApActor::new(self.get_inbox_url()?, person);
     ap_actor
-      .set_outbox(self.get_outbox_url().parse()?)
-      .set_followers(self.get_followers_url().parse()?)
+      .set_outbox(self.get_outbox_url()?)
+      .set_followers(self.get_followers_url()?)
       .set_following(self.get_following_url().parse()?)
       .set_liked(self.get_liked_url().parse()?)
       .set_endpoints(Endpoints {
-        shared_inbox: Some(self.get_shared_inbox_url().parse()?),
+        shared_inbox: Some(self.get_shared_inbox_url()?),
         ..Default::default()
       });
 
@@ -76,7 +91,7 @@ impl ToApub for User_ {
       ap_actor.set_preferred_username(i.to_owned());
     }
 
-    Ok(Ext1::new(ap_actor, self.get_public_key_ext()))
+    Ok(Ext1::new(ap_actor, self.get_public_key_ext()?))
   }
   fn to_tombstone(&self) -> Result<Tombstone, LemmyError> {
     unimplemented!()
@@ -85,142 +100,166 @@ impl ToApub for User_ {
 
 #[async_trait::async_trait(?Send)]
 impl ActorType for User_ {
-  fn actor_id(&self) -> String {
+  fn actor_id_str(&self) -> String {
     self.actor_id.to_owned()
   }
 
-  fn public_key(&self) -> String {
-    self.public_key.to_owned().unwrap()
+  fn public_key(&self) -> Option<String> {
+    self.public_key.to_owned()
   }
 
-  fn private_key(&self) -> String {
-    self.private_key.to_owned().unwrap()
+  fn private_key(&self) -> Option<String> {
+    self.private_key.to_owned()
   }
 
   /// As a given local user, send out a follow request to a remote community.
   async fn send_follow(
     &self,
-    follow_actor_id: &str,
-    client: &Client,
-    pool: &DbPool,
+    follow_actor_id: &Url,
+    context: &LemmyContext,
   ) -> Result<(), LemmyError> {
-    let id = format!("{}/follow/{}", self.actor_id, uuid::Uuid::new_v4());
-    let mut follow = Follow::new(self.actor_id.to_owned(), follow_actor_id);
-    follow.set_context(context()).set_id(id.parse()?);
-    let to = format!("{}/inbox", follow_actor_id);
+    let mut follow = Follow::new(self.actor_id.to_owned(), follow_actor_id.as_str());
+    follow
+      .set_context(activitystreams::context())
+      .set_id(generate_activity_id(FollowType::Follow)?);
+    let follow_actor = get_or_fetch_and_upsert_actor(follow_actor_id, context).await?;
+    let to = follow_actor.get_inbox_url()?;
 
-    insert_activity(self.id, follow.clone(), true, pool).await?;
+    insert_activity(self.id, follow.clone(), true, context.pool()).await?;
 
-    send_activity(client, &follow, self, vec![to]).await?;
+    send_activity(context.client(), &follow.into_any_base()?, self, vec![to]).await?;
     Ok(())
   }
 
   async fn send_unfollow(
     &self,
-    follow_actor_id: &str,
-    client: &Client,
-    pool: &DbPool,
+    follow_actor_id: &Url,
+    context: &LemmyContext,
   ) -> Result<(), LemmyError> {
-    let id = format!("{}/follow/{}", self.actor_id, uuid::Uuid::new_v4());
-    let mut follow = Follow::new(self.actor_id.to_owned(), follow_actor_id);
-    follow.set_context(context()).set_id(id.parse()?);
+    let mut follow = Follow::new(self.actor_id.to_owned(), follow_actor_id.as_str());
+    follow
+      .set_context(activitystreams::context())
+      .set_id(generate_activity_id(FollowType::Follow)?);
+    let follow_actor = get_or_fetch_and_upsert_actor(follow_actor_id, context).await?;
 
-    let to = format!("{}/inbox", follow_actor_id);
+    let to = follow_actor.get_inbox_url()?;
 
-    // TODO
     // Undo that fake activity
-    let undo_id = format!("{}/undo/follow/{}", self.actor_id, uuid::Uuid::new_v4());
-    let mut undo = Undo::new(self.actor_id.parse::<XsdAnyUri>()?, follow.into_any_base()?);
-    undo.set_context(context()).set_id(undo_id.parse()?);
+    let mut undo = Undo::new(Url::parse(&self.actor_id)?, follow.into_any_base()?);
+    undo
+      .set_context(activitystreams::context())
+      .set_id(generate_activity_id(UndoType::Undo)?);
 
-    insert_activity(self.id, undo.clone(), true, pool).await?;
+    insert_activity(self.id, undo.clone(), true, context.pool()).await?;
 
-    send_activity(client, &undo, self, vec![to]).await?;
+    send_activity(context.client(), &undo.into_any_base()?, self, vec![to]).await?;
     Ok(())
   }
 
-  async fn send_delete(
-    &self,
-    _creator: &User_,
-    _client: &Client,
-    _pool: &DbPool,
-  ) -> Result<(), LemmyError> {
+  async fn send_delete(&self, _creator: &User_, _context: &LemmyContext) -> Result<(), LemmyError> {
     unimplemented!()
   }
 
   async fn send_undo_delete(
     &self,
     _creator: &User_,
-    _client: &Client,
-    _pool: &DbPool,
+    _context: &LemmyContext,
   ) -> Result<(), LemmyError> {
     unimplemented!()
   }
 
-  async fn send_remove(
-    &self,
-    _creator: &User_,
-    _client: &Client,
-    _pool: &DbPool,
-  ) -> Result<(), LemmyError> {
+  async fn send_remove(&self, _creator: &User_, _context: &LemmyContext) -> Result<(), LemmyError> {
     unimplemented!()
   }
 
   async fn send_undo_remove(
     &self,
     _creator: &User_,
-    _client: &Client,
-    _pool: &DbPool,
+    _context: &LemmyContext,
   ) -> Result<(), LemmyError> {
     unimplemented!()
   }
 
   async fn send_accept_follow(
     &self,
-    _follow: &Follow,
-    _client: &Client,
-    _pool: &DbPool,
+    _follow: Follow,
+    _context: &LemmyContext,
   ) -> Result<(), LemmyError> {
     unimplemented!()
   }
 
-  async fn get_follower_inboxes(&self, _pool: &DbPool) -> Result<Vec<String>, LemmyError> {
+  async fn get_follower_inboxes(&self, _pool: &DbPool) -> Result<Vec<Url>, LemmyError> {
     unimplemented!()
   }
+
+  fn user_id(&self) -> i32 {
+    self.id
+  }
 }
 
 #[async_trait::async_trait(?Send)]
 impl FromApub for UserForm {
   type ApubType = PersonExt;
   /// Parse an ActivityPub person received from another instance into a Lemmy user.
-  async fn from_apub(person: &PersonExt, _: &Client, _: &DbPool) -> Result<Self, LemmyError> {
+  async fn from_apub(
+    person: &PersonExt,
+    _context: &LemmyContext,
+    expected_domain: Option<Url>,
+  ) -> Result<Self, LemmyError> {
     let avatar = match person.icon() {
-      Some(any_image) => Image::from_any_base(any_image.as_one().unwrap().clone())
-        .unwrap()
-        .unwrap()
-        .url
-        .unwrap()
-        .as_single_xsd_any_uri()
-        .map(|u| u.to_string()),
+      Some(any_image) => Some(
+        Image::from_any_base(any_image.as_one().context(location_info!())?.clone())?
+          .context(location_info!())?
+          .url()
+          .context(location_info!())?
+          .as_single_xsd_any_uri()
+          .map(|u| u.to_string()),
+      ),
       None => None,
     };
 
+    let banner = match person.image() {
+      Some(any_image) => Some(
+        Image::from_any_base(any_image.as_one().context(location_info!())?.clone())
+          .context(location_info!())?
+          .context(location_info!())?
+          .url()
+          .context(location_info!())?
+          .as_single_xsd_any_uri()
+          .map(|u| u.to_string()),
+      ),
+      None => None,
+    };
+
+    let name = person
+      .name()
+      .context(location_info!())?
+      .one()
+      .context(location_info!())?
+      .as_xsd_string()
+      .context(location_info!())?
+      .to_string();
+    let preferred_username = person.inner.preferred_username().map(|u| u.to_string());
+    let bio = person
+      .inner
+      .summary()
+      .map(|s| s.as_single_xsd_string())
+      .flatten()
+      .map(|s| s.to_string());
+    check_slurs(&name)?;
+    check_slurs_opt(&preferred_username)?;
+    check_slurs_opt(&bio)?;
+
     Ok(UserForm {
-      name: person
-        .name()
-        .unwrap()
-        .as_single_xsd_string()
-        .unwrap()
-        .into(),
-      preferred_username: person.inner.preferred_username().map(|u| u.to_string()),
+      name,
+      preferred_username,
       password_encrypted: "".to_string(),
       admin: false,
       banned: false,
       email: None,
       avatar,
-      updated: person
-        .updated()
-        .map(|u| u.as_ref().to_owned().naive_local()),
+      banner,
+      updated: person.updated().map(|u| u.to_owned().naive_local()),
       show_nsfw: false,
       theme: "".to_string(),
       default_sort_type: 0,
@@ -229,10 +268,8 @@ impl FromApub for UserForm {
       show_avatars: false,
       send_notifications_to_email: false,
       matrix_user_id: None,
-      actor_id: person.id().unwrap().to_string(),
-      bio: person
-        .summary()
-        .map(|s| s.as_single_xsd_string().unwrap().into()),
+      actor_id: check_actor_domain(person, expected_domain)?,
+      bio,
       local: false,
       private_key: None,
       public_key: Some(person.ext_one.public_key.to_owned().public_key_pem),
@@ -244,13 +281,13 @@ impl FromApub for UserForm {
 /// Return the user json over HTTP.
 pub async fn get_apub_user_http(
   info: web::Path<UserQuery>,
-  db: DbPoolParam,
+  context: web::Data<LemmyContext>,
 ) -> Result<HttpResponse<Body>, LemmyError> {
   let user_name = info.into_inner().user_name;
-  let user = blocking(&db, move |conn| {
-    Claims::find_by_email_or_username(conn, &user_name)
+  let user = blocking(context.pool(), move |conn| {
+    User_::find_by_email_or_username(conn, &user_name)
   })
   .await??;
-  let u = user.to_apub(&db).await?;
+  let u = user.to_apub(context.pool()).await?;
   Ok(create_apub_response(&u))
 }
diff --git a/server/src/apub/user_inbox.rs b/server/src/apub/user_inbox.rs
deleted file mode 100644 (file)
index 9bc102a..0000000
+++ /dev/null
@@ -1,372 +0,0 @@
-use crate::{
-  api::user::PrivateMessageResponse,
-  apub::{
-    extensions::signatures::verify,
-    fetcher::{get_or_fetch_and_upsert_remote_community, get_or_fetch_and_upsert_remote_user},
-    insert_activity,
-    FromApub,
-  },
-  blocking,
-  routes::{ChatServerParam, DbPoolParam},
-  websocket::{server::SendUserRoomMessage, UserOperation},
-  DbPool,
-  LemmyError,
-};
-use activitystreams::{
-  activity::{Accept, Create, Delete, Undo, Update},
-  object::Note,
-};
-use actix_web::{client::Client, web, HttpRequest, HttpResponse};
-use lemmy_db::{
-  community::{CommunityFollower, CommunityFollowerForm},
-  naive_now,
-  private_message::{PrivateMessage, PrivateMessageForm},
-  private_message_view::PrivateMessageView,
-  user::User_,
-  Crud,
-  Followable,
-};
-use log::debug;
-use serde::Deserialize;
-use std::fmt::Debug;
-
-#[serde(untagged)]
-#[derive(Deserialize, Debug)]
-pub enum UserAcceptedObjects {
-  Accept(Box<Accept>),
-  Create(Box<Create>),
-  Update(Box<Update>),
-  Delete(Box<Delete>),
-  Undo(Box<Undo>),
-}
-
-/// Handler for all incoming activities to user inboxes.
-pub async fn user_inbox(
-  request: HttpRequest,
-  input: web::Json<UserAcceptedObjects>,
-  path: web::Path<String>,
-  client: web::Data<Client>,
-  db: DbPoolParam,
-  chat_server: ChatServerParam,
-) -> Result<HttpResponse, LemmyError> {
-  // TODO: would be nice if we could do the signature check here, but we cant access the actor property
-  let input = input.into_inner();
-  let username = path.into_inner();
-  debug!("User {} received activity: {:?}", &username, &input);
-
-  match input {
-    UserAcceptedObjects::Accept(a) => receive_accept(*a, &request, &username, &client, &db).await,
-    UserAcceptedObjects::Create(c) => {
-      receive_create_private_message(*c, &request, &client, &db, chat_server).await
-    }
-    UserAcceptedObjects::Update(u) => {
-      receive_update_private_message(*u, &request, &client, &db, chat_server).await
-    }
-    UserAcceptedObjects::Delete(d) => {
-      receive_delete_private_message(*d, &request, &client, &db, chat_server).await
-    }
-    UserAcceptedObjects::Undo(u) => {
-      receive_undo_delete_private_message(*u, &request, &client, &db, chat_server).await
-    }
-  }
-}
-
-/// Handle accepted follows.
-async fn receive_accept(
-  accept: Accept,
-  request: &HttpRequest,
-  username: &str,
-  client: &Client,
-  pool: &DbPool,
-) -> Result<HttpResponse, LemmyError> {
-  let community_uri = accept
-    .accept_props
-    .get_actor_xsd_any_uri()
-    .unwrap()
-    .to_string();
-
-  let community = get_or_fetch_and_upsert_remote_community(&community_uri, client, pool).await?;
-  verify(request, &community)?;
-
-  let username = username.to_owned();
-  let user = blocking(pool, move |conn| User_::read_from_name(conn, &username)).await??;
-
-  insert_activity(community.creator_id, accept, false, pool).await?;
-
-  // Now you need to add this to the community follower
-  let community_follower_form = CommunityFollowerForm {
-    community_id: community.id,
-    user_id: user.id,
-  };
-
-  // This will fail if they're already a follower
-  blocking(pool, move |conn| {
-    CommunityFollower::follow(conn, &community_follower_form)
-  })
-  .await??;
-
-  // TODO: make sure that we actually requested a follow
-  Ok(HttpResponse::Ok().finish())
-}
-
-async fn receive_create_private_message(
-  create: Create,
-  request: &HttpRequest,
-  client: &Client,
-  pool: &DbPool,
-  chat_server: ChatServerParam,
-) -> Result<HttpResponse, LemmyError> {
-  let note = create
-    .create_props
-    .get_object_base_box()
-    .to_owned()
-    .unwrap()
-    .to_owned()
-    .into_concrete::<Note>()?;
-
-  let user_uri = create
-    .create_props
-    .get_actor_xsd_any_uri()
-    .unwrap()
-    .to_string();
-
-  let user = get_or_fetch_and_upsert_remote_user(&user_uri, client, pool).await?;
-  verify(request, &user)?;
-
-  insert_activity(user.id, create, false, pool).await?;
-
-  let private_message = PrivateMessageForm::from_apub(&note, client, pool).await?;
-
-  let inserted_private_message = blocking(pool, move |conn| {
-    PrivateMessage::create(conn, &private_message)
-  })
-  .await??;
-
-  let message = blocking(pool, move |conn| {
-    PrivateMessageView::read(conn, inserted_private_message.id)
-  })
-  .await??;
-
-  let res = PrivateMessageResponse { message };
-
-  let recipient_id = res.message.recipient_id;
-
-  chat_server.do_send(SendUserRoomMessage {
-    op: UserOperation::CreatePrivateMessage,
-    response: res,
-    recipient_id,
-    my_id: None,
-  });
-
-  Ok(HttpResponse::Ok().finish())
-}
-
-async fn receive_update_private_message(
-  update: Update,
-  request: &HttpRequest,
-  client: &Client,
-  pool: &DbPool,
-  chat_server: ChatServerParam,
-) -> Result<HttpResponse, LemmyError> {
-  let note = update
-    .update_props
-    .get_object_base_box()
-    .to_owned()
-    .unwrap()
-    .to_owned()
-    .into_concrete::<Note>()?;
-
-  let user_uri = update
-    .update_props
-    .get_actor_xsd_any_uri()
-    .unwrap()
-    .to_string();
-
-  let user = get_or_fetch_and_upsert_remote_user(&user_uri, client, pool).await?;
-  verify(request, &user)?;
-
-  insert_activity(user.id, update, false, pool).await?;
-
-  let private_message_form = PrivateMessageForm::from_apub(&note, client, pool).await?;
-
-  let private_message_ap_id = private_message_form.ap_id.clone();
-  let private_message = blocking(pool, move |conn| {
-    PrivateMessage::read_from_apub_id(conn, &private_message_ap_id)
-  })
-  .await??;
-
-  let private_message_id = private_message.id;
-  blocking(pool, move |conn| {
-    PrivateMessage::update(conn, private_message_id, &private_message_form)
-  })
-  .await??;
-
-  let private_message_id = private_message.id;
-  let message = blocking(pool, move |conn| {
-    PrivateMessageView::read(conn, private_message_id)
-  })
-  .await??;
-
-  let res = PrivateMessageResponse { message };
-
-  let recipient_id = res.message.recipient_id;
-
-  chat_server.do_send(SendUserRoomMessage {
-    op: UserOperation::EditPrivateMessage,
-    response: res,
-    recipient_id,
-    my_id: None,
-  });
-
-  Ok(HttpResponse::Ok().finish())
-}
-
-async fn receive_delete_private_message(
-  delete: Delete,
-  request: &HttpRequest,
-  client: &Client,
-  pool: &DbPool,
-  chat_server: ChatServerParam,
-) -> Result<HttpResponse, LemmyError> {
-  let note = delete
-    .delete_props
-    .get_object_base_box()
-    .to_owned()
-    .unwrap()
-    .to_owned()
-    .into_concrete::<Note>()?;
-
-  let user_uri = delete
-    .delete_props
-    .get_actor_xsd_any_uri()
-    .unwrap()
-    .to_string();
-
-  let user = get_or_fetch_and_upsert_remote_user(&user_uri, client, pool).await?;
-  verify(request, &user)?;
-
-  insert_activity(user.id, delete, false, pool).await?;
-
-  let private_message_form = PrivateMessageForm::from_apub(&note, client, pool).await?;
-
-  let private_message_ap_id = private_message_form.ap_id;
-  let private_message = blocking(pool, move |conn| {
-    PrivateMessage::read_from_apub_id(conn, &private_message_ap_id)
-  })
-  .await??;
-
-  let private_message_form = PrivateMessageForm {
-    content: private_message_form.content,
-    recipient_id: private_message.recipient_id,
-    creator_id: private_message.creator_id,
-    deleted: Some(true),
-    read: None,
-    ap_id: private_message.ap_id,
-    local: private_message.local,
-    published: None,
-    updated: Some(naive_now()),
-  };
-
-  let private_message_id = private_message.id;
-  blocking(pool, move |conn| {
-    PrivateMessage::update(conn, private_message_id, &private_message_form)
-  })
-  .await??;
-
-  let private_message_id = private_message.id;
-  let message = blocking(pool, move |conn| {
-    PrivateMessageView::read(&conn, private_message_id)
-  })
-  .await??;
-
-  let res = PrivateMessageResponse { message };
-
-  let recipient_id = res.message.recipient_id;
-
-  chat_server.do_send(SendUserRoomMessage {
-    op: UserOperation::EditPrivateMessage,
-    response: res,
-    recipient_id,
-    my_id: None,
-  });
-
-  Ok(HttpResponse::Ok().finish())
-}
-
-async fn receive_undo_delete_private_message(
-  undo: Undo,
-  request: &HttpRequest,
-  client: &Client,
-  pool: &DbPool,
-  chat_server: ChatServerParam,
-) -> Result<HttpResponse, LemmyError> {
-  let delete = undo
-    .undo_props
-    .get_object_base_box()
-    .to_owned()
-    .unwrap()
-    .to_owned()
-    .into_concrete::<Delete>()?;
-
-  let note = delete
-    .delete_props
-    .get_object_base_box()
-    .to_owned()
-    .unwrap()
-    .to_owned()
-    .into_concrete::<Note>()?;
-
-  let user_uri = delete
-    .delete_props
-    .get_actor_xsd_any_uri()
-    .unwrap()
-    .to_string();
-
-  let user = get_or_fetch_and_upsert_remote_user(&user_uri, client, pool).await?;
-  verify(request, &user)?;
-
-  insert_activity(user.id, delete, false, pool).await?;
-
-  let private_message = PrivateMessageForm::from_apub(&note, client, pool).await?;
-
-  let private_message_ap_id = private_message.ap_id.clone();
-  let private_message_id = blocking(pool, move |conn| {
-    PrivateMessage::read_from_apub_id(conn, &private_message_ap_id).map(|pm| pm.id)
-  })
-  .await??;
-
-  let private_message_form = PrivateMessageForm {
-    content: private_message.content,
-    recipient_id: private_message.recipient_id,
-    creator_id: private_message.creator_id,
-    deleted: Some(false),
-    read: None,
-    ap_id: private_message.ap_id,
-    local: private_message.local,
-    published: None,
-    updated: Some(naive_now()),
-  };
-
-  blocking(pool, move |conn| {
-    PrivateMessage::update(conn, private_message_id, &private_message_form)
-  })
-  .await??;
-
-  let message = blocking(pool, move |conn| {
-    PrivateMessageView::read(&conn, private_message_id)
-  })
-  .await??;
-
-  let res = PrivateMessageResponse { message };
-
-  let recipient_id = res.message.recipient_id;
-
-  chat_server.do_send(SendUserRoomMessage {
-    op: UserOperation::EditPrivateMessage,
-    response: res,
-    recipient_id,
-    my_id: None,
-  });
-
-  Ok(HttpResponse::Ok().finish())
-}
index b28e120a1ddb021efff7ac769cdae5f28bcf2fd6..7f45237c4b56b6fbdece7b82c76f93f15218705d 100644 (file)
@@ -1,6 +1,9 @@
 // This is for db migrations that require code
 use crate::LemmyError;
-use diesel::*;
+use diesel::{
+  sql_types::{Nullable, Text},
+  *,
+};
 use lemmy_db::{
   comment::Comment,
   community::{Community, CommunityForm},
@@ -10,7 +13,13 @@ use lemmy_db::{
   user::{UserForm, User_},
   Crud,
 };
-use lemmy_utils::{generate_actor_keypair, make_apub_endpoint, EndpointType};
+use lemmy_utils::{
+  generate_actor_keypair,
+  get_apub_protocol_string,
+  make_apub_endpoint,
+  settings::Settings,
+  EndpointType,
+};
 use log::info;
 
 pub fn run_advanced_migrations(conn: &PgConnection) -> Result<(), LemmyError> {
@@ -19,6 +28,7 @@ pub fn run_advanced_migrations(conn: &PgConnection) -> Result<(), LemmyError> {
   post_updates_2020_04_03(&conn)?;
   comment_updates_2020_04_03(&conn)?;
   private_message_updates_2020_05_05(&conn)?;
+  post_thumbnail_url_updates_2020_07_27(&conn)?;
 
   Ok(())
 }
@@ -30,7 +40,7 @@ fn user_updates_2020_04_02(conn: &PgConnection) -> Result<(), LemmyError> {
 
   // Update the actor_id, private_key, and public_key, last_refreshed_at
   let incorrect_users = user_
-    .filter(actor_id.eq("http://fake.com"))
+    .filter(actor_id.like("changeme_%"))
     .filter(local.eq(true))
     .load::<User_>(conn)?;
 
@@ -41,9 +51,10 @@ fn user_updates_2020_04_02(conn: &PgConnection) -> Result<(), LemmyError> {
 
     let form = UserForm {
       name: cuser.name.to_owned(),
-      email: cuser.email.to_owned(),
+      email: Some(cuser.email.to_owned()),
       matrix_user_id: cuser.matrix_user_id.to_owned(),
-      avatar: cuser.avatar.to_owned(),
+      avatar: Some(cuser.avatar.to_owned()),
+      banner: Some(cuser.banner.to_owned()),
       password_encrypted: cuser.password_encrypted.to_owned(),
       preferred_username: cuser.preferred_username.to_owned(),
       updated: None,
@@ -81,7 +92,7 @@ fn community_updates_2020_04_02(conn: &PgConnection) -> Result<(), LemmyError> {
 
   // Update the actor_id, private_key, and public_key, last_refreshed_at
   let incorrect_communities = community
-    .filter(actor_id.eq("http://fake.com"))
+    .filter(actor_id.like("changeme_%"))
     .filter(local.eq(true))
     .load::<Community>(conn)?;
 
@@ -106,6 +117,8 @@ fn community_updates_2020_04_02(conn: &PgConnection) -> Result<(), LemmyError> {
       public_key: Some(keypair.public_key),
       last_refreshed_at: Some(naive_now()),
       published: None,
+      icon: Some(ccommunity.icon.to_owned()),
+      banner: Some(ccommunity.banner.to_owned()),
     };
 
     Community::update(&conn, ccommunity.id, &form)?;
@@ -188,3 +201,32 @@ fn private_message_updates_2020_05_05(conn: &PgConnection) -> Result<(), LemmyEr
 
   Ok(())
 }
+
+fn post_thumbnail_url_updates_2020_07_27(conn: &PgConnection) -> Result<(), LemmyError> {
+  use lemmy_db::schema::post::dsl::*;
+
+  info!("Running post_thumbnail_url_updates_2020_07_27");
+
+  let domain_prefix = format!(
+    "{}://{}/pictrs/image/",
+    get_apub_protocol_string(),
+    Settings::get().hostname
+  );
+
+  let incorrect_thumbnails = post.filter(thumbnail_url.not_like("http%"));
+
+  // Prepend the rows with the update
+  let res = diesel::update(incorrect_thumbnails)
+    .set(
+      thumbnail_url.eq(
+        domain_prefix
+          .into_sql::<Nullable<Text>>()
+          .concat(thumbnail_url),
+      ),
+    )
+    .get_results::<Post>(conn)?;
+
+  info!("{} Post thumbnail_url rows updated.", res.len());
+
+  Ok(())
+}
index 4795cf01ee7bf0afefd138f4241d691492c9d8f9..07ee15d4061bd37a476fe242e1df44c7066b2639 100644 (file)
@@ -3,11 +3,11 @@
 pub extern crate strum_macros;
 #[macro_use]
 pub extern crate lazy_static;
-#[macro_use]
-pub extern crate failure;
 pub extern crate actix;
 pub extern crate actix_web;
+pub extern crate base64;
 pub extern crate bcrypt;
+pub extern crate captcha;
 pub extern crate chrono;
 pub extern crate diesel;
 pub extern crate dotenv;
@@ -29,11 +29,18 @@ pub mod routes;
 pub mod version;
 pub mod websocket;
 
-use crate::request::{retry, RecvError};
+use crate::{
+  request::{retry, RecvError},
+  websocket::server::ChatServer,
+};
+use actix::Addr;
 use actix_web::{client::Client, dev::ConnectionInfo};
+use anyhow::anyhow;
+use lemmy_utils::{get_apub_protocol_string, settings::Settings};
 use log::error;
 use percent_encoding::{utf8_percent_encode, NON_ALPHANUMERIC};
 use serde::Deserialize;
+use std::process::Command;
 
 pub type DbPool = diesel::r2d2::Pool<diesel::r2d2::ConnectionManager<diesel::PgConnection>>;
 pub type ConnectionId = usize;
@@ -44,12 +51,12 @@ pub type IPAddr = String;
 
 #[derive(Debug)]
 pub struct LemmyError {
-  inner: failure::Error,
+  inner: anyhow::Error,
 }
 
 impl<T> From<T> for LemmyError
 where
-  T: Into<failure::Error>,
+  T: Into<anyhow::Error>,
 {
   fn from(t: T) -> Self {
     LemmyError { inner: t.into() }
@@ -64,6 +71,41 @@ impl std::fmt::Display for LemmyError {
 
 impl actix_web::error::ResponseError for LemmyError {}
 
+pub struct LemmyContext {
+  pub pool: DbPool,
+  pub chat_server: Addr<ChatServer>,
+  pub client: Client,
+}
+
+impl LemmyContext {
+  pub fn create(pool: DbPool, chat_server: Addr<ChatServer>, client: Client) -> LemmyContext {
+    LemmyContext {
+      pool,
+      chat_server,
+      client,
+    }
+  }
+  pub fn pool(&self) -> &DbPool {
+    &self.pool
+  }
+  pub fn chat_server(&self) -> &Addr<ChatServer> {
+    &self.chat_server
+  }
+  pub fn client(&self) -> &Client {
+    &self.client
+  }
+}
+
+impl Clone for LemmyContext {
+  fn clone(&self) -> Self {
+    LemmyContext {
+      pool: self.pool.clone(),
+      chat_server: self.chat_server.clone(),
+      client: self.client.clone(),
+    }
+  }
+}
+
 #[derive(Deserialize, Debug)]
 pub struct IframelyResponse {
   title: Option<String>,
@@ -114,7 +156,7 @@ pub async fn fetch_pictrs(client: &Client, image_url: &str) -> Result<PictrsResp
   if response.msg == "ok" {
     Ok(response)
   } else {
-    Err(format_err!("{}", &response.msg).into())
+    Err(anyhow!("{}", &response.msg).into())
   }
 }
 
@@ -140,7 +182,7 @@ async fn fetch_iframely_and_pictrs_data(
         };
 
       // Fetch pictrs thumbnail
-      let pictrs_thumbnail = match iframely_thumbnail_url {
+      let pictrs_hash = match iframely_thumbnail_url {
         Some(iframely_thumbnail_url) => match fetch_pictrs(client, &iframely_thumbnail_url).await {
           Ok(res) => Some(res.files[0].file.to_owned()),
           Err(e) => {
@@ -158,6 +200,18 @@ async fn fetch_iframely_and_pictrs_data(
         },
       };
 
+      // The full urls are necessary for federation
+      let pictrs_thumbnail = if let Some(pictrs_hash) = pictrs_hash {
+        Some(format!(
+          "{}://{}/pictrs/image/{}",
+          get_apub_protocol_string(),
+          Settings::get().hostname,
+          pictrs_hash
+        ))
+      } else {
+        None
+      };
+
       (
         iframely_title,
         iframely_description,
@@ -175,13 +229,13 @@ pub async fn is_image_content_type(client: &Client, test: &str) -> Result<(), Le
   if response
     .headers()
     .get("Content-Type")
-    .ok_or_else(|| format_err!("No Content-Type header"))?
+    .ok_or_else(|| anyhow!("No Content-Type header"))?
     .to_str()?
     .starts_with("image/")
   {
     Ok(())
   } else {
-    Err(format_err!("Not an image type.").into())
+    Err(anyhow!("Not an image type.").into())
   }
 }
 
@@ -211,9 +265,56 @@ where
   Ok(res)
 }
 
+pub fn captcha_espeak_wav_base64(captcha: &str) -> Result<String, LemmyError> {
+  let mut built_text = String::new();
+
+  // Building proper speech text for espeak
+  for mut c in captcha.chars() {
+    let new_str = if c.is_alphabetic() {
+      if c.is_lowercase() {
+        c.make_ascii_uppercase();
+        format!("lower case {} ... ", c)
+      } else {
+        c.make_ascii_uppercase();
+        format!("capital {} ... ", c)
+      }
+    } else {
+      format!("{} ...", c)
+    };
+
+    built_text.push_str(&new_str);
+  }
+
+  espeak_wav_base64(&built_text)
+}
+
+pub fn espeak_wav_base64(text: &str) -> Result<String, LemmyError> {
+  // Make a temp file path
+  let uuid = uuid::Uuid::new_v4().to_string();
+  let file_path = format!("/tmp/lemmy_espeak_{}.wav", &uuid);
+
+  // Write the wav file
+  Command::new("espeak")
+    .arg("-w")
+    .arg(&file_path)
+    .arg(text)
+    .status()?;
+
+  // Read the wav file bytes
+  let bytes = std::fs::read(&file_path)?;
+
+  // Delete the file
+  std::fs::remove_file(file_path)?;
+
+  // Convert to base64
+  let base64 = base64::encode(bytes);
+
+  Ok(base64)
+}
+
 #[cfg(test)]
 mod tests {
-  use crate::is_image_content_type;
+  use crate::{captcha_espeak_wav_base64, is_image_content_type};
 
   #[test]
   fn test_image() {
@@ -228,6 +329,11 @@ mod tests {
     });
   }
 
+  #[test]
+  fn test_espeak() {
+    assert!(captcha_espeak_wav_base64("WxRt2l").is_ok())
+  }
+
   // These helped with testing
   // #[test]
   // fn test_iframely() {
index 7689d7ad1aa363e87468ce6a0e5ff2870cffafca..4a012ab47e718419609579233603504dcf01b0b1 100644 (file)
@@ -1,17 +1,13 @@
-extern crate lemmy_server;
 #[macro_use]
 extern crate diesel_migrations;
 #[macro_use]
 pub extern crate lazy_static;
 
-pub type DbPool = Pool<ConnectionManager<PgConnection>>;
-
-use crate::lemmy_server::actix_web::dev::Service;
 use actix::prelude::*;
 use actix_web::{
   body::Body,
   client::Client,
-  dev::{ServiceRequest, ServiceResponse},
+  dev::{Service, ServiceRequest, ServiceResponse},
   http::{
     header::{CACHE_CONTROL, CONTENT_TYPE},
     HeaderValue,
@@ -27,8 +23,9 @@ use lemmy_server::{
   blocking,
   code_migrations::run_advanced_migrations,
   rate_limit::{rate_limiter::RateLimiter, RateLimit},
-  routes::{api, federation, feeds, index, nodeinfo, webfinger},
+  routes::*,
   websocket::server::*,
+  LemmyContext,
   LemmyError,
 };
 use lemmy_utils::{settings::Settings, CACHE_CONTROL_REGEX};
@@ -72,28 +69,28 @@ async fn main() -> Result<(), LemmyError> {
     rate_limiter: Arc::new(Mutex::new(RateLimiter::default())),
   };
 
-  // Set up websocket server
-  let server = ChatServer::startup(pool.clone(), rate_limiter.clone(), Client::default()).start();
-
   println!(
     "Starting http server at {}:{}",
     settings.bind, settings.port
   );
 
+  let chat_server =
+    ChatServer::startup(pool.clone(), rate_limiter.clone(), Client::default()).start();
+
   // Create Http server with websocket support
   HttpServer::new(move || {
+    let context = LemmyContext::create(pool.clone(), chat_server.to_owned(), Client::default());
     let settings = Settings::get();
     let rate_limiter = rate_limiter.clone();
     App::new()
       .wrap_fn(add_cache_headers)
       .wrap(middleware::Logger::default())
-      .data(pool.clone())
-      .data(server.clone())
-      .data(Client::default())
+      .data(context)
       // The routes
-      .configure(move |cfg| api::config(cfg, &rate_limiter))
+      .configure(|cfg| api::config(cfg, &rate_limiter))
       .configure(federation::config)
       .configure(feeds::config)
+      .configure(|cfg| images::config(cfg, &rate_limiter))
       .configure(index::config)
       .configure(nodeinfo::config)
       .configure(webfinger::config)
index 513c923c6182b006ebedff3cf8c57ac1cbf63f0b..39df726505c02ea03915120747604c003b506f3c 100644 (file)
@@ -45,6 +45,10 @@ impl RateLimit {
     self.kind(RateLimitType::Register)
   }
 
+  pub fn image(&self) -> RateLimited {
+    self.kind(RateLimitType::Image)
+  }
+
   fn kind(&self, type_: RateLimitType) -> RateLimited {
     RateLimited {
       rate_limiter: self.rate_limiter.clone(),
@@ -101,6 +105,15 @@ impl RateLimited {
             true,
           )?;
         }
+        RateLimitType::Image => {
+          limiter.check_rate_limit_full(
+            self.type_,
+            &ip_addr,
+            rate_limit.image,
+            rate_limit.image_per_second,
+            false,
+          )?;
+        }
       };
     }
 
index 20a617c2fe3097d4617750ed8cd0460df3c87f9c..f1a38841244c4a5c429a03945825a5dcdfb4ed4e 100644 (file)
@@ -15,6 +15,7 @@ pub enum RateLimitType {
   Message,
   Register,
   Post,
+  Image,
 }
 
 /// Rate limiting based on rate type and IP addr
index 7d09b60df99ac820f0aadfa4f7e90a5bf470b2f1..70a2b6933ead96078d2e4e968d02410193789436 100644 (file)
@@ -1,12 +1,14 @@
 use crate::LemmyError;
+use anyhow::anyhow;
 use std::future::Future;
+use thiserror::Error;
 
-#[derive(Clone, Debug, Fail)]
-#[fail(display = "Error sending request, {}", _0)]
+#[derive(Clone, Debug, Error)]
+#[error("Error sending request, {0}")]
 struct SendError(pub String);
 
-#[derive(Clone, Debug, Fail)]
-#[fail(display = "Error receiving response, {}", _0)]
+#[derive(Clone, Debug, Error)]
+#[error("Error receiving response, {0}")]
 pub struct RecvError(pub String);
 
 pub async fn retry<F, Fut, T>(f: F) -> Result<T, LemmyError>
@@ -22,7 +24,7 @@ where
   F: Fn() -> Fut,
   Fut: Future<Output = Result<Result<T, actix_web::client::SendRequestError>, LemmyError>>,
 {
-  let mut response = Err(format_err!("connect timeout").into());
+  let mut response = Err(anyhow!("connect timeout").into());
 
   for _ in 0u8..3 {
     match (f)().await? {
index 35e495fa55835346cf353d4975ea6e790bce9cb6..f2ee38d259e8f978c37227c2b5d09ab9eaae354c 100644 (file)
@@ -1,10 +1,9 @@
 use crate::{
-  api::{comment::*, community::*, post::*, site::*, user::*, Oper, Perform},
+  api::{comment::*, community::*, post::*, site::*, user::*, Perform},
   rate_limit::RateLimit,
-  routes::{ChatServerParam, DbPoolParam},
-  websocket::WebsocketInfo,
+  LemmyContext,
 };
-use actix_web::{client::Client, error::ErrorBadRequest, *};
+use actix_web::{error::ErrorBadRequest, *};
 use serde::Serialize;
 
 pub fn config(cfg: &mut web::ServiceConfig, rate_limit: &RateLimit) {
@@ -53,7 +52,9 @@ pub fn config(cfg: &mut web::ServiceConfig, rate_limit: &RateLimit) {
           .route("", web::put().to(route_post::<EditCommunity>))
           .route("/list", web::get().to(route_get::<ListCommunities>))
           .route("/follow", web::post().to(route_post::<FollowCommunity>))
+          .route("/delete", web::post().to(route_post::<DeleteCommunity>))
           // Mod Actions
+          .route("/remove", web::post().to(route_post::<RemoveCommunity>))
           .route("/transfer", web::post().to(route_post::<TransferCommunity>))
           .route("/ban_user", web::post().to(route_post::<BanFromCommunity>))
           .route("/mod", web::post().to(route_post::<AddModToCommunity>)),
@@ -71,6 +72,10 @@ pub fn config(cfg: &mut web::ServiceConfig, rate_limit: &RateLimit) {
           .wrap(rate_limit.message())
           .route("", web::get().to(route_get::<GetPost>))
           .route("", web::put().to(route_post::<EditPost>))
+          .route("/delete", web::post().to(route_post::<DeletePost>))
+          .route("/remove", web::post().to(route_post::<RemovePost>))
+          .route("/lock", web::post().to(route_post::<LockPost>))
+          .route("/sticky", web::post().to(route_post::<StickyPost>))
           .route("/list", web::get().to(route_get::<GetPosts>))
           .route("/like", web::post().to(route_post::<CreatePostLike>))
           .route("/save", web::put().to(route_post::<SavePost>)),
@@ -81,8 +86,15 @@ pub fn config(cfg: &mut web::ServiceConfig, rate_limit: &RateLimit) {
           .wrap(rate_limit.message())
           .route("", web::post().to(route_post::<CreateComment>))
           .route("", web::put().to(route_post::<EditComment>))
+          .route("/delete", web::post().to(route_post::<DeleteComment>))
+          .route("/remove", web::post().to(route_post::<RemoveComment>))
+          .route(
+            "/mark_as_read",
+            web::post().to(route_post::<MarkCommentAsRead>),
+          )
           .route("/like", web::post().to(route_post::<CreateCommentLike>))
-          .route("/save", web::put().to(route_post::<SaveComment>)),
+          .route("/save", web::put().to(route_post::<SaveComment>))
+          .route("/list", web::get().to(route_get::<GetComments>)),
       )
       // Private Message
       .service(
@@ -90,7 +102,15 @@ pub fn config(cfg: &mut web::ServiceConfig, rate_limit: &RateLimit) {
           .wrap(rate_limit.message())
           .route("/list", web::get().to(route_get::<GetPrivateMessages>))
           .route("", web::post().to(route_post::<CreatePrivateMessage>))
-          .route("", web::put().to(route_post::<EditPrivateMessage>)),
+          .route("", web::put().to(route_post::<EditPrivateMessage>))
+          .route(
+            "/delete",
+            web::post().to(route_post::<DeletePrivateMessage>),
+          )
+          .route(
+            "/mark_as_read",
+            web::post().to(route_post::<MarkPrivateMessageAsRead>),
+          ),
       )
       // User
       .service(
@@ -107,16 +127,21 @@ pub fn config(cfg: &mut web::ServiceConfig, rate_limit: &RateLimit) {
           .wrap(rate_limit.message())
           .route("", web::get().to(route_get::<GetUserDetails>))
           .route("/mention", web::get().to(route_get::<GetUserMentions>))
-          .route("/mention", web::put().to(route_post::<EditUserMention>))
+          .route(
+            "/mention/mark_as_read",
+            web::post().to(route_post::<MarkUserMentionAsRead>),
+          )
           .route("/replies", web::get().to(route_get::<GetReplies>))
           .route(
             "/followed_communities",
             web::get().to(route_get::<GetFollowedCommunities>),
           )
+          .route("/join", web::post().to(route_post::<UserJoin>))
           // Admin action. I don't like that it's in /user
           .route("/ban", web::post().to(route_post::<BanUser>))
           // Account actions. I don't like that they're in /user maybe /accounts
           .route("/login", web::post().to(route_post::<Login>))
+          .route("/get_captcha", web::get().to(route_post::<GetCaptcha>))
           .route(
             "/delete_account",
             web::post().to(route_post::<DeleteAccount>),
@@ -150,23 +175,14 @@ pub fn config(cfg: &mut web::ServiceConfig, rate_limit: &RateLimit) {
 
 async fn perform<Request>(
   data: Request,
-  client: &Client,
-  db: DbPoolParam,
-  chat_server: ChatServerParam,
+  context: web::Data<LemmyContext>,
 ) -> Result<HttpResponse, Error>
 where
-  Oper<Request>: Perform,
+  Request: Perform,
   Request: Send + 'static,
 {
-  let ws_info = WebsocketInfo {
-    chatserver: chat_server.get_ref().to_owned(),
-    id: None,
-  };
-
-  let oper: Oper<Request> = Oper::new(data, client.clone());
-
-  let res = oper
-    .perform(&db, Some(ws_info))
+  let res = data
+    .perform(&context, None)
     .await
     .map(|json| HttpResponse::Ok().json(json))
     .map_err(ErrorBadRequest)?;
@@ -175,26 +191,20 @@ where
 
 async fn route_get<Data>(
   data: web::Query<Data>,
-  client: web::Data<Client>,
-  db: DbPoolParam,
-  chat_server: ChatServerParam,
+  context: web::Data<LemmyContext>,
 ) -> Result<HttpResponse, Error>
 where
-  Data: Serialize + Send + 'static,
-  Oper<Data>: Perform,
+  Data: Serialize + Send + 'static + Perform,
 {
-  perform::<Data>(data.0, &client, db, chat_server).await
+  perform::<Data>(data.0, context).await
 }
 
 async fn route_post<Data>(
   data: web::Json<Data>,
-  client: web::Data<Client>,
-  db: DbPoolParam,
-  chat_server: ChatServerParam,
+  context: web::Data<LemmyContext>,
 ) -> Result<HttpResponse, Error>
 where
-  Data: Serialize + Send + 'static,
-  Oper<Data>: Perform,
+  Data: Serialize + Send + 'static + Perform,
 {
-  perform::<Data>(data.0, &client, db, chat_server).await
+  perform::<Data>(data.0, context).await
 }
index cd4c47803dbfbebfaee98da7f1f4cdc7aa505c15..2a0c81b2351e58265edd4e70073bc9b74817b7cc 100644 (file)
@@ -1,11 +1,9 @@
 use crate::apub::{
   comment::get_apub_comment,
   community::*,
-  community_inbox::community_inbox,
+  inbox::{community_inbox::community_inbox, shared_inbox::shared_inbox, user_inbox::user_inbox},
   post::get_apub_post,
-  shared_inbox::shared_inbox,
   user::*,
-  user_inbox::user_inbox,
   APUB_JSON_CONTENT_TYPE,
 };
 use actix_web::*;
@@ -30,11 +28,10 @@ pub fn config(cfg: &mut web::ServiceConfig) {
             "/c/{community_name}/followers",
             web::get().to(get_apub_community_followers),
           )
-          // TODO This is only useful for history which we aren't doing right now
-          // .route(
-          //   "/c/{community_name}/outbox",
-          //   web::get().to(get_apub_community_outbox),
-          // )
+          .route(
+            "/c/{community_name}/outbox",
+            web::get().to(get_apub_community_outbox),
+          )
           .route("/u/{user_name}", web::get().to(get_apub_user_http))
           .route("/post/{post_id}", web::get().to(get_apub_post))
           .route("/comment/{comment_id}", web::get().to(get_apub_comment)),
index 1322feb440e04da295f5dff326ab48e5ffed73e3..890a458f9d7d22e4e6e34a399a6a4dbbbdf31c0a 100644 (file)
@@ -1,10 +1,8 @@
-use crate::{api::claims::Claims, blocking, routes::DbPoolParam, LemmyError};
+use crate::{api::claims::Claims, blocking, LemmyContext, LemmyError};
 use actix_web::{error::ErrorBadRequest, *};
+use anyhow::anyhow;
 use chrono::{DateTime, NaiveDateTime, Utc};
-use diesel::{
-  r2d2::{ConnectionManager, Pool},
-  PgConnection,
-};
+use diesel::PgConnection;
 use lemmy_db::{
   comment_view::{ReplyQueryBuilder, ReplyView},
   community::Community,
@@ -39,12 +37,17 @@ pub fn config(cfg: &mut web::ServiceConfig) {
     .route("/feeds/all.xml", web::get().to(get_all_feed));
 }
 
-async fn get_all_feed(info: web::Query<Params>, db: DbPoolParam) -> Result<HttpResponse, Error> {
+async fn get_all_feed(
+  info: web::Query<Params>,
+  context: web::Data<LemmyContext>,
+) -> Result<HttpResponse, Error> {
   let sort_type = get_sort_type(info).map_err(ErrorBadRequest)?;
 
-  let rss = blocking(&db, move |conn| get_feed_all_data(conn, &sort_type))
-    .await?
-    .map_err(ErrorBadRequest)?;
+  let rss = blocking(context.pool(), move |conn| {
+    get_feed_all_data(conn, &sort_type)
+  })
+  .await?
+  .map_err(ErrorBadRequest)?;
 
   Ok(
     HttpResponse::Ok()
@@ -61,7 +64,7 @@ fn get_feed_all_data(conn: &PgConnection, sort_type: &SortType) -> Result<String
     .sort(sort_type)
     .list()?;
 
-  let items = create_post_items(posts);
+  let items = create_post_items(posts)?;
 
   let mut channel_builder = ChannelBuilder::default();
   channel_builder
@@ -73,13 +76,13 @@ fn get_feed_all_data(conn: &PgConnection, sort_type: &SortType) -> Result<String
     channel_builder.description(&site_desc);
   }
 
-  Ok(channel_builder.build().unwrap().to_string())
+  Ok(channel_builder.build().map_err(|e| anyhow!(e))?.to_string())
 }
 
 async fn get_feed(
   path: web::Path<(String, String)>,
   info: web::Query<Params>,
-  db: web::Data<Pool<ConnectionManager<PgConnection>>>,
+  context: web::Data<LemmyContext>,
 ) -> Result<HttpResponse, Error> {
   let sort_type = get_sort_type(info).map_err(ErrorBadRequest)?;
 
@@ -88,12 +91,12 @@ async fn get_feed(
     "c" => RequestType::Community,
     "front" => RequestType::Front,
     "inbox" => RequestType::Inbox,
-    _ => return Err(ErrorBadRequest(LemmyError::from(format_err!("wrong_type")))),
+    _ => return Err(ErrorBadRequest(LemmyError::from(anyhow!("wrong_type")))),
   };
 
   let param = path.1.to_owned();
 
-  let builder = blocking(&db, move |conn| match request_type {
+  let builder = blocking(context.pool(), move |conn| match request_type {
     RequestType::User => get_feed_user(conn, &sort_type, param),
     RequestType::Community => get_feed_community(conn, &sort_type, param),
     RequestType::Front => get_feed_front(conn, &sort_type, param),
@@ -134,7 +137,7 @@ fn get_feed_user(
     .for_creator_id(user.id)
     .list()?;
 
-  let items = create_post_items(posts);
+  let items = create_post_items(posts)?;
 
   let mut channel_builder = ChannelBuilder::default();
   channel_builder
@@ -159,7 +162,7 @@ fn get_feed_community(
     .for_community_id(community.id)
     .list()?;
 
-  let items = create_post_items(posts);
+  let items = create_post_items(posts)?;
 
   let mut channel_builder = ChannelBuilder::default();
   channel_builder
@@ -188,7 +191,7 @@ fn get_feed_front(
     .my_user_id(user_id)
     .list()?;
 
-  let items = create_post_items(posts);
+  let items = create_post_items(posts)?;
 
   let mut channel_builder = ChannelBuilder::default();
   channel_builder
@@ -217,7 +220,7 @@ fn get_feed_inbox(conn: &PgConnection, jwt: String) -> Result<ChannelBuilder, Le
     .sort(&sort)
     .list()?;
 
-  let items = create_reply_and_mention_items(replies, mentions);
+  let items = create_reply_and_mention_items(replies, mentions)?;
 
   let mut channel_builder = ChannelBuilder::default();
   channel_builder
@@ -235,7 +238,7 @@ fn get_feed_inbox(conn: &PgConnection, jwt: String) -> Result<ChannelBuilder, Le
 fn create_reply_and_mention_items(
   replies: Vec<ReplyView>,
   mentions: Vec<UserMentionView>,
-) -> Vec<Item> {
+) -> Result<Vec<Item>, LemmyError> {
   let mut reply_items: Vec<Item> = replies
     .iter()
     .map(|r| {
@@ -247,7 +250,7 @@ fn create_reply_and_mention_items(
       );
       build_item(&r.creator_name, &r.published, &reply_url, &r.content)
     })
-    .collect();
+    .collect::<Result<Vec<Item>, LemmyError>>()?;
 
   let mut mention_items: Vec<Item> = mentions
     .iter()
@@ -260,13 +263,18 @@ fn create_reply_and_mention_items(
       );
       build_item(&m.creator_name, &m.published, &mention_url, &m.content)
     })
-    .collect();
+    .collect::<Result<Vec<Item>, LemmyError>>()?;
 
   reply_items.append(&mut mention_items);
-  reply_items
+  Ok(reply_items)
 }
 
-fn build_item(creator_name: &str, published: &NaiveDateTime, url: &str, content: &str) -> Item {
+fn build_item(
+  creator_name: &str,
+  published: &NaiveDateTime,
+  url: &str,
+  content: &str,
+) -> Result<Item, LemmyError> {
   let mut i = ItemBuilder::default();
   i.title(format!("Reply from {}", creator_name));
   let author_url = format!("https://{}/u/{}", Settings::get().hostname, creator_name);
@@ -277,16 +285,20 @@ fn build_item(creator_name: &str, published: &NaiveDateTime, url: &str, content:
   let dt = DateTime::<Utc>::from_utc(*published, Utc);
   i.pub_date(dt.to_rfc2822());
   i.comments(url.to_owned());
-  let guid = GuidBuilder::default().permalink(true).value(url).build();
-  i.guid(guid.unwrap());
+  let guid = GuidBuilder::default()
+    .permalink(true)
+    .value(url)
+    .build()
+    .map_err(|e| anyhow!(e))?;
+  i.guid(guid);
   i.link(url.to_owned());
   // TODO add images
   let html = markdown_to_html(&content.to_string());
   i.description(html);
-  i.build().unwrap()
+  Ok(i.build().map_err(|e| anyhow!(e))?)
 }
 
-fn create_post_items(posts: Vec<PostView>) -> Vec<Item> {
+fn create_post_items(posts: Vec<PostView>) -> Result<Vec<Item>, LemmyError> {
   let mut items: Vec<Item> = Vec::new();
 
   for p in posts {
@@ -308,8 +320,9 @@ fn create_post_items(posts: Vec<PostView>) -> Vec<Item> {
     let guid = GuidBuilder::default()
       .permalink(true)
       .value(&post_url)
-      .build();
-    i.guid(guid.unwrap());
+      .build()
+      .map_err(|e| anyhow!(e))?;
+    i.guid(guid);
 
     let community_url = format!(
       "https://{}/c/{}",
@@ -323,8 +336,10 @@ fn create_post_items(posts: Vec<PostView>) -> Vec<Item> {
         p.community_name, community_url
       ))
       .domain(Settings::get().hostname.to_owned())
-      .build();
-    i.categories(vec![category.unwrap()]);
+      .build()
+      .map_err(|e| anyhow!(e))?;
+
+    i.categories(vec![category]);
 
     if let Some(url) = p.url {
       i.link(url);
@@ -347,8 +362,8 @@ fn create_post_items(posts: Vec<PostView>) -> Vec<Item> {
 
     i.description(description);
 
-    items.push(i.build().unwrap());
+    items.push(i.build().map_err(|e| anyhow!(e))?);
   }
 
-  items
+  Ok(items)
 }
diff --git a/server/src/routes/images.rs b/server/src/routes/images.rs
new file mode 100644 (file)
index 0000000..766aff6
--- /dev/null
@@ -0,0 +1,140 @@
+use crate::rate_limit::RateLimit;
+use actix::clock::Duration;
+use actix_web::{body::BodyStream, http::StatusCode, *};
+use awc::Client;
+use lemmy_utils::settings::Settings;
+use serde::{Deserialize, Serialize};
+
+pub fn config(cfg: &mut web::ServiceConfig, rate_limit: &RateLimit) {
+  let client = Client::build()
+    .header("User-Agent", "pict-rs-frontend, v0.1.0")
+    .timeout(Duration::from_secs(30))
+    .finish();
+
+  cfg
+    .data(client)
+    .service(
+      web::resource("/pictrs/image")
+        .wrap(rate_limit.image())
+        .route(web::post().to(upload)),
+    )
+    .service(web::resource("/pictrs/image/{filename}").route(web::get().to(full_res)))
+    .service(
+      web::resource("/pictrs/image/thumbnail{size}/{filename}").route(web::get().to(thumbnail)),
+    )
+    .service(web::resource("/pictrs/image/delete/{token}/{filename}").route(web::get().to(delete)));
+}
+
+#[derive(Debug, Serialize, Deserialize)]
+pub struct Image {
+  file: String,
+  delete_token: String,
+}
+
+#[derive(Debug, Serialize, Deserialize)]
+pub struct Images {
+  msg: String,
+  files: Option<Vec<Image>>,
+}
+
+async fn upload(
+  req: HttpRequest,
+  body: web::Payload,
+  client: web::Data<Client>,
+) -> Result<HttpResponse, Error> {
+  // TODO: check auth and rate limit here
+
+  let mut res = client
+    .request_from(format!("{}/image", Settings::get().pictrs_url), req.head())
+    .if_some(req.head().peer_addr, |addr, req| {
+      req.header("X-Forwarded-For", addr.to_string())
+    })
+    .send_stream(body)
+    .await?;
+
+  let images = res.json::<Images>().await?;
+
+  Ok(HttpResponse::build(res.status()).json(images))
+}
+
+async fn full_res(
+  filename: web::Path<String>,
+  req: HttpRequest,
+  client: web::Data<Client>,
+) -> Result<HttpResponse, Error> {
+  let url = format!(
+    "{}/image/{}",
+    Settings::get().pictrs_url,
+    &filename.into_inner()
+  );
+  image(url, req, client).await
+}
+
+async fn thumbnail(
+  parts: web::Path<(u64, String)>,
+  req: HttpRequest,
+  client: web::Data<Client>,
+) -> Result<HttpResponse, Error> {
+  let (size, file) = parts.into_inner();
+
+  let url = format!(
+    "{}/image/thumbnail{}/{}",
+    Settings::get().pictrs_url,
+    size,
+    &file
+  );
+
+  image(url, req, client).await
+}
+
+async fn image(
+  url: String,
+  req: HttpRequest,
+  client: web::Data<Client>,
+) -> Result<HttpResponse, Error> {
+  let res = client
+    .request_from(url, req.head())
+    .if_some(req.head().peer_addr, |addr, req| {
+      req.header("X-Forwarded-For", addr.to_string())
+    })
+    .no_decompress()
+    .send()
+    .await?;
+
+  if res.status() == StatusCode::NOT_FOUND {
+    return Ok(HttpResponse::NotFound().finish());
+  }
+
+  let mut client_res = HttpResponse::build(res.status());
+
+  for (name, value) in res.headers().iter().filter(|(h, _)| *h != "connection") {
+    client_res.header(name.clone(), value.clone());
+  }
+
+  Ok(client_res.body(BodyStream::new(res)))
+}
+
+async fn delete(
+  components: web::Path<(String, String)>,
+  req: HttpRequest,
+  client: web::Data<Client>,
+) -> Result<HttpResponse, Error> {
+  let (token, file) = components.into_inner();
+
+  let url = format!(
+    "{}/image/delete/{}/{}",
+    Settings::get().pictrs_url,
+    &token,
+    &file
+  );
+  let res = client
+    .request_from(url, req.head())
+    .if_some(req.head().peer_addr, |addr, req| {
+      req.header("X-Forwarded-For", addr.to_string())
+    })
+    .no_decompress()
+    .send()
+    .await?;
+
+  Ok(HttpResponse::build(res.status()).body(BodyStream::new(res)))
+}
index b579a1958384fb625680319b77370fe3578c17c5..88a36c3983b9aa6ae4b46e9c60e669b9abff44ba 100644 (file)
@@ -40,7 +40,8 @@ pub fn config(cfg: &mut web::ServiceConfig) {
     )
     .route("/search", web::get().to(index))
     .route("/sponsors", web::get().to(index))
-    .route("/password_change/{token}", web::get().to(index));
+    .route("/password_change/{token}", web::get().to(index))
+    .route("/instances", web::get().to(index));
 }
 
 async fn index() -> Result<NamedFile, Error> {
index bcb7e45fa33bd631191838008c070b1a839194d7..d24b6ca5b18f1f60f6ad1456aa3aab8efef61c5d 100644 (file)
@@ -1,20 +1,8 @@
 pub mod api;
 pub mod federation;
 pub mod feeds;
+pub mod images;
 pub mod index;
 pub mod nodeinfo;
 pub mod webfinger;
 pub mod websocket;
-
-use crate::{rate_limit::rate_limiter::RateLimiter, websocket::server::ChatServer};
-use actix::prelude::*;
-use actix_web::*;
-use diesel::{
-  r2d2::{ConnectionManager, Pool},
-  PgConnection,
-};
-use std::sync::{Arc, Mutex};
-
-pub type DbPoolParam = web::Data<Pool<ConnectionManager<PgConnection>>>;
-pub type RateLimitParam = web::Data<Arc<Mutex<RateLimiter>>>;
-pub type ChatServerParam = web::Data<Addr<ChatServer>>;
index 5094c2f15e571342376ddb0f46962fa7f37adc77..1c81bc5446c0daeb28f616a51bac1f45b6b6cf0d 100644 (file)
@@ -1,5 +1,6 @@
-use crate::{blocking, routes::DbPoolParam, version, LemmyError};
+use crate::{blocking, version, LemmyContext, LemmyError};
 use actix_web::{body::Body, error::ErrorBadRequest, *};
+use anyhow::anyhow;
 use lemmy_db::site_view::SiteView;
 use lemmy_utils::{get_apub_protocol_string, settings::Settings};
 use serde::{Deserialize, Serialize};
@@ -25,10 +26,10 @@ async fn node_info_well_known() -> Result<HttpResponse<Body>, LemmyError> {
   Ok(HttpResponse::Ok().json(node_info))
 }
 
-async fn node_info(db: DbPoolParam) -> Result<HttpResponse, Error> {
-  let site_view = blocking(&db, SiteView::read)
+async fn node_info(context: web::Data<LemmyContext>) -> Result<HttpResponse, Error> {
+  let site_view = blocking(context.pool(), SiteView::read)
     .await?
-    .map_err(|_| ErrorBadRequest(LemmyError::from(format_err!("not_found"))))?;
+    .map_err(|_| ErrorBadRequest(LemmyError::from(anyhow!("not_found"))))?;
 
   let protocols = if Settings::get().federation.enabled {
     vec!["activitypub".to_string()]
index e616de0e8ed9bd2116f5fa2c4c5e7c79eb0561b2..57bea713d1682b6cfafdb1f8e9daa673607af874 100644 (file)
@@ -1,5 +1,6 @@
-use crate::{blocking, routes::DbPoolParam, LemmyError};
+use crate::{blocking, LemmyContext, LemmyError};
 use actix_web::{error::ErrorBadRequest, web::Query, *};
+use anyhow::anyhow;
 use lemmy_db::{community::Community, user::User_};
 use lemmy_utils::{settings::Settings, WEBFINGER_COMMUNITY_REGEX, WEBFINGER_USER_REGEX};
 use serde::{Deserialize, Serialize};
@@ -43,7 +44,7 @@ pub fn config(cfg: &mut web::ServiceConfig) {
 /// https://radical.town/.well-known/webfinger?resource=acct:felix@radical.town
 async fn get_webfinger_response(
   info: Query<Params>,
-  db: DbPoolParam,
+  context: web::Data<LemmyContext>,
 ) -> Result<HttpResponse, Error> {
   let community_regex_parsed = WEBFINGER_COMMUNITY_REGEX
     .captures(&info.resource)
@@ -58,21 +59,23 @@ async fn get_webfinger_response(
   let url = if let Some(community_name) = community_regex_parsed {
     let community_name = community_name.as_str().to_owned();
     // Make sure the requested community exists.
-    blocking(&db, move |conn| {
+    blocking(context.pool(), move |conn| {
       Community::read_from_name(conn, &community_name)
     })
     .await?
-    .map_err(|_| ErrorBadRequest(LemmyError::from(format_err!("not_found"))))?
+    .map_err(|_| ErrorBadRequest(LemmyError::from(anyhow!("not_found"))))?
     .actor_id
   } else if let Some(user_name) = user_regex_parsed {
     let user_name = user_name.as_str().to_owned();
     // Make sure the requested user exists.
-    blocking(&db, move |conn| User_::read_from_name(conn, &user_name))
-      .await?
-      .map_err(|_| ErrorBadRequest(LemmyError::from(format_err!("not_found"))))?
-      .actor_id
+    blocking(context.pool(), move |conn| {
+      User_::read_from_name(conn, &user_name)
+    })
+    .await?
+    .map_err(|_| ErrorBadRequest(LemmyError::from(anyhow!("not_found"))))?
+    .actor_id
   } else {
-    return Err(ErrorBadRequest(LemmyError::from(format_err!("not_found"))));
+    return Err(ErrorBadRequest(LemmyError::from(anyhow!("not_found"))));
   };
 
   let json = WebFingerResponse {
index 2f964d4313c90c258057450d6e7966320a15eb1c..7c787d66d79bce0e4f0a17fd98ac7fbab22ff5ee 100644 (file)
@@ -1,6 +1,7 @@
 use crate::{
   get_ip,
   websocket::server::{ChatServer, *},
+  LemmyContext,
 };
 use actix::prelude::*;
 use actix_web::*;
@@ -17,11 +18,11 @@ const CLIENT_TIMEOUT: Duration = Duration::from_secs(10);
 pub async fn chat_route(
   req: HttpRequest,
   stream: web::Payload,
-  chat_server: web::Data<Addr<ChatServer>>,
+  context: web::Data<LemmyContext>,
 ) -> Result<HttpResponse, Error> {
   ws::start(
     WSSession {
-      cs_addr: chat_server.get_ref().to_owned(),
+      cs_addr: context.chat_server().to_owned(),
       id: 0,
       hb: Instant::now(),
       ip: get_ip(&req.connection_info()),
index 59864dd0dacf97dcb8cdf5e94e7b8ccfbb898f4a..b4cafe15e3b8433003dbc03ae8d0cf3b62d8eb4d 100644 (file)
@@ -1 +1 @@
-pub const VERSION: &str = "v0.7.21";
+pub const VERSION: &str = "v0.7.55";
index cdaf4f3041dc2de78ab76144448028d09bf1eab2..1430d89ae1555874e7f44132967bd5b3501bbd2d 100644 (file)
@@ -1,6 +1,5 @@
 pub mod server;
 
-use crate::ConnectionId;
 use actix::prelude::*;
 use diesel::{
   r2d2::{ConnectionManager, Pool},
@@ -10,7 +9,6 @@ use log::{error, info};
 use rand::{rngs::ThreadRng, Rng};
 use serde::{Deserialize, Serialize};
 use serde_json::Value;
-use server::ChatServer;
 use std::{
   collections::{HashMap, HashSet},
   str::FromStr,
@@ -20,6 +18,7 @@ use std::{
 pub enum UserOperation {
   Login,
   Register,
+  GetCaptcha,
   CreateCommunity,
   CreatePost,
   ListCommunities,
@@ -28,19 +27,28 @@ pub enum UserOperation {
   GetCommunity,
   CreateComment,
   EditComment,
+  DeleteComment,
+  RemoveComment,
+  MarkCommentAsRead,
   SaveComment,
   CreateCommentLike,
   GetPosts,
   CreatePostLike,
   EditPost,
+  DeletePost,
+  RemovePost,
+  LockPost,
+  StickyPost,
   SavePost,
   EditCommunity,
+  DeleteCommunity,
+  RemoveCommunity,
   FollowCommunity,
   GetFollowedCommunities,
   GetUserDetails,
   GetReplies,
   GetUserMentions,
-  EditUserMention,
+  MarkUserMentionAsRead,
   GetModlog,
   BanFromCommunity,
   AddModToCommunity,
@@ -59,15 +67,11 @@ pub enum UserOperation {
   PasswordChange,
   CreatePrivateMessage,
   EditPrivateMessage,
+  DeletePrivateMessage,
+  MarkPrivateMessageAsRead,
   GetPrivateMessages,
   UserJoin,
   GetComments,
   GetSiteConfig,
   SaveSiteConfig,
 }
-
-#[derive(Clone)]
-pub struct WebsocketInfo {
-  pub chatserver: Addr<ChatServer>,
-  pub id: Option<ConnectionId>,
-}
index aef0abb8a02896398da7c364feab42b0f87b6f4b..4d0a1c4d3afd8439cc2b10f2b9b858a399d4124c 100644 (file)
@@ -9,13 +9,16 @@ use crate::{
   websocket::UserOperation,
   CommunityId,
   ConnectionId,
-  DbPool,
   IPAddr,
+  LemmyContext,
   LemmyError,
   PostId,
   UserId,
 };
-use actix_web::client::Client;
+use actix_web::{client::Client, web};
+use anyhow::Context as acontext;
+use lemmy_db::naive_now;
+use lemmy_utils::location_info;
 
 /// Chat server sends this messages to session
 #[derive(Message)]
@@ -55,7 +58,7 @@ pub struct StandardMessage {
 pub struct SendAllMessage<Response> {
   pub op: UserOperation,
   pub response: Response,
-  pub my_id: Option<ConnectionId>,
+  pub websocket_id: Option<ConnectionId>,
 }
 
 #[derive(Message)]
@@ -64,7 +67,7 @@ pub struct SendUserRoomMessage<Response> {
   pub op: UserOperation,
   pub response: Response,
   pub recipient_id: UserId,
-  pub my_id: Option<ConnectionId>,
+  pub websocket_id: Option<ConnectionId>,
 }
 
 #[derive(Message)]
@@ -73,7 +76,7 @@ pub struct SendCommunityRoomMessage<Response> {
   pub op: UserOperation,
   pub response: Response,
   pub community_id: CommunityId,
-  pub my_id: Option<ConnectionId>,
+  pub websocket_id: Option<ConnectionId>,
 }
 
 #[derive(Message)]
@@ -81,7 +84,7 @@ pub struct SendCommunityRoomMessage<Response> {
 pub struct SendPost {
   pub op: UserOperation,
   pub post: PostResponse,
-  pub my_id: Option<ConnectionId>,
+  pub websocket_id: Option<ConnectionId>,
 }
 
 #[derive(Message)]
@@ -89,7 +92,7 @@ pub struct SendPost {
 pub struct SendComment {
   pub op: UserOperation,
   pub comment: CommentResponse,
-  pub my_id: Option<ConnectionId>,
+  pub websocket_id: Option<ConnectionId>,
 }
 
 #[derive(Message)]
@@ -134,6 +137,21 @@ pub struct SessionInfo {
   pub ip: IPAddr,
 }
 
+#[derive(Message, Debug)]
+#[rtype(result = "()")]
+pub struct CaptchaItem {
+  pub uuid: String,
+  pub answer: String,
+  pub expires: chrono::NaiveDateTime,
+}
+
+#[derive(Message)]
+#[rtype(bool)]
+pub struct CheckCaptcha {
+  pub uuid: String,
+  pub answer: String,
+}
+
 /// `ChatServer` manages chat rooms and responsible for coordinating chat
 /// session.
 pub struct ChatServer {
@@ -158,6 +176,9 @@ pub struct ChatServer {
   /// Rate limiting based on rate type and IP addr
   rate_limiter: RateLimit,
 
+  /// A list of the current captchas
+  captchas: Vec<CaptchaItem>,
+
   /// An HTTP Client
   client: Client,
 }
@@ -176,11 +197,16 @@ impl ChatServer {
       rng: rand::thread_rng(),
       pool,
       rate_limiter,
+      captchas: Vec::new(),
       client,
     }
   }
 
-  pub fn join_community_room(&mut self, community_id: CommunityId, id: ConnectionId) {
+  pub fn join_community_room(
+    &mut self,
+    community_id: CommunityId,
+    id: ConnectionId,
+  ) -> Result<(), LemmyError> {
     // remove session from all rooms
     for sessions in self.community_rooms.values_mut() {
       sessions.remove(&id);
@@ -200,11 +226,12 @@ impl ChatServer {
     self
       .community_rooms
       .get_mut(&community_id)
-      .unwrap()
+      .context(location_info!())?
       .insert(id);
+    Ok(())
   }
 
-  pub fn join_post_room(&mut self, post_id: PostId, id: ConnectionId) {
+  pub fn join_post_room(&mut self, post_id: PostId, id: ConnectionId) -> Result<(), LemmyError> {
     // remove session from all rooms
     for sessions in self.post_rooms.values_mut() {
       sessions.remove(&id);
@@ -212,6 +239,9 @@ impl ChatServer {
 
     // Also leave all communities
     // This avoids double messages
+    // TODO found a bug, whereby community messages like
+    // delete and remove aren't sent, because
+    // you left the community room
     for sessions in self.community_rooms.values_mut() {
       sessions.remove(&id);
     }
@@ -221,10 +251,16 @@ impl ChatServer {
       self.post_rooms.insert(post_id, HashSet::new());
     }
 
-    self.post_rooms.get_mut(&post_id).unwrap().insert(id);
+    self
+      .post_rooms
+      .get_mut(&post_id)
+      .context(location_info!())?
+      .insert(id);
+
+    Ok(())
   }
 
-  pub fn join_user_room(&mut self, user_id: UserId, id: ConnectionId) {
+  pub fn join_user_room(&mut self, user_id: UserId, id: ConnectionId) -> Result<(), LemmyError> {
     // remove session from all rooms
     for sessions in self.user_rooms.values_mut() {
       sessions.remove(&id);
@@ -235,7 +271,13 @@ impl ChatServer {
       self.user_rooms.insert(user_id, HashSet::new());
     }
 
-    self.user_rooms.get_mut(&user_id).unwrap().insert(id);
+    self
+      .user_rooms
+      .get_mut(&user_id)
+      .context(location_info!())?
+      .insert(id);
+
+    Ok(())
   }
 
   fn send_post_room_message<Response>(
@@ -243,7 +285,7 @@ impl ChatServer {
     op: &UserOperation,
     response: &Response,
     post_id: PostId,
-    my_id: Option<ConnectionId>,
+    websocket_id: Option<ConnectionId>,
   ) -> Result<(), LemmyError>
   where
     Response: Serialize,
@@ -251,7 +293,7 @@ impl ChatServer {
     let res_str = &to_json_string(op, response)?;
     if let Some(sessions) = self.post_rooms.get(&post_id) {
       for id in sessions {
-        if let Some(my_id) = my_id {
+        if let Some(my_id) = websocket_id {
           if *id == my_id {
             continue;
           }
@@ -267,7 +309,7 @@ impl ChatServer {
     op: &UserOperation,
     response: &Response,
     community_id: CommunityId,
-    my_id: Option<ConnectionId>,
+    websocket_id: Option<ConnectionId>,
   ) -> Result<(), LemmyError>
   where
     Response: Serialize,
@@ -275,7 +317,7 @@ impl ChatServer {
     let res_str = &to_json_string(op, response)?;
     if let Some(sessions) = self.community_rooms.get(&community_id) {
       for id in sessions {
-        if let Some(my_id) = my_id {
+        if let Some(my_id) = websocket_id {
           if *id == my_id {
             continue;
           }
@@ -290,14 +332,14 @@ impl ChatServer {
     &self,
     op: &UserOperation,
     response: &Response,
-    my_id: Option<ConnectionId>,
+    websocket_id: Option<ConnectionId>,
   ) -> Result<(), LemmyError>
   where
     Response: Serialize,
   {
     let res_str = &to_json_string(op, response)?;
     for id in self.sessions.keys() {
-      if let Some(my_id) = my_id {
+      if let Some(my_id) = websocket_id {
         if *id == my_id {
           continue;
         }
@@ -312,7 +354,7 @@ impl ChatServer {
     op: &UserOperation,
     response: &Response,
     recipient_id: UserId,
-    my_id: Option<ConnectionId>,
+    websocket_id: Option<ConnectionId>,
   ) -> Result<(), LemmyError>
   where
     Response: Serialize,
@@ -320,7 +362,7 @@ impl ChatServer {
     let res_str = &to_json_string(op, response)?;
     if let Some(sessions) = self.user_rooms.get(&recipient_id) {
       for id in sessions {
-        if let Some(my_id) = my_id {
+        if let Some(my_id) = websocket_id {
           if *id == my_id {
             continue;
           }
@@ -335,7 +377,7 @@ impl ChatServer {
     &self,
     user_operation: &UserOperation,
     comment: &CommentResponse,
-    my_id: Option<ConnectionId>,
+    websocket_id: Option<ConnectionId>,
   ) -> Result<(), LemmyError> {
     let mut comment_reply_sent = comment.clone();
     comment_reply_sent.comment.my_vote = None;
@@ -349,21 +391,26 @@ impl ChatServer {
       user_operation,
       &comment_post_sent,
       comment_post_sent.comment.post_id,
-      my_id,
+      websocket_id,
     )?;
 
     // Send it to the recipient(s) including the mentioned users
     for recipient_id in &comment_reply_sent.recipient_ids {
-      self.send_user_room_message(user_operation, &comment_reply_sent, *recipient_id, my_id)?;
+      self.send_user_room_message(
+        user_operation,
+        &comment_reply_sent,
+        *recipient_id,
+        websocket_id,
+      )?;
     }
 
     // Send it to the community too
-    self.send_community_room_message(user_operation, &comment_post_sent, 0, my_id)?;
+    self.send_community_room_message(user_operation, &comment_post_sent, 0, websocket_id)?;
     self.send_community_room_message(
       user_operation,
       &comment_post_sent,
       comment.comment.community_id,
-      my_id,
+      websocket_id,
     )?;
 
     Ok(())
@@ -373,7 +420,7 @@ impl ChatServer {
     &self,
     user_operation: &UserOperation,
     post: &PostResponse,
-    my_id: Option<ConnectionId>,
+    websocket_id: Option<ConnectionId>,
   ) -> Result<(), LemmyError> {
     let community_id = post.post.community_id;
 
@@ -383,11 +430,11 @@ impl ChatServer {
     post_sent.post.user_id = None;
 
     // Send it to /c/all and that community
-    self.send_community_room_message(user_operation, &post_sent, 0, my_id)?;
-    self.send_community_room_message(user_operation, &post_sent, community_id, my_id)?;
+    self.send_community_room_message(user_operation, &post_sent, 0, websocket_id)?;
+    self.send_community_room_message(user_operation, &post_sent, community_id, websocket_id)?;
 
     // Send it to the post room
-    self.send_post_room_message(user_operation, &post_sent, post.post.id, my_id)?;
+    self.send_post_room_message(user_operation, &post_sent, post.post.id, websocket_id)?;
 
     Ok(())
   }
@@ -423,11 +470,14 @@ impl ChatServer {
 
       let user_operation: UserOperation = UserOperation::from_str(&op)?;
 
-      let args = Args {
-        client,
+      let context = LemmyContext {
         pool,
+        chat_server: addr,
+        client,
+      };
+      let args = Args {
+        context,
         rate_limiter,
-        chatserver: addr,
         id: msg.id,
         ip,
         op: user_operation.clone(),
@@ -438,23 +488,34 @@ impl ChatServer {
         // User ops
         UserOperation::Login => do_user_operation::<Login>(args).await,
         UserOperation::Register => do_user_operation::<Register>(args).await,
+        UserOperation::GetCaptcha => do_user_operation::<GetCaptcha>(args).await,
         UserOperation::GetUserDetails => do_user_operation::<GetUserDetails>(args).await,
         UserOperation::GetReplies => do_user_operation::<GetReplies>(args).await,
         UserOperation::AddAdmin => do_user_operation::<AddAdmin>(args).await,
         UserOperation::BanUser => do_user_operation::<BanUser>(args).await,
         UserOperation::GetUserMentions => do_user_operation::<GetUserMentions>(args).await,
-        UserOperation::EditUserMention => do_user_operation::<EditUserMention>(args).await,
+        UserOperation::MarkUserMentionAsRead => {
+          do_user_operation::<MarkUserMentionAsRead>(args).await
+        }
         UserOperation::MarkAllAsRead => do_user_operation::<MarkAllAsRead>(args).await,
         UserOperation::DeleteAccount => do_user_operation::<DeleteAccount>(args).await,
         UserOperation::PasswordReset => do_user_operation::<PasswordReset>(args).await,
         UserOperation::PasswordChange => do_user_operation::<PasswordChange>(args).await,
+        UserOperation::UserJoin => do_user_operation::<UserJoin>(args).await,
+        UserOperation::SaveUserSettings => do_user_operation::<SaveUserSettings>(args).await,
+
+        // Private Message ops
         UserOperation::CreatePrivateMessage => {
           do_user_operation::<CreatePrivateMessage>(args).await
         }
         UserOperation::EditPrivateMessage => do_user_operation::<EditPrivateMessage>(args).await,
+        UserOperation::DeletePrivateMessage => {
+          do_user_operation::<DeletePrivateMessage>(args).await
+        }
+        UserOperation::MarkPrivateMessageAsRead => {
+          do_user_operation::<MarkPrivateMessageAsRead>(args).await
+        }
         UserOperation::GetPrivateMessages => do_user_operation::<GetPrivateMessages>(args).await,
-        UserOperation::UserJoin => do_user_operation::<UserJoin>(args).await,
-        UserOperation::SaveUserSettings => do_user_operation::<SaveUserSettings>(args).await,
 
         // Site ops
         UserOperation::GetModlog => do_user_operation::<GetModlog>(args).await,
@@ -473,6 +534,8 @@ impl ChatServer {
         UserOperation::ListCommunities => do_user_operation::<ListCommunities>(args).await,
         UserOperation::CreateCommunity => do_user_operation::<CreateCommunity>(args).await,
         UserOperation::EditCommunity => do_user_operation::<EditCommunity>(args).await,
+        UserOperation::DeleteCommunity => do_user_operation::<DeleteCommunity>(args).await,
+        UserOperation::RemoveCommunity => do_user_operation::<RemoveCommunity>(args).await,
         UserOperation::FollowCommunity => do_user_operation::<FollowCommunity>(args).await,
         UserOperation::GetFollowedCommunities => {
           do_user_operation::<GetFollowedCommunities>(args).await
@@ -485,12 +548,19 @@ impl ChatServer {
         UserOperation::GetPost => do_user_operation::<GetPost>(args).await,
         UserOperation::GetPosts => do_user_operation::<GetPosts>(args).await,
         UserOperation::EditPost => do_user_operation::<EditPost>(args).await,
+        UserOperation::DeletePost => do_user_operation::<DeletePost>(args).await,
+        UserOperation::RemovePost => do_user_operation::<RemovePost>(args).await,
+        UserOperation::LockPost => do_user_operation::<LockPost>(args).await,
+        UserOperation::StickyPost => do_user_operation::<StickyPost>(args).await,
         UserOperation::CreatePostLike => do_user_operation::<CreatePostLike>(args).await,
         UserOperation::SavePost => do_user_operation::<SavePost>(args).await,
 
         // Comment ops
         UserOperation::CreateComment => do_user_operation::<CreateComment>(args).await,
         UserOperation::EditComment => do_user_operation::<EditComment>(args).await,
+        UserOperation::DeleteComment => do_user_operation::<DeleteComment>(args).await,
+        UserOperation::RemoveComment => do_user_operation::<RemoveComment>(args).await,
+        UserOperation::MarkCommentAsRead => do_user_operation::<MarkCommentAsRead>(args).await,
         UserOperation::SaveComment => do_user_operation::<SaveComment>(args).await,
         UserOperation::GetComments => do_user_operation::<GetComments>(args).await,
         UserOperation::CreateCommentLike => do_user_operation::<CreateCommentLike>(args).await,
@@ -500,10 +570,8 @@ impl ChatServer {
 }
 
 struct Args<'a> {
-  client: Client,
-  pool: DbPool,
+  context: LemmyContext,
   rate_limiter: RateLimit,
-  chatserver: Addr<ChatServer>,
   id: ConnectionId,
   ip: IPAddr,
   op: UserOperation,
@@ -513,33 +581,24 @@ struct Args<'a> {
 async fn do_user_operation<'a, 'b, Data>(args: Args<'b>) -> Result<String, LemmyError>
 where
   for<'de> Data: Deserialize<'de> + 'a,
-  Oper<Data>: Perform,
+  Data: Perform,
 {
   let Args {
-    client,
-    pool,
+    context,
     rate_limiter,
-    chatserver,
     id,
     ip,
     op,
     data,
   } = args;
 
-  let ws_info = WebsocketInfo {
-    chatserver,
-    id: Some(id),
-  };
-
   let data = data.to_string();
   let op2 = op.clone();
 
-  let client = client.clone();
   let fut = async move {
-    let pool = pool.clone();
     let parsed_data: Data = serde_json::from_str(&data)?;
-    let res = Oper::new(parsed_data, client)
-      .perform(&pool, Some(ws_info))
+    let res = parsed_data
+      .perform(&web::Data::new(context), Some(id))
       .await?;
     to_json_string(&op, &res)
   };
@@ -613,7 +672,7 @@ impl Handler<StandardMessage> for ChatServer {
     Box::pin(async move {
       match fut.await {
         Ok(m) => {
-          info!("Message Sent: {}", m);
+          // info!("Message Sent: {}", m);
           Ok(m)
         }
         Err(e) => {
@@ -633,8 +692,8 @@ where
 
   fn handle(&mut self, msg: SendAllMessage<Response>, _: &mut Context<Self>) {
     self
-      .send_all_message(&msg.op, &msg.response, msg.my_id)
-      .unwrap();
+      .send_all_message(&msg.op, &msg.response, msg.websocket_id)
+      .ok();
   }
 }
 
@@ -646,8 +705,8 @@ where
 
   fn handle(&mut self, msg: SendUserRoomMessage<Response>, _: &mut Context<Self>) {
     self
-      .send_user_room_message(&msg.op, &msg.response, msg.recipient_id, msg.my_id)
-      .unwrap();
+      .send_user_room_message(&msg.op, &msg.response, msg.recipient_id, msg.websocket_id)
+      .ok();
   }
 }
 
@@ -659,8 +718,8 @@ where
 
   fn handle(&mut self, msg: SendCommunityRoomMessage<Response>, _: &mut Context<Self>) {
     self
-      .send_community_room_message(&msg.op, &msg.response, msg.community_id, msg.my_id)
-      .unwrap();
+      .send_community_room_message(&msg.op, &msg.response, msg.community_id, msg.websocket_id)
+      .ok();
   }
 }
 
@@ -668,7 +727,7 @@ impl Handler<SendPost> for ChatServer {
   type Result = ();
 
   fn handle(&mut self, msg: SendPost, _: &mut Context<Self>) {
-    self.send_post(&msg.op, &msg.post, msg.my_id).unwrap();
+    self.send_post(&msg.op, &msg.post, msg.websocket_id).ok();
   }
 }
 
@@ -676,7 +735,9 @@ impl Handler<SendComment> for ChatServer {
   type Result = ();
 
   fn handle(&mut self, msg: SendComment, _: &mut Context<Self>) {
-    self.send_comment(&msg.op, &msg.comment, msg.my_id).unwrap();
+    self
+      .send_comment(&msg.op, &msg.comment, msg.websocket_id)
+      .ok();
   }
 }
 
@@ -684,7 +745,7 @@ impl Handler<JoinUserRoom> for ChatServer {
   type Result = ();
 
   fn handle(&mut self, msg: JoinUserRoom, _: &mut Context<Self>) {
-    self.join_user_room(msg.user_id, msg.id);
+    self.join_user_room(msg.user_id, msg.id).ok();
   }
 }
 
@@ -692,7 +753,7 @@ impl Handler<JoinCommunityRoom> for ChatServer {
   type Result = ();
 
   fn handle(&mut self, msg: JoinCommunityRoom, _: &mut Context<Self>) {
-    self.join_community_room(msg.community_id, msg.id);
+    self.join_community_room(msg.community_id, msg.id).ok();
   }
 }
 
@@ -700,7 +761,7 @@ impl Handler<JoinPostRoom> for ChatServer {
   type Result = ();
 
   fn handle(&mut self, msg: JoinPostRoom, _: &mut Context<Self>) {
-    self.join_post_room(msg.post_id, msg.id);
+    self.join_post_room(msg.post_id, msg.id).ok();
   }
 }
 
@@ -752,3 +813,30 @@ where
   };
   Ok(serde_json::to_string(&response)?)
 }
+
+impl Handler<CaptchaItem> for ChatServer {
+  type Result = ();
+
+  fn handle(&mut self, msg: CaptchaItem, _: &mut Context<Self>) {
+    self.captchas.push(msg);
+  }
+}
+
+impl Handler<CheckCaptcha> for ChatServer {
+  type Result = bool;
+
+  fn handle(&mut self, msg: CheckCaptcha, _: &mut Context<Self>) -> Self::Result {
+    // Remove all the ones that are past the expire time
+    self.captchas.retain(|x| x.expires.gt(&naive_now()));
+
+    let check = self
+      .captchas
+      .iter()
+      .any(|r| r.uuid == msg.uuid && r.answer == msg.answer);
+
+    // Remove this uuid so it can't be re-checked (Checks only work once)
+    self.captchas.retain(|x| x.uuid != msg.uuid);
+
+    check
+  }
+}
index 9f744fb1c837c5530e1ce4fb8e9ae2e7f61cfaf7..5367c62226c2a40d0371279dea0f9ba0a7442312 100644 (file)
@@ -2,6 +2,11 @@
   border: 0px;
 }
 
+.navbar-expand-lg .navbar-nav .nav-link {
+  padding-right: .75rem !important;
+  padding-left: .75rem !important;
+}
+
 .pointer {
   cursor: pointer;
 }
   margin-top: -10px;
 }
 
+.custom-select {
+  -moz-appearance: none;
+}
+
 .md-div p {
   overflow: hidden;
   text-overflow: ellipsis;
   line-height: 1.0;
 }
 
+.post-title a:visited {
+  color: var(--gray) !important;
+}
+
 .icon {
   display: inline-flex;
   width: 1em;
@@ -134,12 +147,14 @@ blockquote {
 
 .thumbnail {
   object-fit: cover;
+  min-height: 60px;
   max-height: 80px;
   width: 100%;
 }
 
-svg.thumbnail {
-  height: 40px;
+.thumbnail svg {
+  height: 1.25rem;
+  width: 1.25rem;
 }
 
 .no-s-hows {
@@ -271,3 +286,19 @@ br.big {
   margin-top: 1rem;
 }
 
+.banner {
+  object-fit: cover;
+  width: 100%;
+  max-height: 240px;
+}
+
+.avatar-overlay {
+  width: 20%; 
+  height: 20%;
+  max-width: 120px;
+  max-height: 120px;
+}
+
+.avatar-pushup {
+  margin-top: -60px;
+}
diff --git a/ui/assets/css/themes/_variables.darkly.scss b/ui/assets/css/themes/_variables.darkly.scss
new file mode 100644 (file)
index 0000000..870e42e
--- /dev/null
@@ -0,0 +1,104 @@
+
+$white: #fff;
+$gray-100: #f8f9fa;
+$gray-200: #ebebeb;
+$gray-300: #dee2e6;
+$gray-400: #ced4da;
+$gray-500: #adb5bd;
+$gray-600: #888;
+$gray-700: #444;
+$gray-800: #303030;
+$gray-900: #222;
+$black: #000;
+$blue: #375a7f;
+$indigo: #6610f2;
+$purple: #6f42c1;
+$pink: #e83e8c;
+$red: #e74c3c;
+$orange: #fd7e14;
+$yellow: #f39c12;
+$green: #00bc8c;
+$teal: #20c997;
+$cyan: #3498db;
+$primary: $blue;
+$secondary: $gray-700;
+$success: $green;
+$info: $cyan;
+$warning: $yellow;
+$danger: $red;
+$dark: $gray-300;
+$yiq-contrasted-threshold: 175;
+$body-bg: $gray-900;
+$body-color: $gray-300;
+$link-color: $success;
+$font-family-sans-serif: "Lato", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
+$font-size-base: 0.9375rem;
+$h1-font-size: 3rem;
+$h2-font-size: 2.5rem;
+$h3-font-size: 2rem;
+$text-muted: $gray-600;
+$table-accent-bg: $gray-800;
+$table-border-color: $gray-700;
+$input-border-color: $body-bg;
+$input-group-addon-color: $gray-500;
+$input-group-addon-bg: $gray-700;
+$custom-file-color: $gray-500;
+$custom-file-border-color: $body-bg;
+$dropdown-bg: $gray-900;
+$dropdown-border-color: $gray-700;
+$dropdown-divider-bg: $gray-700;
+$dropdown-link-color: $white;
+$dropdown-link-hover-color: $white;
+$dropdown-link-hover-bg: $primary;
+$nav-link-padding-x: 2rem;
+$nav-link-disabled-color: $gray-500;
+$nav-tabs-border-color: $gray-700;
+$nav-tabs-link-hover-border-color: $nav-tabs-border-color $nav-tabs-border-color transparent;
+$nav-tabs-link-active-color: $white;
+$nav-tabs-link-active-border-color: $nav-tabs-border-color $nav-tabs-border-color transparent;
+$navbar-padding-y: 1rem;
+$navbar-dark-color: rgba($white,.6);
+$navbar-dark-hover-color: $white;
+$navbar-light-color: rgba($white,.6);
+$navbar-light-hover-color: $white;
+$navbar-light-active-color: $white;
+$navbar-light-toggler-border-color: rgba($gray-900, .1);
+$pagination-color: $white;
+$pagination-bg: $success;
+$pagination-border-width: 0;
+$pagination-border-color: transparent;
+$pagination-hover-color: $white;
+$pagination-hover-bg: lighten($success, 10%);
+$pagination-hover-border-color: transparent;
+$pagination-active-bg: $pagination-hover-bg;
+$pagination-active-border-color: transparent;
+$pagination-disabled-color: $white;
+$pagination-disabled-bg: darken($success, 15%);
+$pagination-disabled-border-color: transparent;
+$jumbotron-bg: $gray-800;
+$card-cap-bg: $gray-700;
+$card-bg: $gray-800;
+$popover-bg: $gray-800;
+$popover-header-bg: $gray-700;
+$toast-background-color: $gray-700;
+$toast-header-background-color: $gray-800;
+$modal-content-bg: $gray-800;
+$modal-content-border-color: $gray-700;
+$modal-header-border-color: $gray-700;
+$progress-bg: $gray-700;
+$list-group-bg: $gray-800;
+$list-group-border-color: $gray-700;
+$list-group-hover-bg: $gray-700;
+$breadcrumb-bg: $gray-700;
+$close-color: $white;
+$close-text-shadow: none;
+$pre-color: inherit;
+$mark-bg: #333;
+$custom-select-bg: $secondary;
+$custom-select-color: $white;
+$input-bg: $secondary;
+$input-color: $white;
+$input-disabled-bg: darken($secondary, 10%);;
+$light: $gray-800;
+$navbar-light-brand-color: $navbar-dark-active-color;
+$navbar-light-brand-hover-color: $navbar-dark-active-color;
\ No newline at end of file
index 4ebd5de8f837bf48bc701335872857a8547bde47..c058135a9de33a1bebf1d0ccf2de54e82b809223 100644 (file)
@@ -1,17 +1,17 @@
 
 $white: #ffffff;
-$orange: #faa077;
+$orange: #f1641e;
 $cyan: #02bdc2;
-$green: #d4e9d7;
+$green: #00C853;
 $secondary: $green;
 $body-color: $gray-700;
-$link-color: theme-color("danger");;
+$link-color: theme-color("primary");;
 $primary: $orange;
 $red: #d8486a;
-$border-radius: 1.5rem;
-$border-radius-lg: 1.5rem;
+$border-radius: 0.5rem;
+$border-radius-lg: 0.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";
+$font-family-sans-serif: -apple-system,BlinkMacSystemFont,"Droid Sans","Segoe UI","Helvetica",Arial,sans-serif;
 $headings-color: $gray-700;
 $input-btn-focus-color: rgba($component-active-bg, .75);
 $form-feedback-valid-color: theme-color("info");
@@ -21,10 +21,13 @@ $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%);;
+$info: $blue;
+$body-bg: #fff;
+$success: $indigo;
 $danger: darken($primary, 25%);
 $navbar-light-hover-color: $gray-900;
 $card-bg: $gray-100;
-$border-color: $gray-700;
\ No newline at end of file
+$border-color: $gray-700;
+$mark-bg: rgb(255, 252, 239);
+$font-weight-bold: 600;
+$rounded-pill: 0.25rem;
index b3a48c374df5b1ba508c0dbc1b6836f27d30f5b7..de1f69849ca6f3e37bef9a48617a50636398cee8 100644 (file)
@@ -1,35 +1 @@
-/*!
- * Bootswatch v4.3.1
- * Homepage: https://bootswatch.com
- * Copyright 2012-2019 Thomas Park
- * Licensed under MIT
- * Based on Bootstrap
-*//*!
- * Bootstrap v4.3.1 (https://getbootstrap.com/)
- * Copyright 2011-2019 The Bootstrap Authors
- * Copyright 2011-2019 Twitter, Inc.
- * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
- */:root{--blue: #375a7f;--indigo: #6610f2;--purple: #6f42c1;--pink: #e83e8c;--red: #E74C3C;--orange: #fd7e14;--yellow: #F39C12;--green: #00bc8c;--teal: #20c997;--cyan: #3498DB;--white: #fff;--gray: #999;--gray-dark: #303030;--primary: #375a7f;--secondary: #444;--success: #00bc8c;--info: #3498DB;--warning: #F39C12;--danger: #E74C3C;--light: #303030;--dark: #adb5bd;--breakpoint-xs: 0;--breakpoint-sm: 576px;--breakpoint-md: 768px;--breakpoint-lg: 992px;--breakpoint-xl: 1200px;--font-family-sans-serif: "Lato", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";--font-family-monospace: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace}*,*::before,*::after{-webkit-box-sizing:border-box;box-sizing:border-box}html{font-family:sans-serif;line-height:1.15;-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:rgba(0,0,0,0)}article,aside,figcaption,figure,footer,header,hgroup,main,nav,section{display:block}body{margin:0;font-family:"Lato", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";font-size:0.9375rem;font-weight:400;line-height:1.5;color:#fff;text-align:left;background-color:#222}[tabindex="-1"]:focus{outline:0 !important}hr{-webkit-box-sizing:content-box;box-sizing:content-box;height:0;overflow:visible}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:0.5rem}p{margin-top:0;margin-bottom:1rem}abbr[title],abbr[data-original-title]{text-decoration:underline;-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;border-bottom:0;text-decoration-skip-ink:none}address{margin-bottom:1rem;font-style:normal;line-height:inherit}ol,ul,dl{margin-top:0;margin-bottom:1rem}ol ol,ul ul,ol ul,ul ol{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}b,strong{font-weight:bolder}small{font-size:80%}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:#00bc8c;text-decoration:none;background-color:transparent}a:hover{color:#007053;text-decoration:underline}a:not([href]):not([tabindex]){color:inherit;text-decoration:none}a:not([href]):not([tabindex]):hover,a:not([href]):not([tabindex]):focus{color:inherit;text-decoration:none}a:not([href]):not([tabindex]):focus{outline:0}pre,code,kbd,samp{font-family:SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;font-size:1em}pre{margin-top:0;margin-bottom:1rem;overflow:auto}figure{margin:0 0 1rem}img{vertical-align:middle;border-style:none}svg{overflow:hidden;vertical-align:middle}table{border-collapse:collapse}caption{padding-top:0.75rem;padding-bottom:0.75rem;color:#999;text-align:left;caption-side:bottom}th{text-align:inherit}label{display:inline-block;margin-bottom:0.5rem}button{border-radius:0}button:focus{outline:1px dotted;outline:5px auto -webkit-focus-ring-color}input,button,select,optgroup,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,input{overflow:visible}button,select{text-transform:none}select{word-wrap:normal}button,[type="button"],[type="reset"],[type="submit"]{-webkit-appearance:button}button:not(:disabled),[type="button"]:not(:disabled),[type="reset"]:not(:disabled),[type="submit"]:not(:disabled){cursor:pointer}button::-moz-focus-inner,[type="button"]::-moz-focus-inner,[type="reset"]::-moz-focus-inner,[type="submit"]::-moz-focus-inner{padding:0;border-style:none}input[type="radio"],input[type="checkbox"]{-webkit-box-sizing:border-box;box-sizing:border-box;padding:0}input[type="date"],input[type="time"],input[type="datetime-local"],input[type="month"]{-webkit-appearance:listbox}textarea{overflow:auto;resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{display:block;width:100%;max-width:100%;padding:0;margin-bottom:.5rem;font-size:1.5rem;line-height:inherit;color:inherit;white-space:normal}progress{vertical-align:baseline}[type="number"]::-webkit-inner-spin-button,[type="number"]::-webkit-outer-spin-button{height:auto}[type="search"]{outline-offset:-2px;-webkit-appearance:none}[type="search"]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}output{display:inline-block}summary{display:list-item;cursor:pointer}template{display:none}[hidden]{display:none !important}h1,h2,h3,h4,h5,h6,.h1,.h2,.h3,.h4,.h5,.h6{margin-bottom:0.5rem;font-weight:500;line-height:1.2}h1,.h1{font-size:3rem}h2,.h2{font-size:2.5rem}h3,.h3{font-size:2rem}h4,.h4{font-size:1.40625rem}h5,.h5{font-size:1.171875rem}h6,.h6{font-size:0.9375rem}.lead{font-size:1.171875rem;font-weight:300}.display-1{font-size:6rem;font-weight:300;line-height:1.2}.display-2{font-size:5.5rem;font-weight:300;line-height:1.2}.display-3{font-size:4.5rem;font-weight:300;line-height:1.2}.display-4{font-size:3.5rem;font-weight:300;line-height:1.2}hr{margin-top:1rem;margin-bottom:1rem;border:0;border-top:1px solid rgba(0,0,0,0.1)}small,.small{font-size:80%;font-weight:400}mark,.mark{padding:0.2em;background-color:#fcf8e3}.list-unstyled{padding-left:0;list-style:none}.list-inline{padding-left:0;list-style:none}.list-inline-item{display:inline-block}.list-inline-item:not(:last-child){margin-right:0.5rem}.initialism{font-size:90%;text-transform:uppercase}.blockquote{margin-bottom:1rem;font-size:1.171875rem}.blockquote-footer{display:block;font-size:80%;color:#999}.blockquote-footer::before{content:"\2014\00A0"}.img-fluid{max-width:100%;height:auto}.img-thumbnail{padding:0.25rem;background-color:#222;border:1px solid #dee2e6;border-radius:0.25rem;max-width:100%;height:auto}.figure{display:inline-block}.figure-img{margin-bottom:0.5rem;line-height:1}.figure-caption{font-size:90%;color:#999}code{font-size:87.5%;color:#e83e8c;word-break:break-word}a>code{color:inherit}kbd{padding:0.2rem 0.4rem;font-size:87.5%;color:#fff;background-color:#222;border-radius:0.2rem}kbd kbd{padding:0;font-size:100%;font-weight:700}pre{display:block;font-size:87.5%;color:inherit}pre code{font-size:inherit;color:inherit;word-break:normal}.pre-scrollable{max-height:340px;overflow-y:scroll}.container{width:100%;padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}@media (min-width: 576px){.container{max-width:540px}}@media (min-width: 768px){.container{max-width:720px}}@media (min-width: 992px){.container{max-width:960px}}@media (min-width: 1200px){.container{max-width:1140px}}.container-fluid{width:100%;padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}.row{display:-webkit-box;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;margin-right:-15px;margin-left:-15px}.no-gutters{margin-right:0;margin-left:0}.no-gutters>.col,.no-gutters>[class*="col-"]{padding-right:0;padding-left:0}.col-1,.col-2,.col-3,.col-4,.col-5,.col-6,.col-7,.col-8,.col-9,.col-10,.col-11,.col-12,.col,.col-auto,.col-sm-1,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9,.col-sm-10,.col-sm-11,.col-sm-12,.col-sm,.col-sm-auto,.col-md-1,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9,.col-md-10,.col-md-11,.col-md-12,.col-md,.col-md-auto,.col-lg-1,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9,.col-lg-10,.col-lg-11,.col-lg-12,.col-lg,.col-lg-auto,.col-xl-1,.col-xl-2,.col-xl-3,.col-xl-4,.col-xl-5,.col-xl-6,.col-xl-7,.col-xl-8,.col-xl-9,.col-xl-10,.col-xl-11,.col-xl-12,.col-xl,.col-xl-auto{position:relative;width:100%;padding-right:15px;padding-left:15px}.col{-ms-flex-preferred-size:0;flex-basis:0;-webkit-box-flex:1;-ms-flex-positive:1;flex-grow:1;max-width:100%}.col-auto{-webkit-box-flex:0;-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:100%}.col-1{-webkit-box-flex:0;-ms-flex:0 0 8.3333333333%;flex:0 0 8.3333333333%;max-width:8.3333333333%}.col-2{-webkit-box-flex:0;-ms-flex:0 0 16.6666666667%;flex:0 0 16.6666666667%;max-width:16.6666666667%}.col-3{-webkit-box-flex:0;-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-4{-webkit-box-flex:0;-ms-flex:0 0 33.3333333333%;flex:0 0 33.3333333333%;max-width:33.3333333333%}.col-5{-webkit-box-flex:0;-ms-flex:0 0 41.6666666667%;flex:0 0 41.6666666667%;max-width:41.6666666667%}.col-6{-webkit-box-flex:0;-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-7{-webkit-box-flex:0;-ms-flex:0 0 58.3333333333%;flex:0 0 58.3333333333%;max-width:58.3333333333%}.col-8{-webkit-box-flex:0;-ms-flex:0 0 66.6666666667%;flex:0 0 66.6666666667%;max-width:66.6666666667%}.col-9{-webkit-box-flex:0;-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-10{-webkit-box-flex:0;-ms-flex:0 0 83.3333333333%;flex:0 0 83.3333333333%;max-width:83.3333333333%}.col-11{-webkit-box-flex:0;-ms-flex:0 0 91.6666666667%;flex:0 0 91.6666666667%;max-width:91.6666666667%}.col-12{-webkit-box-flex:0;-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-first{-webkit-box-ordinal-group:0;-ms-flex-order:-1;order:-1}.order-last{-webkit-box-ordinal-group:14;-ms-flex-order:13;order:13}.order-0{-webkit-box-ordinal-group:1;-ms-flex-order:0;order:0}.order-1{-webkit-box-ordinal-group:2;-ms-flex-order:1;order:1}.order-2{-webkit-box-ordinal-group:3;-ms-flex-order:2;order:2}.order-3{-webkit-box-ordinal-group:4;-ms-flex-order:3;order:3}.order-4{-webkit-box-ordinal-group:5;-ms-flex-order:4;order:4}.order-5{-webkit-box-ordinal-group:6;-ms-flex-order:5;order:5}.order-6{-webkit-box-ordinal-group:7;-ms-flex-order:6;order:6}.order-7{-webkit-box-ordinal-group:8;-ms-flex-order:7;order:7}.order-8{-webkit-box-ordinal-group:9;-ms-flex-order:8;order:8}.order-9{-webkit-box-ordinal-group:10;-ms-flex-order:9;order:9}.order-10{-webkit-box-ordinal-group:11;-ms-flex-order:10;order:10}.order-11{-webkit-box-ordinal-group:12;-ms-flex-order:11;order:11}.order-12{-webkit-box-ordinal-group:13;-ms-flex-order:12;order:12}.offset-1{margin-left:8.3333333333%}.offset-2{margin-left:16.6666666667%}.offset-3{margin-left:25%}.offset-4{margin-left:33.3333333333%}.offset-5{margin-left:41.6666666667%}.offset-6{margin-left:50%}.offset-7{margin-left:58.3333333333%}.offset-8{margin-left:66.6666666667%}.offset-9{margin-left:75%}.offset-10{margin-left:83.3333333333%}.offset-11{margin-left:91.6666666667%}@media (min-width: 576px){.col-sm{-ms-flex-preferred-size:0;flex-basis:0;-webkit-box-flex:1;-ms-flex-positive:1;flex-grow:1;max-width:100%}.col-sm-auto{-webkit-box-flex:0;-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:100%}.col-sm-1{-webkit-box-flex:0;-ms-flex:0 0 8.3333333333%;flex:0 0 8.3333333333%;max-width:8.3333333333%}.col-sm-2{-webkit-box-flex:0;-ms-flex:0 0 16.6666666667%;flex:0 0 16.6666666667%;max-width:16.6666666667%}.col-sm-3{-webkit-box-flex:0;-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-sm-4{-webkit-box-flex:0;-ms-flex:0 0 33.3333333333%;flex:0 0 33.3333333333%;max-width:33.3333333333%}.col-sm-5{-webkit-box-flex:0;-ms-flex:0 0 41.6666666667%;flex:0 0 41.6666666667%;max-width:41.6666666667%}.col-sm-6{-webkit-box-flex:0;-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-sm-7{-webkit-box-flex:0;-ms-flex:0 0 58.3333333333%;flex:0 0 58.3333333333%;max-width:58.3333333333%}.col-sm-8{-webkit-box-flex:0;-ms-flex:0 0 66.6666666667%;flex:0 0 66.6666666667%;max-width:66.6666666667%}.col-sm-9{-webkit-box-flex:0;-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-sm-10{-webkit-box-flex:0;-ms-flex:0 0 83.3333333333%;flex:0 0 83.3333333333%;max-width:83.3333333333%}.col-sm-11{-webkit-box-flex:0;-ms-flex:0 0 91.6666666667%;flex:0 0 91.6666666667%;max-width:91.6666666667%}.col-sm-12{-webkit-box-flex:0;-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-sm-first{-webkit-box-ordinal-group:0;-ms-flex-order:-1;order:-1}.order-sm-last{-webkit-box-ordinal-group:14;-ms-flex-order:13;order:13}.order-sm-0{-webkit-box-ordinal-group:1;-ms-flex-order:0;order:0}.order-sm-1{-webkit-box-ordinal-group:2;-ms-flex-order:1;order:1}.order-sm-2{-webkit-box-ordinal-group:3;-ms-flex-order:2;order:2}.order-sm-3{-webkit-box-ordinal-group:4;-ms-flex-order:3;order:3}.order-sm-4{-webkit-box-ordinal-group:5;-ms-flex-order:4;order:4}.order-sm-5{-webkit-box-ordinal-group:6;-ms-flex-order:5;order:5}.order-sm-6{-webkit-box-ordinal-group:7;-ms-flex-order:6;order:6}.order-sm-7{-webkit-box-ordinal-group:8;-ms-flex-order:7;order:7}.order-sm-8{-webkit-box-ordinal-group:9;-ms-flex-order:8;order:8}.order-sm-9{-webkit-box-ordinal-group:10;-ms-flex-order:9;order:9}.order-sm-10{-webkit-box-ordinal-group:11;-ms-flex-order:10;order:10}.order-sm-11{-webkit-box-ordinal-group:12;-ms-flex-order:11;order:11}.order-sm-12{-webkit-box-ordinal-group:13;-ms-flex-order:12;order:12}.offset-sm-0{margin-left:0}.offset-sm-1{margin-left:8.3333333333%}.offset-sm-2{margin-left:16.6666666667%}.offset-sm-3{margin-left:25%}.offset-sm-4{margin-left:33.3333333333%}.offset-sm-5{margin-left:41.6666666667%}.offset-sm-6{margin-left:50%}.offset-sm-7{margin-left:58.3333333333%}.offset-sm-8{margin-left:66.6666666667%}.offset-sm-9{margin-left:75%}.offset-sm-10{margin-left:83.3333333333%}.offset-sm-11{margin-left:91.6666666667%}}@media (min-width: 768px){.col-md{-ms-flex-preferred-size:0;flex-basis:0;-webkit-box-flex:1;-ms-flex-positive:1;flex-grow:1;max-width:100%}.col-md-auto{-webkit-box-flex:0;-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:100%}.col-md-1{-webkit-box-flex:0;-ms-flex:0 0 8.3333333333%;flex:0 0 8.3333333333%;max-width:8.3333333333%}.col-md-2{-webkit-box-flex:0;-ms-flex:0 0 16.6666666667%;flex:0 0 16.6666666667%;max-width:16.6666666667%}.col-md-3{-webkit-box-flex:0;-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-md-4{-webkit-box-flex:0;-ms-flex:0 0 33.3333333333%;flex:0 0 33.3333333333%;max-width:33.3333333333%}.col-md-5{-webkit-box-flex:0;-ms-flex:0 0 41.6666666667%;flex:0 0 41.6666666667%;max-width:41.6666666667%}.col-md-6{-webkit-box-flex:0;-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-md-7{-webkit-box-flex:0;-ms-flex:0 0 58.3333333333%;flex:0 0 58.3333333333%;max-width:58.3333333333%}.col-md-8{-webkit-box-flex:0;-ms-flex:0 0 66.6666666667%;flex:0 0 66.6666666667%;max-width:66.6666666667%}.col-md-9{-webkit-box-flex:0;-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-md-10{-webkit-box-flex:0;-ms-flex:0 0 83.3333333333%;flex:0 0 83.3333333333%;max-width:83.3333333333%}.col-md-11{-webkit-box-flex:0;-ms-flex:0 0 91.6666666667%;flex:0 0 91.6666666667%;max-width:91.6666666667%}.col-md-12{-webkit-box-flex:0;-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-md-first{-webkit-box-ordinal-group:0;-ms-flex-order:-1;order:-1}.order-md-last{-webkit-box-ordinal-group:14;-ms-flex-order:13;order:13}.order-md-0{-webkit-box-ordinal-group:1;-ms-flex-order:0;order:0}.order-md-1{-webkit-box-ordinal-group:2;-ms-flex-order:1;order:1}.order-md-2{-webkit-box-ordinal-group:3;-ms-flex-order:2;order:2}.order-md-3{-webkit-box-ordinal-group:4;-ms-flex-order:3;order:3}.order-md-4{-webkit-box-ordinal-group:5;-ms-flex-order:4;order:4}.order-md-5{-webkit-box-ordinal-group:6;-ms-flex-order:5;order:5}.order-md-6{-webkit-box-ordinal-group:7;-ms-flex-order:6;order:6}.order-md-7{-webkit-box-ordinal-group:8;-ms-flex-order:7;order:7}.order-md-8{-webkit-box-ordinal-group:9;-ms-flex-order:8;order:8}.order-md-9{-webkit-box-ordinal-group:10;-ms-flex-order:9;order:9}.order-md-10{-webkit-box-ordinal-group:11;-ms-flex-order:10;order:10}.order-md-11{-webkit-box-ordinal-group:12;-ms-flex-order:11;order:11}.order-md-12{-webkit-box-ordinal-group:13;-ms-flex-order:12;order:12}.offset-md-0{margin-left:0}.offset-md-1{margin-left:8.3333333333%}.offset-md-2{margin-left:16.6666666667%}.offset-md-3{margin-left:25%}.offset-md-4{margin-left:33.3333333333%}.offset-md-5{margin-left:41.6666666667%}.offset-md-6{margin-left:50%}.offset-md-7{margin-left:58.3333333333%}.offset-md-8{margin-left:66.6666666667%}.offset-md-9{margin-left:75%}.offset-md-10{margin-left:83.3333333333%}.offset-md-11{margin-left:91.6666666667%}}@media (min-width: 992px){.col-lg{-ms-flex-preferred-size:0;flex-basis:0;-webkit-box-flex:1;-ms-flex-positive:1;flex-grow:1;max-width:100%}.col-lg-auto{-webkit-box-flex:0;-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:100%}.col-lg-1{-webkit-box-flex:0;-ms-flex:0 0 8.3333333333%;flex:0 0 8.3333333333%;max-width:8.3333333333%}.col-lg-2{-webkit-box-flex:0;-ms-flex:0 0 16.6666666667%;flex:0 0 16.6666666667%;max-width:16.6666666667%}.col-lg-3{-webkit-box-flex:0;-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-lg-4{-webkit-box-flex:0;-ms-flex:0 0 33.3333333333%;flex:0 0 33.3333333333%;max-width:33.3333333333%}.col-lg-5{-webkit-box-flex:0;-ms-flex:0 0 41.6666666667%;flex:0 0 41.6666666667%;max-width:41.6666666667%}.col-lg-6{-webkit-box-flex:0;-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-lg-7{-webkit-box-flex:0;-ms-flex:0 0 58.3333333333%;flex:0 0 58.3333333333%;max-width:58.3333333333%}.col-lg-8{-webkit-box-flex:0;-ms-flex:0 0 66.6666666667%;flex:0 0 66.6666666667%;max-width:66.6666666667%}.col-lg-9{-webkit-box-flex:0;-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-lg-10{-webkit-box-flex:0;-ms-flex:0 0 83.3333333333%;flex:0 0 83.3333333333%;max-width:83.3333333333%}.col-lg-11{-webkit-box-flex:0;-ms-flex:0 0 91.6666666667%;flex:0 0 91.6666666667%;max-width:91.6666666667%}.col-lg-12{-webkit-box-flex:0;-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-lg-first{-webkit-box-ordinal-group:0;-ms-flex-order:-1;order:-1}.order-lg-last{-webkit-box-ordinal-group:14;-ms-flex-order:13;order:13}.order-lg-0{-webkit-box-ordinal-group:1;-ms-flex-order:0;order:0}.order-lg-1{-webkit-box-ordinal-group:2;-ms-flex-order:1;order:1}.order-lg-2{-webkit-box-ordinal-group:3;-ms-flex-order:2;order:2}.order-lg-3{-webkit-box-ordinal-group:4;-ms-flex-order:3;order:3}.order-lg-4{-webkit-box-ordinal-group:5;-ms-flex-order:4;order:4}.order-lg-5{-webkit-box-ordinal-group:6;-ms-flex-order:5;order:5}.order-lg-6{-webkit-box-ordinal-group:7;-ms-flex-order:6;order:6}.order-lg-7{-webkit-box-ordinal-group:8;-ms-flex-order:7;order:7}.order-lg-8{-webkit-box-ordinal-group:9;-ms-flex-order:8;order:8}.order-lg-9{-webkit-box-ordinal-group:10;-ms-flex-order:9;order:9}.order-lg-10{-webkit-box-ordinal-group:11;-ms-flex-order:10;order:10}.order-lg-11{-webkit-box-ordinal-group:12;-ms-flex-order:11;order:11}.order-lg-12{-webkit-box-ordinal-group:13;-ms-flex-order:12;order:12}.offset-lg-0{margin-left:0}.offset-lg-1{margin-left:8.3333333333%}.offset-lg-2{margin-left:16.6666666667%}.offset-lg-3{margin-left:25%}.offset-lg-4{margin-left:33.3333333333%}.offset-lg-5{margin-left:41.6666666667%}.offset-lg-6{margin-left:50%}.offset-lg-7{margin-left:58.3333333333%}.offset-lg-8{margin-left:66.6666666667%}.offset-lg-9{margin-left:75%}.offset-lg-10{margin-left:83.3333333333%}.offset-lg-11{margin-left:91.6666666667%}}@media (min-width: 1200px){.col-xl{-ms-flex-preferred-size:0;flex-basis:0;-webkit-box-flex:1;-ms-flex-positive:1;flex-grow:1;max-width:100%}.col-xl-auto{-webkit-box-flex:0;-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:100%}.col-xl-1{-webkit-box-flex:0;-ms-flex:0 0 8.3333333333%;flex:0 0 8.3333333333%;max-width:8.3333333333%}.col-xl-2{-webkit-box-flex:0;-ms-flex:0 0 16.6666666667%;flex:0 0 16.6666666667%;max-width:16.6666666667%}.col-xl-3{-webkit-box-flex:0;-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-xl-4{-webkit-box-flex:0;-ms-flex:0 0 33.3333333333%;flex:0 0 33.3333333333%;max-width:33.3333333333%}.col-xl-5{-webkit-box-flex:0;-ms-flex:0 0 41.6666666667%;flex:0 0 41.6666666667%;max-width:41.6666666667%}.col-xl-6{-webkit-box-flex:0;-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-xl-7{-webkit-box-flex:0;-ms-flex:0 0 58.3333333333%;flex:0 0 58.3333333333%;max-width:58.3333333333%}.col-xl-8{-webkit-box-flex:0;-ms-flex:0 0 66.6666666667%;flex:0 0 66.6666666667%;max-width:66.6666666667%}.col-xl-9{-webkit-box-flex:0;-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-xl-10{-webkit-box-flex:0;-ms-flex:0 0 83.3333333333%;flex:0 0 83.3333333333%;max-width:83.3333333333%}.col-xl-11{-webkit-box-flex:0;-ms-flex:0 0 91.6666666667%;flex:0 0 91.6666666667%;max-width:91.6666666667%}.col-xl-12{-webkit-box-flex:0;-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-xl-first{-webkit-box-ordinal-group:0;-ms-flex-order:-1;order:-1}.order-xl-last{-webkit-box-ordinal-group:14;-ms-flex-order:13;order:13}.order-xl-0{-webkit-box-ordinal-group:1;-ms-flex-order:0;order:0}.order-xl-1{-webkit-box-ordinal-group:2;-ms-flex-order:1;order:1}.order-xl-2{-webkit-box-ordinal-group:3;-ms-flex-order:2;order:2}.order-xl-3{-webkit-box-ordinal-group:4;-ms-flex-order:3;order:3}.order-xl-4{-webkit-box-ordinal-group:5;-ms-flex-order:4;order:4}.order-xl-5{-webkit-box-ordinal-group:6;-ms-flex-order:5;order:5}.order-xl-6{-webkit-box-ordinal-group:7;-ms-flex-order:6;order:6}.order-xl-7{-webkit-box-ordinal-group:8;-ms-flex-order:7;order:7}.order-xl-8{-webkit-box-ordinal-group:9;-ms-flex-order:8;order:8}.order-xl-9{-webkit-box-ordinal-group:10;-ms-flex-order:9;order:9}.order-xl-10{-webkit-box-ordinal-group:11;-ms-flex-order:10;order:10}.order-xl-11{-webkit-box-ordinal-group:12;-ms-flex-order:11;order:11}.order-xl-12{-webkit-box-ordinal-group:13;-ms-flex-order:12;order:12}.offset-xl-0{margin-left:0}.offset-xl-1{margin-left:8.3333333333%}.offset-xl-2{margin-left:16.6666666667%}.offset-xl-3{margin-left:25%}.offset-xl-4{margin-left:33.3333333333%}.offset-xl-5{margin-left:41.6666666667%}.offset-xl-6{margin-left:50%}.offset-xl-7{margin-left:58.3333333333%}.offset-xl-8{margin-left:66.6666666667%}.offset-xl-9{margin-left:75%}.offset-xl-10{margin-left:83.3333333333%}.offset-xl-11{margin-left:91.6666666667%}}.table{width:100%;margin-bottom:1rem;color:#fff}.table th,.table td{padding:0.75rem;vertical-align:top;border-top:1px solid #444}.table thead th{vertical-align:bottom;border-bottom:2px solid #444}.table tbody+tbody{border-top:2px solid #444}.table-sm th,.table-sm td{padding:0.3rem}.table-bordered{border:1px solid #444}.table-bordered th,.table-bordered td{border:1px solid #444}.table-bordered thead th,.table-bordered thead td{border-bottom-width:2px}.table-borderless th,.table-borderless td,.table-borderless thead th,.table-borderless tbody+tbody{border:0}.table-striped tbody tr:nth-of-type(odd){background-color:#303030}.table-hover tbody tr:hover{color:#fff;background-color:rgba(0,0,0,0.075)}.table-primary,.table-primary>th,.table-primary>td{background-color:#c7d1db}.table-primary th,.table-primary td,.table-primary thead th,.table-primary tbody+tbody{border-color:#97a9bc}.table-hover .table-primary:hover{background-color:#b7c4d1}.table-hover .table-primary:hover>td,.table-hover .table-primary:hover>th{background-color:#b7c4d1}.table-secondary,.table-secondary>th,.table-secondary>td{background-color:#cbcbcb}.table-secondary th,.table-secondary td,.table-secondary thead th,.table-secondary tbody+tbody{border-color:#9e9e9e}.table-hover .table-secondary:hover{background-color:#bebebe}.table-hover .table-secondary:hover>td,.table-hover .table-secondary:hover>th{background-color:#bebebe}.table-success,.table-success>th,.table-success>td{background-color:#b8ecdf}.table-success th,.table-success td,.table-success thead th,.table-success tbody+tbody{border-color:#7adcc3}.table-hover .table-success:hover{background-color:#a4e7d6}.table-hover .table-success:hover>td,.table-hover .table-success:hover>th{background-color:#a4e7d6}.table-info,.table-info>th,.table-info>td{background-color:#c6e2f5}.table-info th,.table-info td,.table-info thead th,.table-info tbody+tbody{border-color:#95c9ec}.table-hover .table-info:hover{background-color:#b0d7f1}.table-hover .table-info:hover>td,.table-hover .table-info:hover>th{background-color:#b0d7f1}.table-warning,.table-warning>th,.table-warning>td{background-color:#fce3bd}.table-warning th,.table-warning td,.table-warning thead th,.table-warning tbody+tbody{border-color:#f9cc84}.table-hover .table-warning:hover{background-color:#fbd9a5}.table-hover .table-warning:hover>td,.table-hover .table-warning:hover>th{background-color:#fbd9a5}.table-danger,.table-danger>th,.table-danger>td{background-color:#f8cdc8}.table-danger th,.table-danger td,.table-danger thead th,.table-danger tbody+tbody{border-color:#f3a29a}.table-hover .table-danger:hover{background-color:#f5b8b1}.table-hover .table-danger:hover>td,.table-hover .table-danger:hover>th{background-color:#f5b8b1}.table-light,.table-light>th,.table-light>td{background-color:#c5c5c5}.table-light th,.table-light td,.table-light thead th,.table-light tbody+tbody{border-color:#939393}.table-hover .table-light:hover{background-color:#b8b8b8}.table-hover .table-light:hover>td,.table-hover .table-light:hover>th{background-color:#b8b8b8}.table-dark,.table-dark>th,.table-dark>td{background-color:#e8eaed}.table-dark th,.table-dark td,.table-dark thead th,.table-dark tbody+tbody{border-color:#d4d9dd}.table-hover .table-dark:hover{background-color:#dadde2}.table-hover .table-dark:hover>td,.table-hover .table-dark:hover>th{background-color:#dadde2}.table-active,.table-active>th,.table-active>td{background-color:rgba(0,0,0,0.075)}.table-hover .table-active:hover{background-color:rgba(0,0,0,0.075)}.table-hover .table-active:hover>td,.table-hover .table-active:hover>th{background-color:rgba(0,0,0,0.075)}.table .thead-dark th{color:#222;background-color:#adb5bd;border-color:#98a2ac}.table .thead-light th{color:#444;background-color:#ebebeb;border-color:#444}.table-dark{color:#222;background-color:#adb5bd}.table-dark th,.table-dark td,.table-dark thead th{border-color:#98a2ac}.table-dark.table-bordered{border:0}.table-dark.table-striped tbody tr:nth-of-type(odd){background-color:rgba(255,255,255,0.05)}.table-dark.table-hover tbody tr:hover{color:#222;background-color:rgba(255,255,255,0.075)}@media (max-width: 575.98px){.table-responsive-sm{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch}.table-responsive-sm>.table-bordered{border:0}}@media (max-width: 767.98px){.table-responsive-md{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch}.table-responsive-md>.table-bordered{border:0}}@media (max-width: 991.98px){.table-responsive-lg{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch}.table-responsive-lg>.table-bordered{border:0}}@media (max-width: 1199.98px){.table-responsive-xl{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch}.table-responsive-xl>.table-bordered{border:0}}.table-responsive{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch}.table-responsive>.table-bordered{border:0}.form-control{display:block;width:100%;height:calc(1.5em + 0.75rem + 2px);padding:0.375rem 0.75rem;font-size:0.9375rem;font-weight:400;line-height:1.5;color:#444;background-color:#fff;background-clip:padding-box;border:1px solid transparent;border-radius:0.25rem;-webkit-transition:border-color 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out;transition:border-color 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out;transition:border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;transition:border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out}@media (prefers-reduced-motion: reduce){.form-control{-webkit-transition:none;transition:none}}.form-control::-ms-expand{background-color:transparent;border:0}.form-control:focus{color:#444;background-color:#fff;border-color:#739ac2;outline:0;-webkit-box-shadow:0 0 0 0.2rem rgba(55,90,127,0.25);box-shadow:0 0 0 0.2rem rgba(55,90,127,0.25)}.form-control::-webkit-input-placeholder{color:#999;opacity:1}.form-control::-ms-input-placeholder{color:#999;opacity:1}.form-control::placeholder{color:#999;opacity:1}.form-control:disabled,.form-control[readonly]{background-color:#ebebeb;opacity:1}select.form-control:focus::-ms-value{color:#444;background-color:#fff}.form-control-file,.form-control-range{display:block;width:100%}.col-form-label{padding-top:calc(0.375rem + 1px);padding-bottom:calc(0.375rem + 1px);margin-bottom:0;font-size:inherit;line-height:1.5}.col-form-label-lg{padding-top:calc(0.5rem + 1px);padding-bottom:calc(0.5rem + 1px);font-size:1.171875rem;line-height:1.5}.col-form-label-sm{padding-top:calc(0.25rem + 1px);padding-bottom:calc(0.25rem + 1px);font-size:0.8203125rem;line-height:1.5}.form-control-plaintext{display:block;width:100%;padding-top:0.375rem;padding-bottom:0.375rem;margin-bottom:0;line-height:1.5;color:#fff;background-color:transparent;border:solid transparent;border-width:1px 0}.form-control-plaintext.form-control-sm,.form-control-plaintext.form-control-lg{padding-right:0;padding-left:0}.form-control-sm{height:calc(1.5em + 0.5rem + 2px);padding:0.25rem 0.5rem;font-size:0.8203125rem;line-height:1.5;border-radius:0.2rem}.form-control-lg{height:calc(1.5em + 1rem + 2px);padding:0.5rem 1rem;font-size:1.171875rem;line-height:1.5;border-radius:0.3rem}select.form-control[size],select.form-control[multiple]{height:auto}textarea.form-control{height:auto}.form-group{margin-bottom:1rem}.form-text{display:block;margin-top:0.25rem}.form-row{display:-webkit-box;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;margin-right:-5px;margin-left:-5px}.form-row>.col,.form-row>[class*="col-"]{padding-right:5px;padding-left:5px}.form-check{position:relative;display:block;padding-left:1.25rem}.form-check-input{position:absolute;margin-top:0.3rem;margin-left:-1.25rem}.form-check-input:disabled ~ .form-check-label{color:#999}.form-check-label{margin-bottom:0}.form-check-inline{display:-webkit-inline-box;display:-ms-inline-flexbox;display:inline-flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;padding-left:0;margin-right:0.75rem}.form-check-inline .form-check-input{position:static;margin-top:0;margin-right:0.3125rem;margin-left:0}.valid-feedback{display:none;width:100%;margin-top:0.25rem;font-size:80%;color:#00bc8c}.valid-tooltip{position:absolute;top:100%;z-index:5;display:none;max-width:100%;padding:0.25rem 0.5rem;margin-top:.1rem;font-size:0.8203125rem;line-height:1.5;color:#fff;background-color:rgba(0,188,140,0.9);border-radius:0.25rem}.was-validated .form-control:valid,.form-control.is-valid{border-color:#00bc8c;padding-right:calc(1.5em + 0.75rem);background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%2300bc8c' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:center right calc(0.375em + 0.1875rem);background-size:calc(0.75em + 0.375rem) calc(0.75em + 0.375rem)}.was-validated .form-control:valid:focus,.form-control.is-valid:focus{border-color:#00bc8c;-webkit-box-shadow:0 0 0 0.2rem rgba(0,188,140,0.25);box-shadow:0 0 0 0.2rem rgba(0,188,140,0.25)}.was-validated .form-control:valid ~ .valid-feedback,.was-validated .form-control:valid ~ .valid-tooltip,.form-control.is-valid ~ .valid-feedback,.form-control.is-valid ~ .valid-tooltip{display:block}.was-validated textarea.form-control:valid,textarea.form-control.is-valid{padding-right:calc(1.5em + 0.75rem);background-position:top calc(0.375em + 0.1875rem) right calc(0.375em + 0.1875rem)}.was-validated .custom-select:valid,.custom-select.is-valid{border-color:#00bc8c;padding-right:calc((1em + 0.75rem) * 3 / 4 + 1.75rem);background:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 4 5'%3e%3cpath fill='%23303030' d='M2 0L0 2h4zm0 5L0 3h4z'/%3e%3c/svg%3e") no-repeat right 0.75rem center/8px 10px,url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%2300bc8c' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e") #fff no-repeat center right 1.75rem/calc(0.75em + 0.375rem) calc(0.75em + 0.375rem)}.was-validated .custom-select:valid:focus,.custom-select.is-valid:focus{border-color:#00bc8c;-webkit-box-shadow:0 0 0 0.2rem rgba(0,188,140,0.25);box-shadow:0 0 0 0.2rem rgba(0,188,140,0.25)}.was-validated .custom-select:valid ~ .valid-feedback,.was-validated .custom-select:valid ~ .valid-tooltip,.custom-select.is-valid ~ .valid-feedback,.custom-select.is-valid ~ .valid-tooltip{display:block}.was-validated .form-control-file:valid ~ .valid-feedback,.was-validated .form-control-file:valid ~ .valid-tooltip,.form-control-file.is-valid ~ .valid-feedback,.form-control-file.is-valid ~ .valid-tooltip{display:block}.was-validated .form-check-input:valid ~ .form-check-label,.form-check-input.is-valid ~ .form-check-label{color:#00bc8c}.was-validated .form-check-input:valid ~ .valid-feedback,.was-validated .form-check-input:valid ~ .valid-tooltip,.form-check-input.is-valid ~ .valid-feedback,.form-check-input.is-valid ~ .valid-tooltip{display:block}.was-validated .custom-control-input:valid ~ .custom-control-label,.custom-control-input.is-valid ~ .custom-control-label{color:#00bc8c}.was-validated .custom-control-input:valid ~ .custom-control-label::before,.custom-control-input.is-valid ~ .custom-control-label::before{border-color:#00bc8c}.was-validated .custom-control-input:valid ~ .valid-feedback,.was-validated .custom-control-input:valid ~ .valid-tooltip,.custom-control-input.is-valid ~ .valid-feedback,.custom-control-input.is-valid ~ .valid-tooltip{display:block}.was-validated .custom-control-input:valid:checked ~ .custom-control-label::before,.custom-control-input.is-valid:checked ~ .custom-control-label::before{border-color:#00efb2;background-color:#00efb2}.was-validated .custom-control-input:valid:focus ~ .custom-control-label::before,.custom-control-input.is-valid:focus ~ .custom-control-label::before{-webkit-box-shadow:0 0 0 0.2rem rgba(0,188,140,0.25);box-shadow:0 0 0 0.2rem rgba(0,188,140,0.25)}.was-validated .custom-control-input:valid:focus:not(:checked) ~ .custom-control-label::before,.custom-control-input.is-valid:focus:not(:checked) ~ .custom-control-label::before{border-color:#00bc8c}.was-validated .custom-file-input:valid ~ .custom-file-label,.custom-file-input.is-valid ~ .custom-file-label{border-color:#00bc8c}.was-validated .custom-file-input:valid ~ .valid-feedback,.was-validated .custom-file-input:valid ~ .valid-tooltip,.custom-file-input.is-valid ~ .valid-feedback,.custom-file-input.is-valid ~ .valid-tooltip{display:block}.was-validated .custom-file-input:valid:focus ~ .custom-file-label,.custom-file-input.is-valid:focus ~ .custom-file-label{border-color:#00bc8c;-webkit-box-shadow:0 0 0 0.2rem rgba(0,188,140,0.25);box-shadow:0 0 0 0.2rem rgba(0,188,140,0.25)}.invalid-feedback{display:none;width:100%;margin-top:0.25rem;font-size:80%;color:#E74C3C}.invalid-tooltip{position:absolute;top:100%;z-index:5;display:none;max-width:100%;padding:0.25rem 0.5rem;margin-top:.1rem;font-size:0.8203125rem;line-height:1.5;color:#fff;background-color:rgba(231,76,60,0.9);border-radius:0.25rem}.was-validated .form-control:invalid,.form-control.is-invalid{border-color:#E74C3C;padding-right:calc(1.5em + 0.75rem);background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='%23E74C3C' viewBox='-2 -2 7 7'%3e%3cpath stroke='%23E74C3C' d='M0 0l3 3m0-3L0 3'/%3e%3ccircle r='.5'/%3e%3ccircle cx='3' r='.5'/%3e%3ccircle cy='3' r='.5'/%3e%3ccircle cx='3' cy='3' r='.5'/%3e%3c/svg%3E");background-repeat:no-repeat;background-position:center right calc(0.375em + 0.1875rem);background-size:calc(0.75em + 0.375rem) calc(0.75em + 0.375rem)}.was-validated .form-control:invalid:focus,.form-control.is-invalid:focus{border-color:#E74C3C;-webkit-box-shadow:0 0 0 0.2rem rgba(231,76,60,0.25);box-shadow:0 0 0 0.2rem rgba(231,76,60,0.25)}.was-validated .form-control:invalid ~ .invalid-feedback,.was-validated .form-control:invalid ~ .invalid-tooltip,.form-control.is-invalid ~ .invalid-feedback,.form-control.is-invalid ~ .invalid-tooltip{display:block}.was-validated textarea.form-control:invalid,textarea.form-control.is-invalid{padding-right:calc(1.5em + 0.75rem);background-position:top calc(0.375em + 0.1875rem) right calc(0.375em + 0.1875rem)}.was-validated .custom-select:invalid,.custom-select.is-invalid{border-color:#E74C3C;padding-right:calc((1em + 0.75rem) * 3 / 4 + 1.75rem);background:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 4 5'%3e%3cpath fill='%23303030' d='M2 0L0 2h4zm0 5L0 3h4z'/%3e%3c/svg%3e") no-repeat right 0.75rem center/8px 10px,url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='%23E74C3C' viewBox='-2 -2 7 7'%3e%3cpath stroke='%23E74C3C' d='M0 0l3 3m0-3L0 3'/%3e%3ccircle r='.5'/%3e%3ccircle cx='3' r='.5'/%3e%3ccircle cy='3' r='.5'/%3e%3ccircle cx='3' cy='3' r='.5'/%3e%3c/svg%3E") #fff no-repeat center right 1.75rem/calc(0.75em + 0.375rem) calc(0.75em + 0.375rem)}.was-validated .custom-select:invalid:focus,.custom-select.is-invalid:focus{border-color:#E74C3C;-webkit-box-shadow:0 0 0 0.2rem rgba(231,76,60,0.25);box-shadow:0 0 0 0.2rem rgba(231,76,60,0.25)}.was-validated .custom-select:invalid ~ .invalid-feedback,.was-validated .custom-select:invalid ~ .invalid-tooltip,.custom-select.is-invalid ~ .invalid-feedback,.custom-select.is-invalid ~ .invalid-tooltip{display:block}.was-validated .form-control-file:invalid ~ .invalid-feedback,.was-validated .form-control-file:invalid ~ .invalid-tooltip,.form-control-file.is-invalid ~ .invalid-feedback,.form-control-file.is-invalid ~ .invalid-tooltip{display:block}.was-validated .form-check-input:invalid ~ .form-check-label,.form-check-input.is-invalid ~ .form-check-label{color:#E74C3C}.was-validated .form-check-input:invalid ~ .invalid-feedback,.was-validated .form-check-input:invalid ~ .invalid-tooltip,.form-check-input.is-invalid ~ .invalid-feedback,.form-check-input.is-invalid ~ .invalid-tooltip{display:block}.was-validated .custom-control-input:invalid ~ .custom-control-label,.custom-control-input.is-invalid ~ .custom-control-label{color:#E74C3C}.was-validated .custom-control-input:invalid ~ .custom-control-label::before,.custom-control-input.is-invalid ~ .custom-control-label::before{border-color:#E74C3C}.was-validated .custom-control-input:invalid ~ .invalid-feedback,.was-validated .custom-control-input:invalid ~ .invalid-tooltip,.custom-control-input.is-invalid ~ .invalid-feedback,.custom-control-input.is-invalid ~ .invalid-tooltip{display:block}.was-validated .custom-control-input:invalid:checked ~ .custom-control-label::before,.custom-control-input.is-invalid:checked ~ .custom-control-label::before{border-color:#ed7669;background-color:#ed7669}.was-validated .custom-control-input:invalid:focus ~ .custom-control-label::before,.custom-control-input.is-invalid:focus ~ .custom-control-label::before{-webkit-box-shadow:0 0 0 0.2rem rgba(231,76,60,0.25);box-shadow:0 0 0 0.2rem rgba(231,76,60,0.25)}.was-validated .custom-control-input:invalid:focus:not(:checked) ~ .custom-control-label::before,.custom-control-input.is-invalid:focus:not(:checked) ~ .custom-control-label::before{border-color:#E74C3C}.was-validated .custom-file-input:invalid ~ .custom-file-label,.custom-file-input.is-invalid ~ .custom-file-label{border-color:#E74C3C}.was-validated .custom-file-input:invalid ~ .invalid-feedback,.was-validated .custom-file-input:invalid ~ .invalid-tooltip,.custom-file-input.is-invalid ~ .invalid-feedback,.custom-file-input.is-invalid ~ .invalid-tooltip{display:block}.was-validated .custom-file-input:invalid:focus ~ .custom-file-label,.custom-file-input.is-invalid:focus ~ .custom-file-label{border-color:#E74C3C;-webkit-box-shadow:0 0 0 0.2rem rgba(231,76,60,0.25);box-shadow:0 0 0 0.2rem rgba(231,76,60,0.25)}.form-inline{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-flow:row wrap;flex-flow:row wrap;-webkit-box-align:center;-ms-flex-align:center;align-items:center}.form-inline .form-check{width:100%}@media (min-width: 576px){.form-inline label{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;margin-bottom:0}.form-inline .form-group{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-flex:0;-ms-flex:0 0 auto;flex:0 0 auto;-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-flow:row wrap;flex-flow:row wrap;-webkit-box-align:center;-ms-flex-align:center;align-items:center;margin-bottom:0}.form-inline .form-control{display:inline-block;width:auto;vertical-align:middle}.form-inline .form-control-plaintext{display:inline-block}.form-inline .input-group,.form-inline .custom-select{width:auto}.form-inline .form-check{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;width:auto;padding-left:0}.form-inline .form-check-input{position:relative;-ms-flex-negative:0;flex-shrink:0;margin-top:0;margin-right:0.25rem;margin-left:0}.form-inline .custom-control{-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center}.form-inline .custom-control-label{margin-bottom:0}}.btn{display:inline-block;font-weight:400;color:#fff;text-align:center;vertical-align:middle;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;background-color:transparent;border:1px solid transparent;padding:0.375rem 0.75rem;font-size:0.9375rem;line-height:1.5;border-radius:0.25rem;-webkit-transition:color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out;transition:color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out;transition:color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;transition:color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out}@media (prefers-reduced-motion: reduce){.btn{-webkit-transition:none;transition:none}}.btn:hover{color:#fff;text-decoration:none}.btn:focus,.btn.focus{outline:0;-webkit-box-shadow:0 0 0 0.2rem rgba(55,90,127,0.25);box-shadow:0 0 0 0.2rem rgba(55,90,127,0.25)}.btn.disabled,.btn:disabled{opacity:0.65}a.btn.disabled,fieldset:disabled a.btn{pointer-events:none}.btn-primary{color:#fff;background-color:#375a7f;border-color:#375a7f}.btn-primary:hover{color:#fff;background-color:#2b4764;border-color:#28415b}.btn-primary:focus,.btn-primary.focus{-webkit-box-shadow:0 0 0 0.2rem rgba(85,115,146,0.5);box-shadow:0 0 0 0.2rem rgba(85,115,146,0.5)}.btn-primary.disabled,.btn-primary:disabled{color:#fff;background-color:#375a7f;border-color:#375a7f}.btn-primary:not(:disabled):not(.disabled):active,.btn-primary:not(:disabled):not(.disabled).active,.show>.btn-primary.dropdown-toggle{color:#fff;background-color:#28415b;border-color:#243a53}.btn-primary:not(:disabled):not(.disabled):active:focus,.btn-primary:not(:disabled):not(.disabled).active:focus,.show>.btn-primary.dropdown-toggle:focus{-webkit-box-shadow:0 0 0 0.2rem rgba(85,115,146,0.5);box-shadow:0 0 0 0.2rem rgba(85,115,146,0.5)}.btn-secondary{color:#fff;background-color:#444;border-color:#444}.btn-secondary:hover{color:#fff;background-color:#313131;border-color:#2b2a2a}.btn-secondary:focus,.btn-secondary.focus{-webkit-box-shadow:0 0 0 0.2rem rgba(96,96,96,0.5);box-shadow:0 0 0 0.2rem rgba(96,96,96,0.5)}.btn-secondary.disabled,.btn-secondary:disabled{color:#fff;background-color:#444;border-color:#444}.btn-secondary:not(:disabled):not(.disabled):active,.btn-secondary:not(:disabled):not(.disabled).active,.show>.btn-secondary.dropdown-toggle{color:#fff;background-color:#2b2a2a;border-color:#242424}.btn-secondary:not(:disabled):not(.disabled):active:focus,.btn-secondary:not(:disabled):not(.disabled).active:focus,.show>.btn-secondary.dropdown-toggle:focus{-webkit-box-shadow:0 0 0 0.2rem rgba(96,96,96,0.5);box-shadow:0 0 0 0.2rem rgba(96,96,96,0.5)}.btn-success{color:#fff;background-color:#00bc8c;border-color:#00bc8c}.btn-success:hover{color:#fff;background-color:#009670;border-color:#008966}.btn-success:focus,.btn-success.focus{-webkit-box-shadow:0 0 0 0.2rem rgba(38,198,157,0.5);box-shadow:0 0 0 0.2rem rgba(38,198,157,0.5)}.btn-success.disabled,.btn-success:disabled{color:#fff;background-color:#00bc8c;border-color:#00bc8c}.btn-success:not(:disabled):not(.disabled):active,.btn-success:not(:disabled):not(.disabled).active,.show>.btn-success.dropdown-toggle{color:#fff;background-color:#008966;border-color:#007c5d}.btn-success:not(:disabled):not(.disabled):active:focus,.btn-success:not(:disabled):not(.disabled).active:focus,.show>.btn-success.dropdown-toggle:focus{-webkit-box-shadow:0 0 0 0.2rem rgba(38,198,157,0.5);box-shadow:0 0 0 0.2rem rgba(38,198,157,0.5)}.btn-info{color:#fff;background-color:#3498DB;border-color:#3498DB}.btn-info:hover{color:#fff;background-color:#2384c6;border-color:#217dbb}.btn-info:focus,.btn-info.focus{-webkit-box-shadow:0 0 0 0.2rem rgba(82,167,224,0.5);box-shadow:0 0 0 0.2rem rgba(82,167,224,0.5)}.btn-info.disabled,.btn-info:disabled{color:#fff;background-color:#3498DB;border-color:#3498DB}.btn-info:not(:disabled):not(.disabled):active,.btn-info:not(:disabled):not(.disabled).active,.show>.btn-info.dropdown-toggle{color:#fff;background-color:#217dbb;border-color:#1f76b0}.btn-info:not(:disabled):not(.disabled):active:focus,.btn-info:not(:disabled):not(.disabled).active:focus,.show>.btn-info.dropdown-toggle:focus{-webkit-box-shadow:0 0 0 0.2rem rgba(82,167,224,0.5);box-shadow:0 0 0 0.2rem rgba(82,167,224,0.5)}.btn-warning{color:#fff;background-color:#F39C12;border-color:#F39C12}.btn-warning:hover{color:#fff;background-color:#d4860b;border-color:#c87f0a}.btn-warning:focus,.btn-warning.focus{-webkit-box-shadow:0 0 0 0.2rem rgba(245,171,54,0.5);box-shadow:0 0 0 0.2rem rgba(245,171,54,0.5)}.btn-warning.disabled,.btn-warning:disabled{color:#fff;background-color:#F39C12;border-color:#F39C12}.btn-warning:not(:disabled):not(.disabled):active,.btn-warning:not(:disabled):not(.disabled).active,.show>.btn-warning.dropdown-toggle{color:#fff;background-color:#c87f0a;border-color:#bc770a}.btn-warning:not(:disabled):not(.disabled):active:focus,.btn-warning:not(:disabled):not(.disabled).active:focus,.show>.btn-warning.dropdown-toggle:focus{-webkit-box-shadow:0 0 0 0.2rem rgba(245,171,54,0.5);box-shadow:0 0 0 0.2rem rgba(245,171,54,0.5)}.btn-danger{color:#fff;background-color:#E74C3C;border-color:#E74C3C}.btn-danger:hover{color:#fff;background-color:#e12e1c;border-color:#d62c1a}.btn-danger:focus,.btn-danger.focus{-webkit-box-shadow:0 0 0 0.2rem rgba(235,103,89,0.5);box-shadow:0 0 0 0.2rem rgba(235,103,89,0.5)}.btn-danger.disabled,.btn-danger:disabled{color:#fff;background-color:#E74C3C;border-color:#E74C3C}.btn-danger:not(:disabled):not(.disabled):active,.btn-danger:not(:disabled):not(.disabled).active,.show>.btn-danger.dropdown-toggle{color:#fff;background-color:#d62c1a;border-color:#ca2a19}.btn-danger:not(:disabled):not(.disabled):active:focus,.btn-danger:not(:disabled):not(.disabled).active:focus,.show>.btn-danger.dropdown-toggle:focus{-webkit-box-shadow:0 0 0 0.2rem rgba(235,103,89,0.5);box-shadow:0 0 0 0.2rem rgba(235,103,89,0.5)}.btn-light{color:#fff;background-color:#303030;border-color:#303030}.btn-light:hover{color:#fff;background-color:#1d1d1d;border-color:#171616}.btn-light:focus,.btn-light.focus{-webkit-box-shadow:0 0 0 0.2rem rgba(79,79,79,0.5);box-shadow:0 0 0 0.2rem rgba(79,79,79,0.5)}.btn-light.disabled,.btn-light:disabled{color:#fff;background-color:#303030;border-color:#303030}.btn-light:not(:disabled):not(.disabled):active,.btn-light:not(:disabled):not(.disabled).active,.show>.btn-light.dropdown-toggle{color:#fff;background-color:#171616;border-color:#101010}.btn-light:not(:disabled):not(.disabled):active:focus,.btn-light:not(:disabled):not(.disabled).active:focus,.show>.btn-light.dropdown-toggle:focus{-webkit-box-shadow:0 0 0 0.2rem rgba(79,79,79,0.5);box-shadow:0 0 0 0.2rem rgba(79,79,79,0.5)}.btn-dark{color:#222;background-color:#adb5bd;border-color:#adb5bd}.btn-dark:hover{color:#fff;background-color:#98a2ac;border-color:#919ca6}.btn-dark:focus,.btn-dark.focus{-webkit-box-shadow:0 0 0 0.2rem rgba(152,159,166,0.5);box-shadow:0 0 0 0.2rem rgba(152,159,166,0.5)}.btn-dark.disabled,.btn-dark:disabled{color:#222;background-color:#adb5bd;border-color:#adb5bd}.btn-dark:not(:disabled):not(.disabled):active,.btn-dark:not(:disabled):not(.disabled).active,.show>.btn-dark.dropdown-toggle{color:#fff;background-color:#919ca6;border-color:#8a95a1}.btn-dark:not(:disabled):not(.disabled):active:focus,.btn-dark:not(:disabled):not(.disabled).active:focus,.show>.btn-dark.dropdown-toggle:focus{-webkit-box-shadow:0 0 0 0.2rem rgba(152,159,166,0.5);box-shadow:0 0 0 0.2rem rgba(152,159,166,0.5)}.btn-outline-primary{color:#375a7f;border-color:#375a7f}.btn-outline-primary:hover{color:#fff;background-color:#375a7f;border-color:#375a7f}.btn-outline-primary:focus,.btn-outline-primary.focus{-webkit-box-shadow:0 0 0 0.2rem rgba(55,90,127,0.5);box-shadow:0 0 0 0.2rem rgba(55,90,127,0.5)}.btn-outline-primary.disabled,.btn-outline-primary:disabled{color:#375a7f;background-color:transparent}.btn-outline-primary:not(:disabled):not(.disabled):active,.btn-outline-primary:not(:disabled):not(.disabled).active,.show>.btn-outline-primary.dropdown-toggle{color:#fff;background-color:#375a7f;border-color:#375a7f}.btn-outline-primary:not(:disabled):not(.disabled):active:focus,.btn-outline-primary:not(:disabled):not(.disabled).active:focus,.show>.btn-outline-primary.dropdown-toggle:focus{-webkit-box-shadow:0 0 0 0.2rem rgba(55,90,127,0.5);box-shadow:0 0 0 0.2rem rgba(55,90,127,0.5)}.btn-outline-secondary{color:#444;border-color:#444}.btn-outline-secondary:hover{color:#fff;background-color:#444;border-color:#444}.btn-outline-secondary:focus,.btn-outline-secondary.focus{-webkit-box-shadow:0 0 0 0.2rem rgba(68,68,68,0.5);box-shadow:0 0 0 0.2rem rgba(68,68,68,0.5)}.btn-outline-secondary.disabled,.btn-outline-secondary:disabled{color:#444;background-color:transparent}.btn-outline-secondary:not(:disabled):not(.disabled):active,.btn-outline-secondary:not(:disabled):not(.disabled).active,.show>.btn-outline-secondary.dropdown-toggle{color:#fff;background-color:#444;border-color:#444}.btn-outline-secondary:not(:disabled):not(.disabled):active:focus,.btn-outline-secondary:not(:disabled):not(.disabled).active:focus,.show>.btn-outline-secondary.dropdown-toggle:focus{-webkit-box-shadow:0 0 0 0.2rem rgba(68,68,68,0.5);box-shadow:0 0 0 0.2rem rgba(68,68,68,0.5)}.btn-outline-success{color:#00bc8c;border-color:#00bc8c}.btn-outline-success:hover{color:#fff;background-color:#00bc8c;border-color:#00bc8c}.btn-outline-success:focus,.btn-outline-success.focus{-webkit-box-shadow:0 0 0 0.2rem rgba(0,188,140,0.5);box-shadow:0 0 0 0.2rem rgba(0,188,140,0.5)}.btn-outline-success.disabled,.btn-outline-success:disabled{color:#00bc8c;background-color:transparent}.btn-outline-success:not(:disabled):not(.disabled):active,.btn-outline-success:not(:disabled):not(.disabled).active,.show>.btn-outline-success.dropdown-toggle{color:#fff;background-color:#00bc8c;border-color:#00bc8c}.btn-outline-success:not(:disabled):not(.disabled):active:focus,.btn-outline-success:not(:disabled):not(.disabled).active:focus,.show>.btn-outline-success.dropdown-toggle:focus{-webkit-box-shadow:0 0 0 0.2rem rgba(0,188,140,0.5);box-shadow:0 0 0 0.2rem rgba(0,188,140,0.5)}.btn-outline-info{color:#3498DB;border-color:#3498DB}.btn-outline-info:hover{color:#fff;background-color:#3498DB;border-color:#3498DB}.btn-outline-info:focus,.btn-outline-info.focus{-webkit-box-shadow:0 0 0 0.2rem rgba(52,152,219,0.5);box-shadow:0 0 0 0.2rem rgba(52,152,219,0.5)}.btn-outline-info.disabled,.btn-outline-info:disabled{color:#3498DB;background-color:transparent}.btn-outline-info:not(:disabled):not(.disabled):active,.btn-outline-info:not(:disabled):not(.disabled).active,.show>.btn-outline-info.dropdown-toggle{color:#fff;background-color:#3498DB;border-color:#3498DB}.btn-outline-info:not(:disabled):not(.disabled):active:focus,.btn-outline-info:not(:disabled):not(.disabled).active:focus,.show>.btn-outline-info.dropdown-toggle:focus{-webkit-box-shadow:0 0 0 0.2rem rgba(52,152,219,0.5);box-shadow:0 0 0 0.2rem rgba(52,152,219,0.5)}.btn-outline-warning{color:#F39C12;border-color:#F39C12}.btn-outline-warning:hover{color:#fff;background-color:#F39C12;border-color:#F39C12}.btn-outline-warning:focus,.btn-outline-warning.focus{-webkit-box-shadow:0 0 0 0.2rem rgba(243,156,18,0.5);box-shadow:0 0 0 0.2rem rgba(243,156,18,0.5)}.btn-outline-warning.disabled,.btn-outline-warning:disabled{color:#F39C12;background-color:transparent}.btn-outline-warning:not(:disabled):not(.disabled):active,.btn-outline-warning:not(:disabled):not(.disabled).active,.show>.btn-outline-warning.dropdown-toggle{color:#fff;background-color:#F39C12;border-color:#F39C12}.btn-outline-warning:not(:disabled):not(.disabled):active:focus,.btn-outline-warning:not(:disabled):not(.disabled).active:focus,.show>.btn-outline-warning.dropdown-toggle:focus{-webkit-box-shadow:0 0 0 0.2rem rgba(243,156,18,0.5);box-shadow:0 0 0 0.2rem rgba(243,156,18,0.5)}.btn-outline-danger{color:#E74C3C;border-color:#E74C3C}.btn-outline-danger:hover{color:#fff;background-color:#E74C3C;border-color:#E74C3C}.btn-outline-danger:focus,.btn-outline-danger.focus{-webkit-box-shadow:0 0 0 0.2rem rgba(231,76,60,0.5);box-shadow:0 0 0 0.2rem rgba(231,76,60,0.5)}.btn-outline-danger.disabled,.btn-outline-danger:disabled{color:#E74C3C;background-color:transparent}.btn-outline-danger:not(:disabled):not(.disabled):active,.btn-outline-danger:not(:disabled):not(.disabled).active,.show>.btn-outline-danger.dropdown-toggle{color:#fff;background-color:#E74C3C;border-color:#E74C3C}.btn-outline-danger:not(:disabled):not(.disabled):active:focus,.btn-outline-danger:not(:disabled):not(.disabled).active:focus,.show>.btn-outline-danger.dropdown-toggle:focus{-webkit-box-shadow:0 0 0 0.2rem rgba(231,76,60,0.5);box-shadow:0 0 0 0.2rem rgba(231,76,60,0.5)}.btn-outline-light{color:#303030;border-color:#303030}.btn-outline-light:hover{color:#fff;background-color:#303030;border-color:#303030}.btn-outline-light:focus,.btn-outline-light.focus{-webkit-box-shadow:0 0 0 0.2rem rgba(48,48,48,0.5);box-shadow:0 0 0 0.2rem rgba(48,48,48,0.5)}.btn-outline-light.disabled,.btn-outline-light:disabled{color:#303030;background-color:transparent}.btn-outline-light:not(:disabled):not(.disabled):active,.btn-outline-light:not(:disabled):not(.disabled).active,.show>.btn-outline-light.dropdown-toggle{color:#fff;background-color:#303030;border-color:#303030}.btn-outline-light:not(:disabled):not(.disabled):active:focus,.btn-outline-light:not(:disabled):not(.disabled).active:focus,.show>.btn-outline-light.dropdown-toggle:focus{-webkit-box-shadow:0 0 0 0.2rem rgba(48,48,48,0.5);box-shadow:0 0 0 0.2rem rgba(48,48,48,0.5)}.btn-outline-dark{color:#adb5bd;border-color:#adb5bd}.btn-outline-dark:hover{color:#222;background-color:#adb5bd;border-color:#adb5bd}.btn-outline-dark:focus,.btn-outline-dark.focus{-webkit-box-shadow:0 0 0 0.2rem rgba(173,181,189,0.5);box-shadow:0 0 0 0.2rem rgba(173,181,189,0.5)}.btn-outline-dark.disabled,.btn-outline-dark:disabled{color:#adb5bd;background-color:transparent}.btn-outline-dark:not(:disabled):not(.disabled):active,.btn-outline-dark:not(:disabled):not(.disabled).active,.show>.btn-outline-dark.dropdown-toggle{color:#222;background-color:#adb5bd;border-color:#adb5bd}.btn-outline-dark:not(:disabled):not(.disabled):active:focus,.btn-outline-dark:not(:disabled):not(.disabled).active:focus,.show>.btn-outline-dark.dropdown-toggle:focus{-webkit-box-shadow:0 0 0 0.2rem rgba(173,181,189,0.5);box-shadow:0 0 0 0.2rem rgba(173,181,189,0.5)}.btn-link{font-weight:400;color:#00bc8c;text-decoration:none}.btn-link:hover{color:#007053;text-decoration:underline}.btn-link:focus,.btn-link.focus{text-decoration:underline;-webkit-box-shadow:none;box-shadow:none}.btn-link:disabled,.btn-link.disabled{color:#999;pointer-events:none}.btn-lg,.btn-group-lg>.btn{padding:0.5rem 1rem;font-size:1.171875rem;line-height:1.5;border-radius:0.3rem}.btn-sm,.btn-group-sm>.btn{padding:0.25rem 0.5rem;font-size:0.8203125rem;line-height:1.5;border-radius:0.2rem}.btn-block{display:block;width:100%}.btn-block+.btn-block{margin-top:0.5rem}input[type="submit"].btn-block,input[type="reset"].btn-block,input[type="button"].btn-block{width:100%}.fade{-webkit-transition:opacity 0.15s linear;transition:opacity 0.15s linear}@media (prefers-reduced-motion: reduce){.fade{-webkit-transition:none;transition:none}}.fade:not(.show){opacity:0}.collapse:not(.show){display:none}.collapsing{position:relative;height:0;overflow:hidden;-webkit-transition:height 0.35s ease;transition:height 0.35s ease}@media (prefers-reduced-motion: reduce){.collapsing{-webkit-transition:none;transition:none}}.dropup,.dropright,.dropdown,.dropleft{position:relative}.dropdown-toggle{white-space:nowrap}.dropdown-toggle::after{display:inline-block;margin-left:0.255em;vertical-align:0.255em;content:"";border-top:0.3em solid;border-right:0.3em solid transparent;border-bottom:0;border-left:0.3em solid transparent}.dropdown-toggle:empty::after{margin-left:0}.dropdown-menu{position:absolute;top:100%;left:0;z-index:1000;display:none;float:left;min-width:10rem;padding:0.5rem 0;margin:0.125rem 0 0;font-size:0.9375rem;color:#fff;text-align:left;list-style:none;background-color:#222;background-clip:padding-box;border:1px solid #444;border-radius:0.25rem}.dropdown-menu-left{right:auto;left:0}.dropdown-menu-right{right:0;left:auto}@media (min-width: 576px){.dropdown-menu-sm-left{right:auto;left:0}.dropdown-menu-sm-right{right:0;left:auto}}@media (min-width: 768px){.dropdown-menu-md-left{right:auto;left:0}.dropdown-menu-md-right{right:0;left:auto}}@media (min-width: 992px){.dropdown-menu-lg-left{right:auto;left:0}.dropdown-menu-lg-right{right:0;left:auto}}@media (min-width: 1200px){.dropdown-menu-xl-left{right:auto;left:0}.dropdown-menu-xl-right{right:0;left:auto}}.dropup .dropdown-menu{top:auto;bottom:100%;margin-top:0;margin-bottom:0.125rem}.dropup .dropdown-toggle::after{display:inline-block;margin-left:0.255em;vertical-align:0.255em;content:"";border-top:0;border-right:0.3em solid transparent;border-bottom:0.3em solid;border-left:0.3em solid transparent}.dropup .dropdown-toggle:empty::after{margin-left:0}.dropright .dropdown-menu{top:0;right:auto;left:100%;margin-top:0;margin-left:0.125rem}.dropright .dropdown-toggle::after{display:inline-block;margin-left:0.255em;vertical-align:0.255em;content:"";border-top:0.3em solid transparent;border-right:0;border-bottom:0.3em solid transparent;border-left:0.3em solid}.dropright .dropdown-toggle:empty::after{margin-left:0}.dropright .dropdown-toggle::after{vertical-align:0}.dropleft .dropdown-menu{top:0;right:100%;left:auto;margin-top:0;margin-right:0.125rem}.dropleft .dropdown-toggle::after{display:inline-block;margin-left:0.255em;vertical-align:0.255em;content:""}.dropleft .dropdown-toggle::after{display:none}.dropleft .dropdown-toggle::before{display:inline-block;margin-right:0.255em;vertical-align:0.255em;content:"";border-top:0.3em solid transparent;border-right:0.3em solid;border-bottom:0.3em solid transparent}.dropleft .dropdown-toggle:empty::after{margin-left:0}.dropleft .dropdown-toggle::before{vertical-align:0}.dropdown-menu[x-placement^="top"],.dropdown-menu[x-placement^="right"],.dropdown-menu[x-placement^="bottom"],.dropdown-menu[x-placement^="left"]{right:auto;bottom:auto}.dropdown-divider{height:0;margin:0.5rem 0;overflow:hidden;border-top:1px solid #444}.dropdown-item{display:block;width:100%;padding:0.25rem 1.5rem;clear:both;font-weight:400;color:#fff;text-align:inherit;white-space:nowrap;background-color:transparent;border:0}.dropdown-item:hover,.dropdown-item:focus{color:#fff;text-decoration:none;background-color:#375a7f}.dropdown-item.active,.dropdown-item:active{color:#fff;text-decoration:none;background-color:#375a7f}.dropdown-item.disabled,.dropdown-item:disabled{color:#999;pointer-events:none;background-color:transparent}.dropdown-menu.show{display:block}.dropdown-header{display:block;padding:0.5rem 1.5rem;margin-bottom:0;font-size:0.8203125rem;color:#999;white-space:nowrap}.dropdown-item-text{display:block;padding:0.25rem 1.5rem;color:#fff}.btn-group,.btn-group-vertical{position:relative;display:-webkit-inline-box;display:-ms-inline-flexbox;display:inline-flex;vertical-align:middle}.btn-group>.btn,.btn-group-vertical>.btn{position:relative;-webkit-box-flex:1;-ms-flex:1 1 auto;flex:1 1 auto}.btn-group>.btn:hover,.btn-group-vertical>.btn:hover{z-index:1}.btn-group>.btn:focus,.btn-group>.btn:active,.btn-group>.btn.active,.btn-group-vertical>.btn:focus,.btn-group-vertical>.btn:active,.btn-group-vertical>.btn.active{z-index:1}.btn-toolbar{display:-webkit-box;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-webkit-box-pack:start;-ms-flex-pack:start;justify-content:flex-start}.btn-toolbar .input-group{width:auto}.btn-group>.btn:not(:first-child),.btn-group>.btn-group:not(:first-child){margin-left:-1px}.btn-group>.btn:not(:last-child):not(.dropdown-toggle),.btn-group>.btn-group:not(:last-child)>.btn{border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn:not(:first-child),.btn-group>.btn-group:not(:first-child)>.btn{border-top-left-radius:0;border-bottom-left-radius:0}.dropdown-toggle-split{padding-right:0.5625rem;padding-left:0.5625rem}.dropdown-toggle-split::after,.dropup .dropdown-toggle-split::after,.dropright .dropdown-toggle-split::after{margin-left:0}.dropleft .dropdown-toggle-split::before{margin-right:0}.btn-sm+.dropdown-toggle-split,.btn-group-sm>.btn+.dropdown-toggle-split{padding-right:0.375rem;padding-left:0.375rem}.btn-lg+.dropdown-toggle-split,.btn-group-lg>.btn+.dropdown-toggle-split{padding-right:0.75rem;padding-left:0.75rem}.btn-group-vertical{-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;-webkit-box-align:start;-ms-flex-align:start;align-items:flex-start;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center}.btn-group-vertical>.btn,.btn-group-vertical>.btn-group{width:100%}.btn-group-vertical>.btn:not(:first-child),.btn-group-vertical>.btn-group:not(:first-child){margin-top:-1px}.btn-group-vertical>.btn:not(:last-child):not(.dropdown-toggle),.btn-group-vertical>.btn-group:not(:last-child)>.btn{border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn:not(:first-child),.btn-group-vertical>.btn-group:not(:first-child)>.btn{border-top-left-radius:0;border-top-right-radius:0}.btn-group-toggle>.btn,.btn-group-toggle>.btn-group>.btn{margin-bottom:0}.btn-group-toggle>.btn input[type="radio"],.btn-group-toggle>.btn input[type="checkbox"],.btn-group-toggle>.btn-group>.btn input[type="radio"],.btn-group-toggle>.btn-group>.btn input[type="checkbox"]{position:absolute;clip:rect(0, 0, 0, 0);pointer-events:none}.input-group{position:relative;display:-webkit-box;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-webkit-box-align:stretch;-ms-flex-align:stretch;align-items:stretch;width:100%}.input-group>.form-control,.input-group>.form-control-plaintext,.input-group>.custom-select,.input-group>.custom-file{position:relative;-webkit-box-flex:1;-ms-flex:1 1 auto;flex:1 1 auto;width:1%;margin-bottom:0}.input-group>.form-control+.form-control,.input-group>.form-control+.custom-select,.input-group>.form-control+.custom-file,.input-group>.form-control-plaintext+.form-control,.input-group>.form-control-plaintext+.custom-select,.input-group>.form-control-plaintext+.custom-file,.input-group>.custom-select+.form-control,.input-group>.custom-select+.custom-select,.input-group>.custom-select+.custom-file,.input-group>.custom-file+.form-control,.input-group>.custom-file+.custom-select,.input-group>.custom-file+.custom-file{margin-left:-1px}.input-group>.form-control:focus,.input-group>.custom-select:focus,.input-group>.custom-file .custom-file-input:focus ~ .custom-file-label{z-index:3}.input-group>.custom-file .custom-file-input:focus{z-index:4}.input-group>.form-control:not(:last-child),.input-group>.custom-select:not(:last-child){border-top-right-radius:0;border-bottom-right-radius:0}.input-group>.form-control:not(:first-child),.input-group>.custom-select:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.input-group>.custom-file{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center}.input-group>.custom-file:not(:last-child) .custom-file-label,.input-group>.custom-file:not(:last-child) .custom-file-label::after{border-top-right-radius:0;border-bottom-right-radius:0}.input-group>.custom-file:not(:first-child) .custom-file-label{border-top-left-radius:0;border-bottom-left-radius:0}.input-group-prepend,.input-group-append{display:-webkit-box;display:-ms-flexbox;display:flex}.input-group-prepend .btn,.input-group-append .btn{position:relative;z-index:2}.input-group-prepend .btn:focus,.input-group-append .btn:focus{z-index:3}.input-group-prepend .btn+.btn,.input-group-prepend .btn+.input-group-text,.input-group-prepend .input-group-text+.input-group-text,.input-group-prepend .input-group-text+.btn,.input-group-append .btn+.btn,.input-group-append .btn+.input-group-text,.input-group-append .input-group-text+.input-group-text,.input-group-append .input-group-text+.btn{margin-left:-1px}.input-group-prepend{margin-right:-1px}.input-group-append{margin-left:-1px}.input-group-text{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;padding:0.375rem 0.75rem;margin-bottom:0;font-size:0.9375rem;font-weight:400;line-height:1.5;color:#adb5bd;text-align:center;white-space:nowrap;background-color:#444;border:1px solid transparent;border-radius:0.25rem}.input-group-text input[type="radio"],.input-group-text input[type="checkbox"]{margin-top:0}.input-group-lg>.form-control:not(textarea),.input-group-lg>.custom-select{height:calc(1.5em + 1rem + 2px)}.input-group-lg>.form-control,.input-group-lg>.custom-select,.input-group-lg>.input-group-prepend>.input-group-text,.input-group-lg>.input-group-append>.input-group-text,.input-group-lg>.input-group-prepend>.btn,.input-group-lg>.input-group-append>.btn{padding:0.5rem 1rem;font-size:1.171875rem;line-height:1.5;border-radius:0.3rem}.input-group-sm>.form-control:not(textarea),.input-group-sm>.custom-select{height:calc(1.5em + 0.5rem + 2px)}.input-group-sm>.form-control,.input-group-sm>.custom-select,.input-group-sm>.input-group-prepend>.input-group-text,.input-group-sm>.input-group-append>.input-group-text,.input-group-sm>.input-group-prepend>.btn,.input-group-sm>.input-group-append>.btn{padding:0.25rem 0.5rem;font-size:0.8203125rem;line-height:1.5;border-radius:0.2rem}.input-group-lg>.custom-select,.input-group-sm>.custom-select{padding-right:1.75rem}.input-group>.input-group-prepend>.btn,.input-group>.input-group-prepend>.input-group-text,.input-group>.input-group-append:not(:last-child)>.btn,.input-group>.input-group-append:not(:last-child)>.input-group-text,.input-group>.input-group-append:last-child>.btn:not(:last-child):not(.dropdown-toggle),.input-group>.input-group-append:last-child>.input-group-text:not(:last-child){border-top-right-radius:0;border-bottom-right-radius:0}.input-group>.input-group-append>.btn,.input-group>.input-group-append>.input-group-text,.input-group>.input-group-prepend:not(:first-child)>.btn,.input-group>.input-group-prepend:not(:first-child)>.input-group-text,.input-group>.input-group-prepend:first-child>.btn:not(:first-child),.input-group>.input-group-prepend:first-child>.input-group-text:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.custom-control{position:relative;display:block;min-height:1.40625rem;padding-left:1.5rem}.custom-control-inline{display:-webkit-inline-box;display:-ms-inline-flexbox;display:inline-flex;margin-right:1rem}.custom-control-input{position:absolute;z-index:-1;opacity:0}.custom-control-input:checked ~ .custom-control-label::before{color:#fff;border-color:#375a7f;background-color:#375a7f}.custom-control-input:focus ~ .custom-control-label::before{-webkit-box-shadow:0 0 0 0.2rem rgba(55,90,127,0.25);box-shadow:0 0 0 0.2rem rgba(55,90,127,0.25)}.custom-control-input:focus:not(:checked) ~ .custom-control-label::before{border-color:#739ac2}.custom-control-input:not(:disabled):active ~ .custom-control-label::before{color:#fff;background-color:#97b3d2;border-color:#97b3d2}.custom-control-input:disabled ~ .custom-control-label{color:#999}.custom-control-input:disabled ~ .custom-control-label::before{background-color:#ebebeb}.custom-control-label{position:relative;margin-bottom:0;vertical-align:top}.custom-control-label::before{position:absolute;top:0.203125rem;left:-1.5rem;display:block;width:1rem;height:1rem;pointer-events:none;content:"";background-color:#fff;border:#adb5bd solid 1px}.custom-control-label::after{position:absolute;top:0.203125rem;left:-1.5rem;display:block;width:1rem;height:1rem;content:"";background:no-repeat 50% / 50% 50%}.custom-checkbox .custom-control-label::before{border-radius:0.25rem}.custom-checkbox .custom-control-input:checked ~ .custom-control-label::after{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%23fff' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z'/%3e%3c/svg%3e")}.custom-checkbox .custom-control-input:indeterminate ~ .custom-control-label::before{border-color:#375a7f;background-color:#375a7f}.custom-checkbox .custom-control-input:indeterminate ~ .custom-control-label::after{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 4 4'%3e%3cpath stroke='%23fff' d='M0 2h4'/%3e%3c/svg%3e")}.custom-checkbox .custom-control-input:disabled:checked ~ .custom-control-label::before{background-color:rgba(55,90,127,0.5)}.custom-checkbox .custom-control-input:disabled:indeterminate ~ .custom-control-label::before{background-color:rgba(55,90,127,0.5)}.custom-radio .custom-control-label::before{border-radius:50%}.custom-radio .custom-control-input:checked ~ .custom-control-label::after{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%23fff'/%3e%3c/svg%3e")}.custom-radio .custom-control-input:disabled:checked ~ .custom-control-label::before{background-color:rgba(55,90,127,0.5)}.custom-switch{padding-left:2.25rem}.custom-switch .custom-control-label::before{left:-2.25rem;width:1.75rem;pointer-events:all;border-radius:0.5rem}.custom-switch .custom-control-label::after{top:calc(0.203125rem + 2px);left:calc(-2.25rem + 2px);width:calc(1rem - 4px);height:calc(1rem - 4px);background-color:#adb5bd;border-radius:0.5rem;-webkit-transition:background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, -webkit-transform 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out;transition:background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, -webkit-transform 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out;transition:transform 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;transition:transform 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out, -webkit-transform 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out}@media (prefers-reduced-motion: reduce){.custom-switch .custom-control-label::after{-webkit-transition:none;transition:none}}.custom-switch .custom-control-input:checked ~ .custom-control-label::after{background-color:#fff;-webkit-transform:translateX(0.75rem);transform:translateX(0.75rem)}.custom-switch .custom-control-input:disabled:checked ~ .custom-control-label::before{background-color:rgba(55,90,127,0.5)}.custom-select{display:inline-block;width:100%;height:calc(1.5em + 0.75rem + 2px);padding:0.375rem 1.75rem 0.375rem 0.75rem;font-size:0.9375rem;font-weight:400;line-height:1.5;color:#444;vertical-align:middle;background:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 4 5'%3e%3cpath fill='%23303030' d='M2 0L0 2h4zm0 5L0 3h4z'/%3e%3c/svg%3e") no-repeat right 0.75rem center/8px 10px;background-color:#fff;border:1px solid transparent;border-radius:0.25rem;-webkit-appearance:none;-moz-appearance:none;appearance:none}.custom-select:focus{border-color:#739ac2;outline:0;-webkit-box-shadow:0 0 0 0.2rem rgba(55,90,127,0.25);box-shadow:0 0 0 0.2rem rgba(55,90,127,0.25)}.custom-select:focus::-ms-value{color:#444;background-color:#fff}.custom-select[multiple],.custom-select[size]:not([size="1"]){height:auto;padding-right:0.75rem;background-image:none}.custom-select:disabled{color:#999;background-color:#ebebeb}.custom-select::-ms-expand{display:none}.custom-select-sm{height:calc(1.5em + 0.5rem + 2px);padding-top:0.25rem;padding-bottom:0.25rem;padding-left:0.5rem;font-size:0.8203125rem}.custom-select-lg{height:calc(1.5em + 1rem + 2px);padding-top:0.5rem;padding-bottom:0.5rem;padding-left:1rem;font-size:1.171875rem}.custom-file{position:relative;display:inline-block;width:100%;height:calc(1.5em + 0.75rem + 2px);margin-bottom:0}.custom-file-input{position:relative;z-index:2;width:100%;height:calc(1.5em + 0.75rem + 2px);margin:0;opacity:0}.custom-file-input:focus ~ .custom-file-label{border-color:#739ac2;-webkit-box-shadow:0 0 0 0.2rem rgba(55,90,127,0.25);box-shadow:0 0 0 0.2rem rgba(55,90,127,0.25)}.custom-file-input:disabled ~ .custom-file-label{background-color:#ebebeb}.custom-file-input:lang(en) ~ .custom-file-label::after{content:"Browse"}.custom-file-input ~ .custom-file-label[data-browse]::after{content:attr(data-browse)}.custom-file-label{position:absolute;top:0;right:0;left:0;z-index:1;height:calc(1.5em + 0.75rem + 2px);padding:0.375rem 0.75rem;font-weight:400;line-height:1.5;color:#adb5bd;background-color:#fff;border:1px solid #444;border-radius:0.25rem}.custom-file-label::after{position:absolute;top:0;right:0;bottom:0;z-index:3;display:block;height:calc(1.5em + 0.75rem);padding:0.375rem 0.75rem;line-height:1.5;color:#adb5bd;content:"Browse";background-color:#444;border-left:inherit;border-radius:0 0.25rem 0.25rem 0}.custom-range{width:100%;height:calc(1rem + 0.4rem);padding:0;background-color:transparent;-webkit-appearance:none;-moz-appearance:none;appearance:none}.custom-range:focus{outline:none}.custom-range:focus::-webkit-slider-thumb{-webkit-box-shadow:0 0 0 1px #222,0 0 0 0.2rem rgba(55,90,127,0.25);box-shadow:0 0 0 1px #222,0 0 0 0.2rem rgba(55,90,127,0.25)}.custom-range:focus::-moz-range-thumb{box-shadow:0 0 0 1px #222,0 0 0 0.2rem rgba(55,90,127,0.25)}.custom-range:focus::-ms-thumb{box-shadow:0 0 0 1px #222,0 0 0 0.2rem rgba(55,90,127,0.25)}.custom-range::-moz-focus-outer{border:0}.custom-range::-webkit-slider-thumb{width:1rem;height:1rem;margin-top:-0.25rem;background-color:#375a7f;border:0;border-radius:1rem;-webkit-transition:background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out;transition:background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out;transition:background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;transition:background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out;-webkit-appearance:none;appearance:none}@media (prefers-reduced-motion: reduce){.custom-range::-webkit-slider-thumb{-webkit-transition:none;transition:none}}.custom-range::-webkit-slider-thumb:active{background-color:#97b3d2}.custom-range::-webkit-slider-runnable-track{width:100%;height:0.5rem;color:transparent;cursor:pointer;background-color:#dee2e6;border-color:transparent;border-radius:1rem}.custom-range::-moz-range-thumb{width:1rem;height:1rem;background-color:#375a7f;border:0;border-radius:1rem;-webkit-transition:background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out;transition:background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out;transition:background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;transition:background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out;-moz-appearance:none;appearance:none}@media (prefers-reduced-motion: reduce){.custom-range::-moz-range-thumb{-webkit-transition:none;transition:none}}.custom-range::-moz-range-thumb:active{background-color:#97b3d2}.custom-range::-moz-range-track{width:100%;height:0.5rem;color:transparent;cursor:pointer;background-color:#dee2e6;border-color:transparent;border-radius:1rem}.custom-range::-ms-thumb{width:1rem;height:1rem;margin-top:0;margin-right:0.2rem;margin-left:0.2rem;background-color:#375a7f;border:0;border-radius:1rem;-webkit-transition:background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out;transition:background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out;transition:background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;transition:background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out;appearance:none}@media (prefers-reduced-motion: reduce){.custom-range::-ms-thumb{-webkit-transition:none;transition:none}}.custom-range::-ms-thumb:active{background-color:#97b3d2}.custom-range::-ms-track{width:100%;height:0.5rem;color:transparent;cursor:pointer;background-color:transparent;border-color:transparent;border-width:0.5rem}.custom-range::-ms-fill-lower{background-color:#dee2e6;border-radius:1rem}.custom-range::-ms-fill-upper{margin-right:15px;background-color:#dee2e6;border-radius:1rem}.custom-range:disabled::-webkit-slider-thumb{background-color:#adb5bd}.custom-range:disabled::-webkit-slider-runnable-track{cursor:default}.custom-range:disabled::-moz-range-thumb{background-color:#adb5bd}.custom-range:disabled::-moz-range-track{cursor:default}.custom-range:disabled::-ms-thumb{background-color:#adb5bd}.custom-control-label::before,.custom-file-label,.custom-select{-webkit-transition:background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out;transition:background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out;transition:background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;transition:background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out}@media (prefers-reduced-motion: reduce){.custom-control-label::before,.custom-file-label,.custom-select{-webkit-transition:none;transition:none}}.nav{display:-webkit-box;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;padding-left:0;margin-bottom:0;list-style:none}.nav-link{display:block;padding:0.5rem 2rem}.nav-link:hover,.nav-link:focus{text-decoration:none}.nav-link.disabled{color:#adb5bd;pointer-events:none;cursor:default}.nav-tabs{border-bottom:1px solid #444}.nav-tabs .nav-item{margin-bottom:-1px}.nav-tabs .nav-link{border:1px solid transparent;border-top-left-radius:0.25rem;border-top-right-radius:0.25rem}.nav-tabs .nav-link:hover,.nav-tabs .nav-link:focus{border-color:#444 #444 transparent}.nav-tabs .nav-link.disabled{color:#adb5bd;background-color:transparent;border-color:transparent}.nav-tabs .nav-link.active,.nav-tabs .nav-item.show .nav-link{color:#fff;background-color:#222;border-color:#444 #444 transparent}.nav-tabs .dropdown-menu{margin-top:-1px;border-top-left-radius:0;border-top-right-radius:0}.nav-pills .nav-link{border-radius:0.25rem}.nav-pills .nav-link.active,.nav-pills .show>.nav-link{color:#fff;background-color:#375a7f}.nav-fill .nav-item{-webkit-box-flex:1;-ms-flex:1 1 auto;flex:1 1 auto;text-align:center}.nav-justified .nav-item{-ms-flex-preferred-size:0;flex-basis:0;-webkit-box-flex:1;-ms-flex-positive:1;flex-grow:1;text-align:center}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.navbar{position:relative;display:-webkit-box;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between;padding:1rem 1rem}.navbar>.container,.navbar>.container-fluid{display:-webkit-box;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between}.navbar-brand{display:inline-block;padding-top:0.32421875rem;padding-bottom:0.32421875rem;margin-right:1rem;font-size:1.171875rem;line-height:inherit;white-space:nowrap}.navbar-brand:hover,.navbar-brand:focus{text-decoration:none}.navbar-nav{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;padding-left:0;margin-bottom:0;list-style:none}.navbar-nav .nav-link{padding-right:0;padding-left:0}.navbar-nav .dropdown-menu{position:static;float:none}.navbar-text{display:inline-block;padding-top:0.5rem;padding-bottom:0.5rem}.navbar-collapse{-ms-flex-preferred-size:100%;flex-basis:100%;-webkit-box-flex:1;-ms-flex-positive:1;flex-grow:1;-webkit-box-align:center;-ms-flex-align:center;align-items:center}.navbar-toggler{padding:0.25rem 0.75rem;font-size:1.171875rem;line-height:1;background-color:transparent;border:1px solid transparent;border-radius:0.25rem}.navbar-toggler:hover,.navbar-toggler:focus{text-decoration:none}.navbar-toggler-icon{display:inline-block;width:1.5em;height:1.5em;vertical-align:middle;content:"";background:no-repeat center center;background-size:100% 100%}@media (max-width: 575.98px){.navbar-expand-sm>.container,.navbar-expand-sm>.container-fluid{padding-right:0;padding-left:0}}@media (min-width: 576px){.navbar-expand-sm{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-flow:row nowrap;flex-flow:row nowrap;-webkit-box-pack:start;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand-sm .navbar-nav{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-direction:row;flex-direction:row}.navbar-expand-sm .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-sm .navbar-nav .nav-link{padding-right:0.5rem;padding-left:0.5rem}.navbar-expand-sm>.container,.navbar-expand-sm>.container-fluid{-ms-flex-wrap:nowrap;flex-wrap:nowrap}.navbar-expand-sm .navbar-collapse{display:-webkit-box !important;display:-ms-flexbox !important;display:flex !important;-ms-flex-preferred-size:auto;flex-basis:auto}.navbar-expand-sm .navbar-toggler{display:none}}@media (max-width: 767.98px){.navbar-expand-md>.container,.navbar-expand-md>.container-fluid{padding-right:0;padding-left:0}}@media (min-width: 768px){.navbar-expand-md{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-flow:row nowrap;flex-flow:row nowrap;-webkit-box-pack:start;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand-md .navbar-nav{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-direction:row;flex-direction:row}.navbar-expand-md .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-md .navbar-nav .nav-link{padding-right:0.5rem;padding-left:0.5rem}.navbar-expand-md>.container,.navbar-expand-md>.container-fluid{-ms-flex-wrap:nowrap;flex-wrap:nowrap}.navbar-expand-md .navbar-collapse{display:-webkit-box !important;display:-ms-flexbox !important;display:flex !important;-ms-flex-preferred-size:auto;flex-basis:auto}.navbar-expand-md .navbar-toggler{display:none}}@media (max-width: 991.98px){.navbar-expand-lg>.container,.navbar-expand-lg>.container-fluid{padding-right:0;padding-left:0}}@media (min-width: 992px){.navbar-expand-lg{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-flow:row nowrap;flex-flow:row nowrap;-webkit-box-pack:start;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand-lg .navbar-nav{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-direction:row;flex-direction:row}.navbar-expand-lg .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-lg .navbar-nav .nav-link{padding-right:0.5rem;padding-left:0.5rem}.navbar-expand-lg>.container,.navbar-expand-lg>.container-fluid{-ms-flex-wrap:nowrap;flex-wrap:nowrap}.navbar-expand-lg .navbar-collapse{display:-webkit-box !important;display:-ms-flexbox !important;display:flex !important;-ms-flex-preferred-size:auto;flex-basis:auto}.navbar-expand-lg .navbar-toggler{display:none}}@media (max-width: 1199.98px){.navbar-expand-xl>.container,.navbar-expand-xl>.container-fluid{padding-right:0;padding-left:0}}@media (min-width: 1200px){.navbar-expand-xl{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-flow:row nowrap;flex-flow:row nowrap;-webkit-box-pack:start;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand-xl .navbar-nav{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-direction:row;flex-direction:row}.navbar-expand-xl .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-xl .navbar-nav .nav-link{padding-right:0.5rem;padding-left:0.5rem}.navbar-expand-xl>.container,.navbar-expand-xl>.container-fluid{-ms-flex-wrap:nowrap;flex-wrap:nowrap}.navbar-expand-xl .navbar-collapse{display:-webkit-box !important;display:-ms-flexbox !important;display:flex !important;-ms-flex-preferred-size:auto;flex-basis:auto}.navbar-expand-xl .navbar-toggler{display:none}}.navbar-expand{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-flow:row nowrap;flex-flow:row nowrap;-webkit-box-pack:start;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand>.container,.navbar-expand>.container-fluid{padding-right:0;padding-left:0}.navbar-expand .navbar-nav{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-direction:row;flex-direction:row}.navbar-expand .navbar-nav .dropdown-menu{position:absolute}.navbar-expand .navbar-nav .nav-link{padding-right:0.5rem;padding-left:0.5rem}.navbar-expand>.container,.navbar-expand>.container-fluid{-ms-flex-wrap:nowrap;flex-wrap:nowrap}.navbar-expand .navbar-collapse{display:-webkit-box !important;display:-ms-flexbox !important;display:flex !important;-ms-flex-preferred-size:auto;flex-basis:auto}.navbar-expand .navbar-toggler{display:none}.navbar-light .navbar-brand{color:#fff}.navbar-light .navbar-brand:hover,.navbar-light .navbar-brand:focus{color:#fff}.navbar-light .navbar-nav .nav-link{color:rgba(255,255,255,0.5)}.navbar-light .navbar-nav .nav-link:hover,.navbar-light .navbar-nav .nav-link:focus{color:#fff}.navbar-light .navbar-nav .nav-link.disabled{color:rgba(255,255,255,0.3)}.navbar-light .navbar-nav .show>.nav-link,.navbar-light .navbar-nav .active>.nav-link,.navbar-light .navbar-nav .nav-link.show,.navbar-light .navbar-nav .nav-link.active{color:#fff}.navbar-light .navbar-toggler{color:rgba(255,255,255,0.5);border-color:rgba(255,255,255,0.1)}.navbar-light .navbar-toggler-icon{background-image:url("data:image/svg+xml,%3csvg viewBox='0 0 30 30' xmlns='http://www.w3.org/2000/svg'%3e%3cpath stroke='rgba(255, 255, 255, 0.5)' stroke-width='2' stroke-linecap='round' stroke-miterlimit='10' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e")}.navbar-light .navbar-text{color:rgba(255,255,255,0.5)}.navbar-light .navbar-text a{color:#fff}.navbar-light .navbar-text a:hover,.navbar-light .navbar-text a:focus{color:#fff}.navbar-dark .navbar-brand{color:#fff}.navbar-dark .navbar-brand:hover,.navbar-dark .navbar-brand:focus{color:#fff}.navbar-dark .navbar-nav .nav-link{color:#fff}.navbar-dark .navbar-nav .nav-link:hover,.navbar-dark .navbar-nav .nav-link:focus{color:#00bc8c}.navbar-dark .navbar-nav .nav-link.disabled{color:rgba(255,255,255,0.25)}.navbar-dark .navbar-nav .show>.nav-link,.navbar-dark .navbar-nav .active>.nav-link,.navbar-dark .navbar-nav .nav-link.show,.navbar-dark .navbar-nav .nav-link.active{color:#fff}.navbar-dark .navbar-toggler{color:#fff;border-color:rgba(255,255,255,0.1)}.navbar-dark .navbar-toggler-icon{background-image:url("data:image/svg+xml,%3csvg viewBox='0 0 30 30' xmlns='http://www.w3.org/2000/svg'%3e%3cpath stroke='%23fff' stroke-width='2' stroke-linecap='round' stroke-miterlimit='10' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e")}.navbar-dark .navbar-text{color:#fff}.navbar-dark .navbar-text a{color:#fff}.navbar-dark .navbar-text a:hover,.navbar-dark .navbar-text a:focus{color:#fff}.card{position:relative;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;min-width:0;word-wrap:break-word;background-color:#303030;background-clip:border-box;border:1px solid rgba(0,0,0,0.125);border-radius:0.25rem}.card>hr{margin-right:0;margin-left:0}.card>.list-group:first-child .list-group-item:first-child{border-top-left-radius:0.25rem;border-top-right-radius:0.25rem}.card>.list-group:last-child .list-group-item:last-child{border-bottom-right-radius:0.25rem;border-bottom-left-radius:0.25rem}.card-body{-webkit-box-flex:1;-ms-flex:1 1 auto;flex:1 1 auto;padding:1.25rem}.card-title{margin-bottom:0.75rem}.card-subtitle{margin-top:-0.375rem;margin-bottom:0}.card-text:last-child{margin-bottom:0}.card-link:hover{text-decoration:none}.card-link+.card-link{margin-left:1.25rem}.card-header{padding:0.75rem 1.25rem;margin-bottom:0;background-color:#444;border-bottom:1px solid rgba(0,0,0,0.125)}.card-header:first-child{border-radius:calc(0.25rem - 1px) calc(0.25rem - 1px) 0 0}.card-header+.list-group .list-group-item:first-child{border-top:0}.card-footer{padding:0.75rem 1.25rem;background-color:#444;border-top:1px solid rgba(0,0,0,0.125)}.card-footer:last-child{border-radius:0 0 calc(0.25rem - 1px) calc(0.25rem - 1px)}.card-header-tabs{margin-right:-0.625rem;margin-bottom:-0.75rem;margin-left:-0.625rem;border-bottom:0}.card-header-pills{margin-right:-0.625rem;margin-left:-0.625rem}.card-img-overlay{position:absolute;top:0;right:0;bottom:0;left:0;padding:1.25rem}.card-img{width:100%;border-radius:calc(0.25rem - 1px)}.card-img-top{width:100%;border-top-left-radius:calc(0.25rem - 1px);border-top-right-radius:calc(0.25rem - 1px)}.card-img-bottom{width:100%;border-bottom-right-radius:calc(0.25rem - 1px);border-bottom-left-radius:calc(0.25rem - 1px)}.card-deck{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column}.card-deck .card{margin-bottom:15px}@media (min-width: 576px){.card-deck{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-flow:row wrap;flex-flow:row wrap;margin-right:-15px;margin-left:-15px}.card-deck .card{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-flex:1;-ms-flex:1 0 0%;flex:1 0 0%;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;margin-right:15px;margin-bottom:0;margin-left:15px}}.card-group{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column}.card-group>.card{margin-bottom:15px}@media (min-width: 576px){.card-group{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-flow:row wrap;flex-flow:row wrap}.card-group>.card{-webkit-box-flex:1;-ms-flex:1 0 0%;flex:1 0 0%;margin-bottom:0}.card-group>.card+.card{margin-left:0;border-left:0}.card-group>.card:not(:last-child){border-top-right-radius:0;border-bottom-right-radius:0}.card-group>.card:not(:last-child) .card-img-top,.card-group>.card:not(:last-child) .card-header{border-top-right-radius:0}.card-group>.card:not(:last-child) .card-img-bottom,.card-group>.card:not(:last-child) .card-footer{border-bottom-right-radius:0}.card-group>.card:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.card-group>.card:not(:first-child) .card-img-top,.card-group>.card:not(:first-child) .card-header{border-top-left-radius:0}.card-group>.card:not(:first-child) .card-img-bottom,.card-group>.card:not(:first-child) .card-footer{border-bottom-left-radius:0}}.card-columns .card{margin-bottom:0.75rem}@media (min-width: 576px){.card-columns{-webkit-column-count:3;column-count:3;-webkit-column-gap:1.25rem;column-gap:1.25rem;orphans:1;widows:1}.card-columns .card{display:inline-block;width:100%}}.accordion>.card{overflow:hidden}.accordion>.card:not(:first-of-type) .card-header:first-child{border-radius:0}.accordion>.card:not(:first-of-type):not(:last-of-type){border-bottom:0;border-radius:0}.accordion>.card:first-of-type{border-bottom:0;border-bottom-right-radius:0;border-bottom-left-radius:0}.accordion>.card:last-of-type{border-top-left-radius:0;border-top-right-radius:0}.accordion>.card .card-header{margin-bottom:-1px}.breadcrumb{display:-webkit-box;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;padding:0.75rem 1rem;margin-bottom:1rem;list-style:none;background-color:#444;border-radius:0.25rem}.breadcrumb-item+.breadcrumb-item{padding-left:0.5rem}.breadcrumb-item+.breadcrumb-item::before{display:inline-block;padding-right:0.5rem;color:#999;content:"/"}.breadcrumb-item+.breadcrumb-item:hover::before{text-decoration:underline}.breadcrumb-item+.breadcrumb-item:hover::before{text-decoration:none}.breadcrumb-item.active{color:#999}.pagination{display:-webkit-box;display:-ms-flexbox;display:flex;padding-left:0;list-style:none;border-radius:0.25rem}.page-link{position:relative;display:block;padding:0.5rem 0.75rem;margin-left:0;line-height:1.25;color:#fff;background-color:#00bc8c;border:0 solid transparent}.page-link:hover{z-index:2;color:#fff;text-decoration:none;background-color:#00efb2;border-color:transparent}.page-link:focus{z-index:2;outline:0;-webkit-box-shadow:0 0 0 0.2rem rgba(55,90,127,0.25);box-shadow:0 0 0 0.2rem rgba(55,90,127,0.25)}.page-item:first-child .page-link{margin-left:0;border-top-left-radius:0.25rem;border-bottom-left-radius:0.25rem}.page-item:last-child .page-link{border-top-right-radius:0.25rem;border-bottom-right-radius:0.25rem}.page-item.active .page-link{z-index:1;color:#fff;background-color:#00efb2;border-color:transparent}.page-item.disabled .page-link{color:#fff;pointer-events:none;cursor:auto;background-color:#007053;border-color:transparent}.pagination-lg .page-link{padding:0.75rem 1.5rem;font-size:1.171875rem;line-height:1.5}.pagination-lg .page-item:first-child .page-link{border-top-left-radius:0.3rem;border-bottom-left-radius:0.3rem}.pagination-lg .page-item:last-child .page-link{border-top-right-radius:0.3rem;border-bottom-right-radius:0.3rem}.pagination-sm .page-link{padding:0.25rem 0.5rem;font-size:0.8203125rem;line-height:1.5}.pagination-sm .page-item:first-child .page-link{border-top-left-radius:0.2rem;border-bottom-left-radius:0.2rem}.pagination-sm .page-item:last-child .page-link{border-top-right-radius:0.2rem;border-bottom-right-radius:0.2rem}.badge{display:inline-block;padding:0.25em 0.4em;font-size:75%;font-weight:700;line-height:1;text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:0.25rem;-webkit-transition:color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out;transition:color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out;transition:color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;transition:color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out}@media (prefers-reduced-motion: reduce){.badge{-webkit-transition:none;transition:none}}a.badge:hover,a.badge:focus{text-decoration:none}.badge:empty{display:none}.btn .badge{position:relative;top:-1px}.badge-pill{padding-right:0.6em;padding-left:0.6em;border-radius:10rem}.badge-primary{color:#fff;background-color:#375a7f}a.badge-primary:hover,a.badge-primary:focus{color:#fff;background-color:#28415b}a.badge-primary:focus,a.badge-primary.focus{outline:0;-webkit-box-shadow:0 0 0 0.2rem rgba(55,90,127,0.5);box-shadow:0 0 0 0.2rem rgba(55,90,127,0.5)}.badge-secondary{color:#fff;background-color:#444}a.badge-secondary:hover,a.badge-secondary:focus{color:#fff;background-color:#2b2a2a}a.badge-secondary:focus,a.badge-secondary.focus{outline:0;-webkit-box-shadow:0 0 0 0.2rem rgba(68,68,68,0.5);box-shadow:0 0 0 0.2rem rgba(68,68,68,0.5)}.badge-success{color:#fff;background-color:#00bc8c}a.badge-success:hover,a.badge-success:focus{color:#fff;background-color:#008966}a.badge-success:focus,a.badge-success.focus{outline:0;-webkit-box-shadow:0 0 0 0.2rem rgba(0,188,140,0.5);box-shadow:0 0 0 0.2rem rgba(0,188,140,0.5)}.badge-info{color:#fff;background-color:#3498DB}a.badge-info:hover,a.badge-info:focus{color:#fff;background-color:#217dbb}a.badge-info:focus,a.badge-info.focus{outline:0;-webkit-box-shadow:0 0 0 0.2rem rgba(52,152,219,0.5);box-shadow:0 0 0 0.2rem rgba(52,152,219,0.5)}.badge-warning{color:#fff;background-color:#F39C12}a.badge-warning:hover,a.badge-warning:focus{color:#fff;background-color:#c87f0a}a.badge-warning:focus,a.badge-warning.focus{outline:0;-webkit-box-shadow:0 0 0 0.2rem rgba(243,156,18,0.5);box-shadow:0 0 0 0.2rem rgba(243,156,18,0.5)}.badge-danger{color:#fff;background-color:#E74C3C}a.badge-danger:hover,a.badge-danger:focus{color:#fff;background-color:#d62c1a}a.badge-danger:focus,a.badge-danger.focus{outline:0;-webkit-box-shadow:0 0 0 0.2rem rgba(231,76,60,0.5);box-shadow:0 0 0 0.2rem rgba(231,76,60,0.5)}.badge-light{color:#fff;background-color:#303030}a.badge-light:hover,a.badge-light:focus{color:#fff;background-color:#171616}a.badge-light:focus,a.badge-light.focus{outline:0;-webkit-box-shadow:0 0 0 0.2rem rgba(48,48,48,0.5);box-shadow:0 0 0 0.2rem rgba(48,48,48,0.5)}.badge-dark{color:#222;background-color:#adb5bd}a.badge-dark:hover,a.badge-dark:focus{color:#222;background-color:#919ca6}a.badge-dark:focus,a.badge-dark.focus{outline:0;-webkit-box-shadow:0 0 0 0.2rem rgba(173,181,189,0.5);box-shadow:0 0 0 0.2rem rgba(173,181,189,0.5)}.jumbotron{padding:2rem 1rem;margin-bottom:2rem;background-color:#303030;border-radius:0.3rem}@media (min-width: 576px){.jumbotron{padding:4rem 2rem}}.jumbotron-fluid{padding-right:0;padding-left:0;border-radius:0}.alert{position:relative;padding:0.75rem 1.25rem;margin-bottom:1rem;border:1px solid transparent;border-radius:0.25rem}.alert-heading{color:inherit}.alert-link{font-weight:700}.alert-dismissible{padding-right:3.90625rem}.alert-dismissible .close{position:absolute;top:0;right:0;padding:0.75rem 1.25rem;color:inherit}.alert-primary{color:#1d2f42;background-color:#d7dee5;border-color:#c7d1db}.alert-primary hr{border-top-color:#b7c4d1}.alert-primary .alert-link{color:#0d161f}.alert-secondary{color:#232323;background-color:#dadada;border-color:#cbcbcb}.alert-secondary hr{border-top-color:#bebebe}.alert-secondary .alert-link{color:#0a0909}.alert-success{color:#006249;background-color:#ccf2e8;border-color:#b8ecdf}.alert-success hr{border-top-color:#a4e7d6}.alert-success .alert-link{color:#002f23}.alert-info{color:#1b4f72;background-color:#d6eaf8;border-color:#c6e2f5}.alert-info hr{border-top-color:#b0d7f1}.alert-info .alert-link{color:#113249}.alert-warning{color:#7e5109;background-color:#fdebd0;border-color:#fce3bd}.alert-warning hr{border-top-color:#fbd9a5}.alert-warning .alert-link{color:#4e3206}.alert-danger{color:#78281f;background-color:#fadbd8;border-color:#f8cdc8}.alert-danger hr{border-top-color:#f5b8b1}.alert-danger .alert-link{color:#4f1a15}.alert-light{color:#191919;background-color:#d6d6d6;border-color:#c5c5c5}.alert-light hr{border-top-color:#b8b8b8}.alert-light .alert-link{color:black}.alert-dark{color:#5a5e62;background-color:#eff0f2;border-color:#e8eaed}.alert-dark hr{border-top-color:#dadde2}.alert-dark .alert-link{color:#424547}@-webkit-keyframes progress-bar-stripes{from{background-position:0.625rem 0}to{background-position:0 0}}@keyframes progress-bar-stripes{from{background-position:0.625rem 0}to{background-position:0 0}}.progress{display:-webkit-box;display:-ms-flexbox;display:flex;height:0.625rem;overflow:hidden;font-size:0.625rem;background-color:#444;border-radius:0.25rem}.progress-bar{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;color:#fff;text-align:center;white-space:nowrap;background-color:#375a7f;-webkit-transition:width 0.6s ease;transition:width 0.6s ease}@media (prefers-reduced-motion: reduce){.progress-bar{-webkit-transition:none;transition:none}}.progress-bar-striped{background-image:linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-size:0.625rem 0.625rem}.progress-bar-animated{-webkit-animation:progress-bar-stripes 1s linear infinite;animation:progress-bar-stripes 1s linear infinite}@media (prefers-reduced-motion: reduce){.progress-bar-animated{-webkit-animation:none;animation:none}}.media{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:start;-ms-flex-align:start;align-items:flex-start}.media-body{-webkit-box-flex:1;-ms-flex:1;flex:1}.list-group{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;padding-left:0;margin-bottom:0}.list-group-item-action{width:100%;color:#444;text-align:inherit}.list-group-item-action:hover,.list-group-item-action:focus{z-index:1;color:#444;text-decoration:none;background-color:#444}.list-group-item-action:active{color:#fff;background-color:#ebebeb}.list-group-item{position:relative;display:block;padding:0.75rem 1.25rem;margin-bottom:-1px;background-color:#303030;border:1px solid #444}.list-group-item:first-child{border-top-left-radius:0.25rem;border-top-right-radius:0.25rem}.list-group-item:last-child{margin-bottom:0;border-bottom-right-radius:0.25rem;border-bottom-left-radius:0.25rem}.list-group-item.disabled,.list-group-item:disabled{color:#999;pointer-events:none;background-color:#303030}.list-group-item.active{z-index:2;color:#fff;background-color:#375a7f;border-color:#375a7f}.list-group-horizontal{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-direction:row;flex-direction:row}.list-group-horizontal .list-group-item{margin-right:-1px;margin-bottom:0}.list-group-horizontal .list-group-item:first-child{border-top-left-radius:0.25rem;border-bottom-left-radius:0.25rem;border-top-right-radius:0}.list-group-horizontal .list-group-item:last-child{margin-right:0;border-top-right-radius:0.25rem;border-bottom-right-radius:0.25rem;border-bottom-left-radius:0}@media (min-width: 576px){.list-group-horizontal-sm{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-direction:row;flex-direction:row}.list-group-horizontal-sm .list-group-item{margin-right:-1px;margin-bottom:0}.list-group-horizontal-sm .list-group-item:first-child{border-top-left-radius:0.25rem;border-bottom-left-radius:0.25rem;border-top-right-radius:0}.list-group-horizontal-sm .list-group-item:last-child{margin-right:0;border-top-right-radius:0.25rem;border-bottom-right-radius:0.25rem;border-bottom-left-radius:0}}@media (min-width: 768px){.list-group-horizontal-md{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-direction:row;flex-direction:row}.list-group-horizontal-md .list-group-item{margin-right:-1px;margin-bottom:0}.list-group-horizontal-md .list-group-item:first-child{border-top-left-radius:0.25rem;border-bottom-left-radius:0.25rem;border-top-right-radius:0}.list-group-horizontal-md .list-group-item:last-child{margin-right:0;border-top-right-radius:0.25rem;border-bottom-right-radius:0.25rem;border-bottom-left-radius:0}}@media (min-width: 992px){.list-group-horizontal-lg{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-direction:row;flex-direction:row}.list-group-horizontal-lg .list-group-item{margin-right:-1px;margin-bottom:0}.list-group-horizontal-lg .list-group-item:first-child{border-top-left-radius:0.25rem;border-bottom-left-radius:0.25rem;border-top-right-radius:0}.list-group-horizontal-lg .list-group-item:last-child{margin-right:0;border-top-right-radius:0.25rem;border-bottom-right-radius:0.25rem;border-bottom-left-radius:0}}@media (min-width: 1200px){.list-group-horizontal-xl{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-direction:row;flex-direction:row}.list-group-horizontal-xl .list-group-item{margin-right:-1px;margin-bottom:0}.list-group-horizontal-xl .list-group-item:first-child{border-top-left-radius:0.25rem;border-bottom-left-radius:0.25rem;border-top-right-radius:0}.list-group-horizontal-xl .list-group-item:last-child{margin-right:0;border-top-right-radius:0.25rem;border-bottom-right-radius:0.25rem;border-bottom-left-radius:0}}.list-group-flush .list-group-item{border-right:0;border-left:0;border-radius:0}.list-group-flush .list-group-item:last-child{margin-bottom:-1px}.list-group-flush:first-child .list-group-item:first-child{border-top:0}.list-group-flush:last-child .list-group-item:last-child{margin-bottom:0;border-bottom:0}.list-group-item-primary{color:#1d2f42;background-color:#c7d1db}.list-group-item-primary.list-group-item-action:hover,.list-group-item-primary.list-group-item-action:focus{color:#1d2f42;background-color:#b7c4d1}.list-group-item-primary.list-group-item-action.active{color:#fff;background-color:#1d2f42;border-color:#1d2f42}.list-group-item-secondary{color:#232323;background-color:#cbcbcb}.list-group-item-secondary.list-group-item-action:hover,.list-group-item-secondary.list-group-item-action:focus{color:#232323;background-color:#bebebe}.list-group-item-secondary.list-group-item-action.active{color:#fff;background-color:#232323;border-color:#232323}.list-group-item-success{color:#006249;background-color:#b8ecdf}.list-group-item-success.list-group-item-action:hover,.list-group-item-success.list-group-item-action:focus{color:#006249;background-color:#a4e7d6}.list-group-item-success.list-group-item-action.active{color:#fff;background-color:#006249;border-color:#006249}.list-group-item-info{color:#1b4f72;background-color:#c6e2f5}.list-group-item-info.list-group-item-action:hover,.list-group-item-info.list-group-item-action:focus{color:#1b4f72;background-color:#b0d7f1}.list-group-item-info.list-group-item-action.active{color:#fff;background-color:#1b4f72;border-color:#1b4f72}.list-group-item-warning{color:#7e5109;background-color:#fce3bd}.list-group-item-warning.list-group-item-action:hover,.list-group-item-warning.list-group-item-action:focus{color:#7e5109;background-color:#fbd9a5}.list-group-item-warning.list-group-item-action.active{color:#fff;background-color:#7e5109;border-color:#7e5109}.list-group-item-danger{color:#78281f;background-color:#f8cdc8}.list-group-item-danger.list-group-item-action:hover,.list-group-item-danger.list-group-item-action:focus{color:#78281f;background-color:#f5b8b1}.list-group-item-danger.list-group-item-action.active{color:#fff;background-color:#78281f;border-color:#78281f}.list-group-item-light{color:#191919;background-color:#c5c5c5}.list-group-item-light.list-group-item-action:hover,.list-group-item-light.list-group-item-action:focus{color:#191919;background-color:#b8b8b8}.list-group-item-light.list-group-item-action.active{color:#fff;background-color:#191919;border-color:#191919}.list-group-item-dark{color:#5a5e62;background-color:#e8eaed}.list-group-item-dark.list-group-item-action:hover,.list-group-item-dark.list-group-item-action:focus{color:#5a5e62;background-color:#dadde2}.list-group-item-dark.list-group-item-action.active{color:#fff;background-color:#5a5e62;border-color:#5a5e62}.close{float:right;font-size:1.40625rem;font-weight:700;line-height:1;color:#fff;text-shadow:none;opacity:.5}.close:hover{color:#fff;text-decoration:none}.close:not(:disabled):not(.disabled):hover,.close:not(:disabled):not(.disabled):focus{opacity:.75}button.close{padding:0;background-color:transparent;border:0;-webkit-appearance:none;-moz-appearance:none;appearance:none}a.close.disabled{pointer-events:none}.toast{max-width:350px;overflow:hidden;font-size:0.875rem;background-color:rgba(255,255,255,0.85);background-clip:padding-box;border:1px solid rgba(0,0,0,0.1);-webkit-box-shadow:0 0.25rem 0.75rem rgba(0,0,0,0.1);box-shadow:0 0.25rem 0.75rem rgba(0,0,0,0.1);-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px);opacity:0;border-radius:0.25rem}.toast:not(:last-child){margin-bottom:0.75rem}.toast.showing{opacity:1}.toast.show{display:block;opacity:1}.toast.hide{display:none}.toast-header{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;padding:0.25rem 0.75rem;color:#999;background-color:rgba(255,255,255,0.85);background-clip:padding-box;border-bottom:1px solid rgba(0,0,0,0.05)}.toast-body{padding:0.75rem}.modal-open{overflow:hidden}.modal-open .modal{overflow-x:hidden;overflow-y:auto}.modal{position:fixed;top:0;left:0;z-index:1050;display:none;width:100%;height:100%;overflow:hidden;outline:0}.modal-dialog{position:relative;width:auto;margin:0.5rem;pointer-events:none}.modal.fade .modal-dialog{-webkit-transition:-webkit-transform 0.3s ease-out;transition:-webkit-transform 0.3s ease-out;transition:transform 0.3s ease-out;transition:transform 0.3s ease-out, -webkit-transform 0.3s ease-out;-webkit-transform:translate(0, -50px);transform:translate(0, -50px)}@media (prefers-reduced-motion: reduce){.modal.fade .modal-dialog{-webkit-transition:none;transition:none}}.modal.show .modal-dialog{-webkit-transform:none;transform:none}.modal-dialog-scrollable{display:-webkit-box;display:-ms-flexbox;display:flex;max-height:calc(100% - 1rem)}.modal-dialog-scrollable .modal-content{max-height:calc(100vh - 1rem);overflow:hidden}.modal-dialog-scrollable .modal-header,.modal-dialog-scrollable .modal-footer{-ms-flex-negative:0;flex-shrink:0}.modal-dialog-scrollable .modal-body{overflow-y:auto}.modal-dialog-centered{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;min-height:calc(100% - 1rem)}.modal-dialog-centered::before{display:block;height:calc(100vh - 1rem);content:""}.modal-dialog-centered.modal-dialog-scrollable{-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;height:100%}.modal-dialog-centered.modal-dialog-scrollable .modal-content{max-height:none}.modal-dialog-centered.modal-dialog-scrollable::before{content:none}.modal-content{position:relative;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;width:100%;pointer-events:auto;background-color:#303030;background-clip:padding-box;border:1px solid #444;border-radius:0.3rem;outline:0}.modal-backdrop{position:fixed;top:0;left:0;z-index:1040;width:100vw;height:100vh;background-color:#000}.modal-backdrop.fade{opacity:0}.modal-backdrop.show{opacity:0.5}.modal-header{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:start;-ms-flex-align:start;align-items:flex-start;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between;padding:1rem 1rem;border-bottom:1px solid #444;border-top-left-radius:0.3rem;border-top-right-radius:0.3rem}.modal-header .close{padding:1rem 1rem;margin:-1rem -1rem -1rem auto}.modal-title{margin-bottom:0;line-height:1.5}.modal-body{position:relative;-webkit-box-flex:1;-ms-flex:1 1 auto;flex:1 1 auto;padding:1rem}.modal-footer{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:end;-ms-flex-pack:end;justify-content:flex-end;padding:1rem;border-top:1px solid #444;border-bottom-right-radius:0.3rem;border-bottom-left-radius:0.3rem}.modal-footer>:not(:first-child){margin-left:.25rem}.modal-footer>:not(:last-child){margin-right:.25rem}.modal-scrollbar-measure{position:absolute;top:-9999px;width:50px;height:50px;overflow:scroll}@media (min-width: 576px){.modal-dialog{max-width:500px;margin:1.75rem auto}.modal-dialog-scrollable{max-height:calc(100% - 3.5rem)}.modal-dialog-scrollable .modal-content{max-height:calc(100vh - 3.5rem)}.modal-dialog-centered{min-height:calc(100% - 3.5rem)}.modal-dialog-centered::before{height:calc(100vh - 3.5rem)}.modal-sm{max-width:300px}}@media (min-width: 992px){.modal-lg,.modal-xl{max-width:800px}}@media (min-width: 1200px){.modal-xl{max-width:1140px}}.tooltip{position:absolute;z-index:1070;display:block;margin:0;font-family:"Lato", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;white-space:normal;line-break:auto;font-size:0.8203125rem;word-wrap:break-word;opacity:0}.tooltip.show{opacity:0.9}.tooltip .arrow{position:absolute;display:block;width:0.8rem;height:0.4rem}.tooltip .arrow::before{position:absolute;content:"";border-color:transparent;border-style:solid}.bs-tooltip-top,.bs-tooltip-auto[x-placement^="top"]{padding:0.4rem 0}.bs-tooltip-top .arrow,.bs-tooltip-auto[x-placement^="top"] .arrow{bottom:0}.bs-tooltip-top .arrow::before,.bs-tooltip-auto[x-placement^="top"] .arrow::before{top:0;border-width:0.4rem 0.4rem 0;border-top-color:#000}.bs-tooltip-right,.bs-tooltip-auto[x-placement^="right"]{padding:0 0.4rem}.bs-tooltip-right .arrow,.bs-tooltip-auto[x-placement^="right"] .arrow{left:0;width:0.4rem;height:0.8rem}.bs-tooltip-right .arrow::before,.bs-tooltip-auto[x-placement^="right"] .arrow::before{right:0;border-width:0.4rem 0.4rem 0.4rem 0;border-right-color:#000}.bs-tooltip-bottom,.bs-tooltip-auto[x-placement^="bottom"]{padding:0.4rem 0}.bs-tooltip-bottom .arrow,.bs-tooltip-auto[x-placement^="bottom"] .arrow{top:0}.bs-tooltip-bottom .arrow::before,.bs-tooltip-auto[x-placement^="bottom"] .arrow::before{bottom:0;border-width:0 0.4rem 0.4rem;border-bottom-color:#000}.bs-tooltip-left,.bs-tooltip-auto[x-placement^="left"]{padding:0 0.4rem}.bs-tooltip-left .arrow,.bs-tooltip-auto[x-placement^="left"] .arrow{right:0;width:0.4rem;height:0.8rem}.bs-tooltip-left .arrow::before,.bs-tooltip-auto[x-placement^="left"] .arrow::before{left:0;border-width:0.4rem 0 0.4rem 0.4rem;border-left-color:#000}.tooltip-inner{max-width:200px;padding:0.25rem 0.5rem;color:#fff;text-align:center;background-color:#000;border-radius:0.25rem}.popover{position:absolute;top:0;left:0;z-index:1060;display:block;max-width:276px;font-family:"Lato", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;white-space:normal;line-break:auto;font-size:0.8203125rem;word-wrap:break-word;background-color:#303030;background-clip:padding-box;border:1px solid rgba(0,0,0,0.2);border-radius:0.3rem}.popover .arrow{position:absolute;display:block;width:1rem;height:0.5rem;margin:0 0.3rem}.popover .arrow::before,.popover .arrow::after{position:absolute;display:block;content:"";border-color:transparent;border-style:solid}.bs-popover-top,.bs-popover-auto[x-placement^="top"]{margin-bottom:0.5rem}.bs-popover-top>.arrow,.bs-popover-auto[x-placement^="top"]>.arrow{bottom:calc((0.5rem + 1px) * -1)}.bs-popover-top>.arrow::before,.bs-popover-auto[x-placement^="top"]>.arrow::before{bottom:0;border-width:0.5rem 0.5rem 0;border-top-color:rgba(0,0,0,0.25)}.bs-popover-top>.arrow::after,.bs-popover-auto[x-placement^="top"]>.arrow::after{bottom:1px;border-width:0.5rem 0.5rem 0;border-top-color:#303030}.bs-popover-right,.bs-popover-auto[x-placement^="right"]{margin-left:0.5rem}.bs-popover-right>.arrow,.bs-popover-auto[x-placement^="right"]>.arrow{left:calc((0.5rem + 1px) * -1);width:0.5rem;height:1rem;margin:0.3rem 0}.bs-popover-right>.arrow::before,.bs-popover-auto[x-placement^="right"]>.arrow::before{left:0;border-width:0.5rem 0.5rem 0.5rem 0;border-right-color:rgba(0,0,0,0.25)}.bs-popover-right>.arrow::after,.bs-popover-auto[x-placement^="right"]>.arrow::after{left:1px;border-width:0.5rem 0.5rem 0.5rem 0;border-right-color:#303030}.bs-popover-bottom,.bs-popover-auto[x-placement^="bottom"]{margin-top:0.5rem}.bs-popover-bottom>.arrow,.bs-popover-auto[x-placement^="bottom"]>.arrow{top:calc((0.5rem + 1px) * -1)}.bs-popover-bottom>.arrow::before,.bs-popover-auto[x-placement^="bottom"]>.arrow::before{top:0;border-width:0 0.5rem 0.5rem 0.5rem;border-bottom-color:rgba(0,0,0,0.25)}.bs-popover-bottom>.arrow::after,.bs-popover-auto[x-placement^="bottom"]>.arrow::after{top:1px;border-width:0 0.5rem 0.5rem 0.5rem;border-bottom-color:#303030}.bs-popover-bottom .popover-header::before,.bs-popover-auto[x-placement^="bottom"] .popover-header::before{position:absolute;top:0;left:50%;display:block;width:1rem;margin-left:-0.5rem;content:"";border-bottom:1px solid #444}.bs-popover-left,.bs-popover-auto[x-placement^="left"]{margin-right:0.5rem}.bs-popover-left>.arrow,.bs-popover-auto[x-placement^="left"]>.arrow{right:calc((0.5rem + 1px) * -1);width:0.5rem;height:1rem;margin:0.3rem 0}.bs-popover-left>.arrow::before,.bs-popover-auto[x-placement^="left"]>.arrow::before{right:0;border-width:0.5rem 0 0.5rem 0.5rem;border-left-color:rgba(0,0,0,0.25)}.bs-popover-left>.arrow::after,.bs-popover-auto[x-placement^="left"]>.arrow::after{right:1px;border-width:0.5rem 0 0.5rem 0.5rem;border-left-color:#303030}.popover-header{padding:0.5rem 0.75rem;margin-bottom:0;font-size:0.9375rem;background-color:#444;border-bottom:1px solid #373737;border-top-left-radius:calc(0.3rem - 1px);border-top-right-radius:calc(0.3rem - 1px)}.popover-header:empty{display:none}.popover-body{padding:0.5rem 0.75rem;color:#fff}.carousel{position:relative}.carousel.pointer-event{-ms-touch-action:pan-y;touch-action:pan-y}.carousel-inner{position:relative;width:100%;overflow:hidden}.carousel-inner::after{display:block;clear:both;content:""}.carousel-item{position:relative;display:none;float:left;width:100%;margin-right:-100%;-webkit-backface-visibility:hidden;backface-visibility:hidden;-webkit-transition:-webkit-transform 0.6s ease-in-out;transition:-webkit-transform 0.6s ease-in-out;transition:transform 0.6s ease-in-out;transition:transform 0.6s ease-in-out, -webkit-transform 0.6s ease-in-out}@media (prefers-reduced-motion: reduce){.carousel-item{-webkit-transition:none;transition:none}}.carousel-item.active,.carousel-item-next,.carousel-item-prev{display:block}.carousel-item-next:not(.carousel-item-left),.active.carousel-item-right{-webkit-transform:translateX(100%);transform:translateX(100%)}.carousel-item-prev:not(.carousel-item-right),.active.carousel-item-left{-webkit-transform:translateX(-100%);transform:translateX(-100%)}.carousel-fade .carousel-item{opacity:0;-webkit-transition-property:opacity;transition-property:opacity;-webkit-transform:none;transform:none}.carousel-fade .carousel-item.active,.carousel-fade .carousel-item-next.carousel-item-left,.carousel-fade .carousel-item-prev.carousel-item-right{z-index:1;opacity:1}.carousel-fade .active.carousel-item-left,.carousel-fade .active.carousel-item-right{z-index:0;opacity:0;-webkit-transition:0s 0.6s opacity;transition:0s 0.6s opacity}@media (prefers-reduced-motion: reduce){.carousel-fade .active.carousel-item-left,.carousel-fade .active.carousel-item-right{-webkit-transition:none;transition:none}}.carousel-control-prev,.carousel-control-next{position:absolute;top:0;bottom:0;z-index:1;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;width:15%;color:#fff;text-align:center;opacity:0.5;-webkit-transition:opacity 0.15s ease;transition:opacity 0.15s ease}@media (prefers-reduced-motion: reduce){.carousel-control-prev,.carousel-control-next{-webkit-transition:none;transition:none}}.carousel-control-prev:hover,.carousel-control-prev:focus,.carousel-control-next:hover,.carousel-control-next:focus{color:#fff;text-decoration:none;outline:0;opacity:0.9}.carousel-control-prev{left:0}.carousel-control-next{right:0}.carousel-control-prev-icon,.carousel-control-next-icon{display:inline-block;width:20px;height:20px;background:no-repeat 50% / 100% 100%}.carousel-control-prev-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='%23fff' viewBox='0 0 8 8'%3e%3cpath d='M5.25 0l-4 4 4 4 1.5-1.5-2.5-2.5 2.5-2.5-1.5-1.5z'/%3e%3c/svg%3e")}.carousel-control-next-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='%23fff' viewBox='0 0 8 8'%3e%3cpath d='M2.75 0l-1.5 1.5 2.5 2.5-2.5 2.5 1.5 1.5 4-4-4-4z'/%3e%3c/svg%3e")}.carousel-indicators{position:absolute;right:0;bottom:0;left:0;z-index:15;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;padding-left:0;margin-right:15%;margin-left:15%;list-style:none}.carousel-indicators li{-webkit-box-sizing:content-box;box-sizing:content-box;-webkit-box-flex:0;-ms-flex:0 1 auto;flex:0 1 auto;width:30px;height:3px;margin-right:3px;margin-left:3px;text-indent:-999px;cursor:pointer;background-color:#fff;background-clip:padding-box;border-top:10px solid transparent;border-bottom:10px solid transparent;opacity:.5;-webkit-transition:opacity 0.6s ease;transition:opacity 0.6s ease}@media (prefers-reduced-motion: reduce){.carousel-indicators li{-webkit-transition:none;transition:none}}.carousel-indicators .active{opacity:1}.carousel-caption{position:absolute;right:15%;bottom:20px;left:15%;z-index:10;padding-top:20px;padding-bottom:20px;color:#fff;text-align:center}@-webkit-keyframes spinner-border{to{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}@keyframes spinner-border{to{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}.spinner-border{display:inline-block;width:2rem;height:2rem;vertical-align:text-bottom;border:0.25em solid currentColor;border-right-color:transparent;border-radius:50%;-webkit-animation:spinner-border .75s linear infinite;animation:spinner-border .75s linear infinite}.spinner-border-sm{width:1rem;height:1rem;border-width:0.2em}@-webkit-keyframes spinner-grow{0%{-webkit-transform:scale(0);transform:scale(0)}50%{opacity:1}}@keyframes spinner-grow{0%{-webkit-transform:scale(0);transform:scale(0)}50%{opacity:1}}.spinner-grow{display:inline-block;width:2rem;height:2rem;vertical-align:text-bottom;background-color:currentColor;border-radius:50%;opacity:0;-webkit-animation:spinner-grow .75s linear infinite;animation:spinner-grow .75s linear infinite}.spinner-grow-sm{width:1rem;height:1rem}.align-baseline{vertical-align:baseline !important}.align-top{vertical-align:top !important}.align-middle{vertical-align:middle !important}.align-bottom{vertical-align:bottom !important}.align-text-bottom{vertical-align:text-bottom !important}.align-text-top{vertical-align:text-top !important}.bg-primary{background-color:#375a7f !important}a.bg-primary:hover,a.bg-primary:focus,button.bg-primary:hover,button.bg-primary:focus{background-color:#28415b !important}.bg-secondary{background-color:#444 !important}a.bg-secondary:hover,a.bg-secondary:focus,button.bg-secondary:hover,button.bg-secondary:focus{background-color:#2b2a2a !important}.bg-success{background-color:#00bc8c !important}a.bg-success:hover,a.bg-success:focus,button.bg-success:hover,button.bg-success:focus{background-color:#008966 !important}.bg-info{background-color:#3498DB !important}a.bg-info:hover,a.bg-info:focus,button.bg-info:hover,button.bg-info:focus{background-color:#217dbb !important}.bg-warning{background-color:#F39C12 !important}a.bg-warning:hover,a.bg-warning:focus,button.bg-warning:hover,button.bg-warning:focus{background-color:#c87f0a !important}.bg-danger{background-color:#E74C3C !important}a.bg-danger:hover,a.bg-danger:focus,button.bg-danger:hover,button.bg-danger:focus{background-color:#d62c1a !important}.bg-light{background-color:#303030 !important}a.bg-light:hover,a.bg-light:focus,button.bg-light:hover,button.bg-light:focus{background-color:#171616 !important}.bg-dark{background-color:#adb5bd !important}a.bg-dark:hover,a.bg-dark:focus,button.bg-dark:hover,button.bg-dark:focus{background-color:#919ca6 !important}.bg-white{background-color:#fff !important}.bg-transparent{background-color:transparent !important}.border{border:1px solid #dee2e6 !important}.border-top{border-top:1px solid #dee2e6 !important}.border-right{border-right:1px solid #dee2e6 !important}.border-bottom{border-bottom:1px solid #dee2e6 !important}.border-left{border-left:1px solid #dee2e6 !important}.border-0{border:0 !important}.border-top-0{border-top:0 !important}.border-right-0{border-right:0 !important}.border-bottom-0{border-bottom:0 !important}.border-left-0{border-left:0 !important}.border-primary{border-color:#375a7f !important}.border-secondary{border-color:#444 !important}.border-success{border-color:#00bc8c !important}.border-info{border-color:#3498DB !important}.border-warning{border-color:#F39C12 !important}.border-danger{border-color:#E74C3C !important}.border-light{border-color:#303030 !important}.border-dark{border-color:#adb5bd !important}.border-white{border-color:#fff !important}.rounded-sm{border-radius:0.2rem !important}.rounded{border-radius:0.25rem !important}.rounded-top{border-top-left-radius:0.25rem !important;border-top-right-radius:0.25rem !important}.rounded-right{border-top-right-radius:0.25rem !important;border-bottom-right-radius:0.25rem !important}.rounded-bottom{border-bottom-right-radius:0.25rem !important;border-bottom-left-radius:0.25rem !important}.rounded-left{border-top-left-radius:0.25rem !important;border-bottom-left-radius:0.25rem !important}.rounded-lg{border-radius:0.3rem !important}.rounded-circle{border-radius:50% !important}.rounded-pill{border-radius:50rem !important}.rounded-0{border-radius:0 !important}.clearfix::after{display:block;clear:both;content:""}.d-none{display:none !important}.d-inline{display:inline !important}.d-inline-block{display:inline-block !important}.d-block{display:block !important}.d-table{display:table !important}.d-table-row{display:table-row !important}.d-table-cell{display:table-cell !important}.d-flex{display:-webkit-box !important;display:-ms-flexbox !important;display:flex !important}.d-inline-flex{display:-webkit-inline-box !important;display:-ms-inline-flexbox !important;display:inline-flex !important}@media (min-width: 576px){.d-sm-none{display:none !important}.d-sm-inline{display:inline !important}.d-sm-inline-block{display:inline-block !important}.d-sm-block{display:block !important}.d-sm-table{display:table !important}.d-sm-table-row{display:table-row !important}.d-sm-table-cell{display:table-cell !important}.d-sm-flex{display:-webkit-box !important;display:-ms-flexbox !important;display:flex !important}.d-sm-inline-flex{display:-webkit-inline-box !important;display:-ms-inline-flexbox !important;display:inline-flex !important}}@media (min-width: 768px){.d-md-none{display:none !important}.d-md-inline{display:inline !important}.d-md-inline-block{display:inline-block !important}.d-md-block{display:block !important}.d-md-table{display:table !important}.d-md-table-row{display:table-row !important}.d-md-table-cell{display:table-cell !important}.d-md-flex{display:-webkit-box !important;display:-ms-flexbox !important;display:flex !important}.d-md-inline-flex{display:-webkit-inline-box !important;display:-ms-inline-flexbox !important;display:inline-flex !important}}@media (min-width: 992px){.d-lg-none{display:none !important}.d-lg-inline{display:inline !important}.d-lg-inline-block{display:inline-block !important}.d-lg-block{display:block !important}.d-lg-table{display:table !important}.d-lg-table-row{display:table-row !important}.d-lg-table-cell{display:table-cell !important}.d-lg-flex{display:-webkit-box !important;display:-ms-flexbox !important;display:flex !important}.d-lg-inline-flex{display:-webkit-inline-box !important;display:-ms-inline-flexbox !important;display:inline-flex !important}}@media (min-width: 1200px){.d-xl-none{display:none !important}.d-xl-inline{display:inline !important}.d-xl-inline-block{display:inline-block !important}.d-xl-block{display:block !important}.d-xl-table{display:table !important}.d-xl-table-row{display:table-row !important}.d-xl-table-cell{display:table-cell !important}.d-xl-flex{display:-webkit-box !important;display:-ms-flexbox !important;display:flex !important}.d-xl-inline-flex{display:-webkit-inline-box !important;display:-ms-inline-flexbox !important;display:inline-flex !important}}@media print{.d-print-none{display:none !important}.d-print-inline{display:inline !important}.d-print-inline-block{display:inline-block !important}.d-print-block{display:block !important}.d-print-table{display:table !important}.d-print-table-row{display:table-row !important}.d-print-table-cell{display:table-cell !important}.d-print-flex{display:-webkit-box !important;display:-ms-flexbox !important;display:flex !important}.d-print-inline-flex{display:-webkit-inline-box !important;display:-ms-inline-flexbox !important;display:inline-flex !important}}.embed-responsive{position:relative;display:block;width:100%;padding:0;overflow:hidden}.embed-responsive::before{display:block;content:""}.embed-responsive .embed-responsive-item,.embed-responsive iframe,.embed-responsive embed,.embed-responsive object,.embed-responsive video{position:absolute;top:0;bottom:0;left:0;width:100%;height:100%;border:0}.embed-responsive-21by9::before{padding-top:42.8571428571%}.embed-responsive-16by9::before{padding-top:56.25%}.embed-responsive-4by3::before{padding-top:75%}.embed-responsive-1by1::before{padding-top:100%}.flex-row{-webkit-box-orient:horizontal !important;-webkit-box-direction:normal !important;-ms-flex-direction:row !important;flex-direction:row !important}.flex-column{-webkit-box-orient:vertical !important;-webkit-box-direction:normal !important;-ms-flex-direction:column !important;flex-direction:column !important}.flex-row-reverse{-webkit-box-orient:horizontal !important;-webkit-box-direction:reverse !important;-ms-flex-direction:row-reverse !important;flex-direction:row-reverse !important}.flex-column-reverse{-webkit-box-orient:vertical !important;-webkit-box-direction:reverse !important;-ms-flex-direction:column-reverse !important;flex-direction:column-reverse !important}.flex-wrap{-ms-flex-wrap:wrap !important;flex-wrap:wrap !important}.flex-nowrap{-ms-flex-wrap:nowrap !important;flex-wrap:nowrap !important}.flex-wrap-reverse{-ms-flex-wrap:wrap-reverse !important;flex-wrap:wrap-reverse !important}.flex-fill{-webkit-box-flex:1 !important;-ms-flex:1 1 auto !important;flex:1 1 auto !important}.flex-grow-0{-webkit-box-flex:0 !important;-ms-flex-positive:0 !important;flex-grow:0 !important}.flex-grow-1{-webkit-box-flex:1 !important;-ms-flex-positive:1 !important;flex-grow:1 !important}.flex-shrink-0{-ms-flex-negative:0 !important;flex-shrink:0 !important}.flex-shrink-1{-ms-flex-negative:1 !important;flex-shrink:1 !important}.justify-content-start{-webkit-box-pack:start !important;-ms-flex-pack:start !important;justify-content:flex-start !important}.justify-content-end{-webkit-box-pack:end !important;-ms-flex-pack:end !important;justify-content:flex-end !important}.justify-content-center{-webkit-box-pack:center !important;-ms-flex-pack:center !important;justify-content:center !important}.justify-content-between{-webkit-box-pack:justify !important;-ms-flex-pack:justify !important;justify-content:space-between !important}.justify-content-around{-ms-flex-pack:distribute !important;justify-content:space-around !important}.align-items-start{-webkit-box-align:start !important;-ms-flex-align:start !important;align-items:flex-start !important}.align-items-end{-webkit-box-align:end !important;-ms-flex-align:end !important;align-items:flex-end !important}.align-items-center{-webkit-box-align:center !important;-ms-flex-align:center !important;align-items:center !important}.align-items-baseline{-webkit-box-align:baseline !important;-ms-flex-align:baseline !important;align-items:baseline !important}.align-items-stretch{-webkit-box-align:stretch !important;-ms-flex-align:stretch !important;align-items:stretch !important}.align-content-start{-ms-flex-line-pack:start !important;align-content:flex-start !important}.align-content-end{-ms-flex-line-pack:end !important;align-content:flex-end !important}.align-content-center{-ms-flex-line-pack:center !important;align-content:center !important}.align-content-between{-ms-flex-line-pack:justify !important;align-content:space-between !important}.align-content-around{-ms-flex-line-pack:distribute !important;align-content:space-around !important}.align-content-stretch{-ms-flex-line-pack:stretch !important;align-content:stretch !important}.align-self-auto{-ms-flex-item-align:auto !important;align-self:auto !important}.align-self-start{-ms-flex-item-align:start !important;align-self:flex-start !important}.align-self-end{-ms-flex-item-align:end !important;align-self:flex-end !important}.align-self-center{-ms-flex-item-align:center !important;align-self:center !important}.align-self-baseline{-ms-flex-item-align:baseline !important;align-self:baseline !important}.align-self-stretch{-ms-flex-item-align:stretch !important;align-self:stretch !important}@media (min-width: 576px){.flex-sm-row{-webkit-box-orient:horizontal !important;-webkit-box-direction:normal !important;-ms-flex-direction:row !important;flex-direction:row !important}.flex-sm-column{-webkit-box-orient:vertical !important;-webkit-box-direction:normal !important;-ms-flex-direction:column !important;flex-direction:column !important}.flex-sm-row-reverse{-webkit-box-orient:horizontal !important;-webkit-box-direction:reverse !important;-ms-flex-direction:row-reverse !important;flex-direction:row-reverse !important}.flex-sm-column-reverse{-webkit-box-orient:vertical !important;-webkit-box-direction:reverse !important;-ms-flex-direction:column-reverse !important;flex-direction:column-reverse !important}.flex-sm-wrap{-ms-flex-wrap:wrap !important;flex-wrap:wrap !important}.flex-sm-nowrap{-ms-flex-wrap:nowrap !important;flex-wrap:nowrap !important}.flex-sm-wrap-reverse{-ms-flex-wrap:wrap-reverse !important;flex-wrap:wrap-reverse !important}.flex-sm-fill{-webkit-box-flex:1 !important;-ms-flex:1 1 auto !important;flex:1 1 auto !important}.flex-sm-grow-0{-webkit-box-flex:0 !important;-ms-flex-positive:0 !important;flex-grow:0 !important}.flex-sm-grow-1{-webkit-box-flex:1 !important;-ms-flex-positive:1 !important;flex-grow:1 !important}.flex-sm-shrink-0{-ms-flex-negative:0 !important;flex-shrink:0 !important}.flex-sm-shrink-1{-ms-flex-negative:1 !important;flex-shrink:1 !important}.justify-content-sm-start{-webkit-box-pack:start !important;-ms-flex-pack:start !important;justify-content:flex-start !important}.justify-content-sm-end{-webkit-box-pack:end !important;-ms-flex-pack:end !important;justify-content:flex-end !important}.justify-content-sm-center{-webkit-box-pack:center !important;-ms-flex-pack:center !important;justify-content:center !important}.justify-content-sm-between{-webkit-box-pack:justify !important;-ms-flex-pack:justify !important;justify-content:space-between !important}.justify-content-sm-around{-ms-flex-pack:distribute !important;justify-content:space-around !important}.align-items-sm-start{-webkit-box-align:start !important;-ms-flex-align:start !important;align-items:flex-start !important}.align-items-sm-end{-webkit-box-align:end !important;-ms-flex-align:end !important;align-items:flex-end !important}.align-items-sm-center{-webkit-box-align:center !important;-ms-flex-align:center !important;align-items:center !important}.align-items-sm-baseline{-webkit-box-align:baseline !important;-ms-flex-align:baseline !important;align-items:baseline !important}.align-items-sm-stretch{-webkit-box-align:stretch !important;-ms-flex-align:stretch !important;align-items:stretch !important}.align-content-sm-start{-ms-flex-line-pack:start !important;align-content:flex-start !important}.align-content-sm-end{-ms-flex-line-pack:end !important;align-content:flex-end !important}.align-content-sm-center{-ms-flex-line-pack:center !important;align-content:center !important}.align-content-sm-between{-ms-flex-line-pack:justify !important;align-content:space-between !important}.align-content-sm-around{-ms-flex-line-pack:distribute !important;align-content:space-around !important}.align-content-sm-stretch{-ms-flex-line-pack:stretch !important;align-content:stretch !important}.align-self-sm-auto{-ms-flex-item-align:auto !important;align-self:auto !important}.align-self-sm-start{-ms-flex-item-align:start !important;align-self:flex-start !important}.align-self-sm-end{-ms-flex-item-align:end !important;align-self:flex-end !important}.align-self-sm-center{-ms-flex-item-align:center !important;align-self:center !important}.align-self-sm-baseline{-ms-flex-item-align:baseline !important;align-self:baseline !important}.align-self-sm-stretch{-ms-flex-item-align:stretch !important;align-self:stretch !important}}@media (min-width: 768px){.flex-md-row{-webkit-box-orient:horizontal !important;-webkit-box-direction:normal !important;-ms-flex-direction:row !important;flex-direction:row !important}.flex-md-column{-webkit-box-orient:vertical !important;-webkit-box-direction:normal !important;-ms-flex-direction:column !important;flex-direction:column !important}.flex-md-row-reverse{-webkit-box-orient:horizontal !important;-webkit-box-direction:reverse !important;-ms-flex-direction:row-reverse !important;flex-direction:row-reverse !important}.flex-md-column-reverse{-webkit-box-orient:vertical !important;-webkit-box-direction:reverse !important;-ms-flex-direction:column-reverse !important;flex-direction:column-reverse !important}.flex-md-wrap{-ms-flex-wrap:wrap !important;flex-wrap:wrap !important}.flex-md-nowrap{-ms-flex-wrap:nowrap !important;flex-wrap:nowrap !important}.flex-md-wrap-reverse{-ms-flex-wrap:wrap-reverse !important;flex-wrap:wrap-reverse !important}.flex-md-fill{-webkit-box-flex:1 !important;-ms-flex:1 1 auto !important;flex:1 1 auto !important}.flex-md-grow-0{-webkit-box-flex:0 !important;-ms-flex-positive:0 !important;flex-grow:0 !important}.flex-md-grow-1{-webkit-box-flex:1 !important;-ms-flex-positive:1 !important;flex-grow:1 !important}.flex-md-shrink-0{-ms-flex-negative:0 !important;flex-shrink:0 !important}.flex-md-shrink-1{-ms-flex-negative:1 !important;flex-shrink:1 !important}.justify-content-md-start{-webkit-box-pack:start !important;-ms-flex-pack:start !important;justify-content:flex-start !important}.justify-content-md-end{-webkit-box-pack:end !important;-ms-flex-pack:end !important;justify-content:flex-end !important}.justify-content-md-center{-webkit-box-pack:center !important;-ms-flex-pack:center !important;justify-content:center !important}.justify-content-md-between{-webkit-box-pack:justify !important;-ms-flex-pack:justify !important;justify-content:space-between !important}.justify-content-md-around{-ms-flex-pack:distribute !important;justify-content:space-around !important}.align-items-md-start{-webkit-box-align:start !important;-ms-flex-align:start !important;align-items:flex-start !important}.align-items-md-end{-webkit-box-align:end !important;-ms-flex-align:end !important;align-items:flex-end !important}.align-items-md-center{-webkit-box-align:center !important;-ms-flex-align:center !important;align-items:center !important}.align-items-md-baseline{-webkit-box-align:baseline !important;-ms-flex-align:baseline !important;align-items:baseline !important}.align-items-md-stretch{-webkit-box-align:stretch !important;-ms-flex-align:stretch !important;align-items:stretch !important}.align-content-md-start{-ms-flex-line-pack:start !important;align-content:flex-start !important}.align-content-md-end{-ms-flex-line-pack:end !important;align-content:flex-end !important}.align-content-md-center{-ms-flex-line-pack:center !important;align-content:center !important}.align-content-md-between{-ms-flex-line-pack:justify !important;align-content:space-between !important}.align-content-md-around{-ms-flex-line-pack:distribute !important;align-content:space-around !important}.align-content-md-stretch{-ms-flex-line-pack:stretch !important;align-content:stretch !important}.align-self-md-auto{-ms-flex-item-align:auto !important;align-self:auto !important}.align-self-md-start{-ms-flex-item-align:start !important;align-self:flex-start !important}.align-self-md-end{-ms-flex-item-align:end !important;align-self:flex-end !important}.align-self-md-center{-ms-flex-item-align:center !important;align-self:center !important}.align-self-md-baseline{-ms-flex-item-align:baseline !important;align-self:baseline !important}.align-self-md-stretch{-ms-flex-item-align:stretch !important;align-self:stretch !important}}@media (min-width: 992px){.flex-lg-row{-webkit-box-orient:horizontal !important;-webkit-box-direction:normal !important;-ms-flex-direction:row !important;flex-direction:row !important}.flex-lg-column{-webkit-box-orient:vertical !important;-webkit-box-direction:normal !important;-ms-flex-direction:column !important;flex-direction:column !important}.flex-lg-row-reverse{-webkit-box-orient:horizontal !important;-webkit-box-direction:reverse !important;-ms-flex-direction:row-reverse !important;flex-direction:row-reverse !important}.flex-lg-column-reverse{-webkit-box-orient:vertical !important;-webkit-box-direction:reverse !important;-ms-flex-direction:column-reverse !important;flex-direction:column-reverse !important}.flex-lg-wrap{-ms-flex-wrap:wrap !important;flex-wrap:wrap !important}.flex-lg-nowrap{-ms-flex-wrap:nowrap !important;flex-wrap:nowrap !important}.flex-lg-wrap-reverse{-ms-flex-wrap:wrap-reverse !important;flex-wrap:wrap-reverse !important}.flex-lg-fill{-webkit-box-flex:1 !important;-ms-flex:1 1 auto !important;flex:1 1 auto !important}.flex-lg-grow-0{-webkit-box-flex:0 !important;-ms-flex-positive:0 !important;flex-grow:0 !important}.flex-lg-grow-1{-webkit-box-flex:1 !important;-ms-flex-positive:1 !important;flex-grow:1 !important}.flex-lg-shrink-0{-ms-flex-negative:0 !important;flex-shrink:0 !important}.flex-lg-shrink-1{-ms-flex-negative:1 !important;flex-shrink:1 !important}.justify-content-lg-start{-webkit-box-pack:start !important;-ms-flex-pack:start !important;justify-content:flex-start !important}.justify-content-lg-end{-webkit-box-pack:end !important;-ms-flex-pack:end !important;justify-content:flex-end !important}.justify-content-lg-center{-webkit-box-pack:center !important;-ms-flex-pack:center !important;justify-content:center !important}.justify-content-lg-between{-webkit-box-pack:justify !important;-ms-flex-pack:justify !important;justify-content:space-between !important}.justify-content-lg-around{-ms-flex-pack:distribute !important;justify-content:space-around !important}.align-items-lg-start{-webkit-box-align:start !important;-ms-flex-align:start !important;align-items:flex-start !important}.align-items-lg-end{-webkit-box-align:end !important;-ms-flex-align:end !important;align-items:flex-end !important}.align-items-lg-center{-webkit-box-align:center !important;-ms-flex-align:center !important;align-items:center !important}.align-items-lg-baseline{-webkit-box-align:baseline !important;-ms-flex-align:baseline !important;align-items:baseline !important}.align-items-lg-stretch{-webkit-box-align:stretch !important;-ms-flex-align:stretch !important;align-items:stretch !important}.align-content-lg-start{-ms-flex-line-pack:start !important;align-content:flex-start !important}.align-content-lg-end{-ms-flex-line-pack:end !important;align-content:flex-end !important}.align-content-lg-center{-ms-flex-line-pack:center !important;align-content:center !important}.align-content-lg-between{-ms-flex-line-pack:justify !important;align-content:space-between !important}.align-content-lg-around{-ms-flex-line-pack:distribute !important;align-content:space-around !important}.align-content-lg-stretch{-ms-flex-line-pack:stretch !important;align-content:stretch !important}.align-self-lg-auto{-ms-flex-item-align:auto !important;align-self:auto !important}.align-self-lg-start{-ms-flex-item-align:start !important;align-self:flex-start !important}.align-self-lg-end{-ms-flex-item-align:end !important;align-self:flex-end !important}.align-self-lg-center{-ms-flex-item-align:center !important;align-self:center !important}.align-self-lg-baseline{-ms-flex-item-align:baseline !important;align-self:baseline !important}.align-self-lg-stretch{-ms-flex-item-align:stretch !important;align-self:stretch !important}}@media (min-width: 1200px){.flex-xl-row{-webkit-box-orient:horizontal !important;-webkit-box-direction:normal !important;-ms-flex-direction:row !important;flex-direction:row !important}.flex-xl-column{-webkit-box-orient:vertical !important;-webkit-box-direction:normal !important;-ms-flex-direction:column !important;flex-direction:column !important}.flex-xl-row-reverse{-webkit-box-orient:horizontal !important;-webkit-box-direction:reverse !important;-ms-flex-direction:row-reverse !important;flex-direction:row-reverse !important}.flex-xl-column-reverse{-webkit-box-orient:vertical !important;-webkit-box-direction:reverse !important;-ms-flex-direction:column-reverse !important;flex-direction:column-reverse !important}.flex-xl-wrap{-ms-flex-wrap:wrap !important;flex-wrap:wrap !important}.flex-xl-nowrap{-ms-flex-wrap:nowrap !important;flex-wrap:nowrap !important}.flex-xl-wrap-reverse{-ms-flex-wrap:wrap-reverse !important;flex-wrap:wrap-reverse !important}.flex-xl-fill{-webkit-box-flex:1 !important;-ms-flex:1 1 auto !important;flex:1 1 auto !important}.flex-xl-grow-0{-webkit-box-flex:0 !important;-ms-flex-positive:0 !important;flex-grow:0 !important}.flex-xl-grow-1{-webkit-box-flex:1 !important;-ms-flex-positive:1 !important;flex-grow:1 !important}.flex-xl-shrink-0{-ms-flex-negative:0 !important;flex-shrink:0 !important}.flex-xl-shrink-1{-ms-flex-negative:1 !important;flex-shrink:1 !important}.justify-content-xl-start{-webkit-box-pack:start !important;-ms-flex-pack:start !important;justify-content:flex-start !important}.justify-content-xl-end{-webkit-box-pack:end !important;-ms-flex-pack:end !important;justify-content:flex-end !important}.justify-content-xl-center{-webkit-box-pack:center !important;-ms-flex-pack:center !important;justify-content:center !important}.justify-content-xl-between{-webkit-box-pack:justify !important;-ms-flex-pack:justify !important;justify-content:space-between !important}.justify-content-xl-around{-ms-flex-pack:distribute !important;justify-content:space-around !important}.align-items-xl-start{-webkit-box-align:start !important;-ms-flex-align:start !important;align-items:flex-start !important}.align-items-xl-end{-webkit-box-align:end !important;-ms-flex-align:end !important;align-items:flex-end !important}.align-items-xl-center{-webkit-box-align:center !important;-ms-flex-align:center !important;align-items:center !important}.align-items-xl-baseline{-webkit-box-align:baseline !important;-ms-flex-align:baseline !important;align-items:baseline !important}.align-items-xl-stretch{-webkit-box-align:stretch !important;-ms-flex-align:stretch !important;align-items:stretch !important}.align-content-xl-start{-ms-flex-line-pack:start !important;align-content:flex-start !important}.align-content-xl-end{-ms-flex-line-pack:end !important;align-content:flex-end !important}.align-content-xl-center{-ms-flex-line-pack:center !important;align-content:center !important}.align-content-xl-between{-ms-flex-line-pack:justify !important;align-content:space-between !important}.align-content-xl-around{-ms-flex-line-pack:distribute !important;align-content:space-around !important}.align-content-xl-stretch{-ms-flex-line-pack:stretch !important;align-content:stretch !important}.align-self-xl-auto{-ms-flex-item-align:auto !important;align-self:auto !important}.align-self-xl-start{-ms-flex-item-align:start !important;align-self:flex-start !important}.align-self-xl-end{-ms-flex-item-align:end !important;align-self:flex-end !important}.align-self-xl-center{-ms-flex-item-align:center !important;align-self:center !important}.align-self-xl-baseline{-ms-flex-item-align:baseline !important;align-self:baseline !important}.align-self-xl-stretch{-ms-flex-item-align:stretch !important;align-self:stretch !important}}.float-left{float:left !important}.float-right{float:right !important}.float-none{float:none !important}@media (min-width: 576px){.float-sm-left{float:left !important}.float-sm-right{float:right !important}.float-sm-none{float:none !important}}@media (min-width: 768px){.float-md-left{float:left !important}.float-md-right{float:right !important}.float-md-none{float:none !important}}@media (min-width: 992px){.float-lg-left{float:left !important}.float-lg-right{float:right !important}.float-lg-none{float:none !important}}@media (min-width: 1200px){.float-xl-left{float:left !important}.float-xl-right{float:right !important}.float-xl-none{float:none !important}}.overflow-auto{overflow:auto !important}.overflow-hidden{overflow:hidden !important}.position-static{position:static !important}.position-relative{position:relative !important}.position-absolute{position:absolute !important}.position-fixed{position:fixed !important}.position-sticky{position:-webkit-sticky !important;position:sticky !important}.fixed-top{position:fixed;top:0;right:0;left:0;z-index:1030}.fixed-bottom{position:fixed;right:0;bottom:0;left:0;z-index:1030}@supports (position: -webkit-sticky) or (position: sticky){.sticky-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}}.sr-only{position:absolute;width:1px;height:1px;padding:0;overflow:hidden;clip:rect(0, 0, 0, 0);white-space:nowrap;border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;overflow:visible;clip:auto;white-space:normal}.shadow-sm{-webkit-box-shadow:0 0.125rem 0.25rem rgba(0,0,0,0.075) !important;box-shadow:0 0.125rem 0.25rem rgba(0,0,0,0.075) !important}.shadow{-webkit-box-shadow:0 0.5rem 1rem rgba(0,0,0,0.15) !important;box-shadow:0 0.5rem 1rem rgba(0,0,0,0.15) !important}.shadow-lg{-webkit-box-shadow:0 1rem 3rem rgba(0,0,0,0.175) !important;box-shadow:0 1rem 3rem rgba(0,0,0,0.175) !important}.shadow-none{-webkit-box-shadow:none !important;box-shadow:none !important}.w-25{width:25% !important}.w-50{width:50% !important}.w-75{width:75% !important}.w-100{width:100% !important}.w-auto{width:auto !important}.h-25{height:25% !important}.h-50{height:50% !important}.h-75{height:75% !important}.h-100{height:100% !important}.h-auto{height:auto !important}.mw-100{max-width:100% !important}.mh-100{max-height:100% !important}.min-vw-100{min-width:100vw !important}.min-vh-100{min-height:100vh !important}.vw-100{width:100vw !important}.vh-100{height:100vh !important}.stretched-link::after{position:absolute;top:0;right:0;bottom:0;left:0;z-index:1;pointer-events:auto;content:"";background-color:rgba(0,0,0,0)}.m-0{margin:0 !important}.mt-0,.my-0{margin-top:0 !important}.mr-0,.mx-0{margin-right:0 !important}.mb-0,.my-0{margin-bottom:0 !important}.ml-0,.mx-0{margin-left:0 !important}.m-1{margin:0.25rem !important}.mt-1,.my-1{margin-top:0.25rem !important}.mr-1,.mx-1{margin-right:0.25rem !important}.mb-1,.my-1{margin-bottom:0.25rem !important}.ml-1,.mx-1{margin-left:0.25rem !important}.m-2{margin:0.5rem !important}.mt-2,.my-2{margin-top:0.5rem !important}.mr-2,.mx-2{margin-right:0.5rem !important}.mb-2,.my-2{margin-bottom:0.5rem !important}.ml-2,.mx-2{margin-left:0.5rem !important}.m-3{margin:1rem !important}.mt-3,.my-3{margin-top:1rem !important}.mr-3,.mx-3{margin-right:1rem !important}.mb-3,.my-3{margin-bottom:1rem !important}.ml-3,.mx-3{margin-left:1rem !important}.m-4{margin:1.5rem !important}.mt-4,.my-4{margin-top:1.5rem !important}.mr-4,.mx-4{margin-right:1.5rem !important}.mb-4,.my-4{margin-bottom:1.5rem !important}.ml-4,.mx-4{margin-left:1.5rem !important}.m-5{margin:3rem !important}.mt-5,.my-5{margin-top:3rem !important}.mr-5,.mx-5{margin-right:3rem !important}.mb-5,.my-5{margin-bottom:3rem !important}.ml-5,.mx-5{margin-left:3rem !important}.p-0{padding:0 !important}.pt-0,.py-0{padding-top:0 !important}.pr-0,.px-0{padding-right:0 !important}.pb-0,.py-0{padding-bottom:0 !important}.pl-0,.px-0{padding-left:0 !important}.p-1{padding:0.25rem !important}.pt-1,.py-1{padding-top:0.25rem !important}.pr-1,.px-1{padding-right:0.25rem !important}.pb-1,.py-1{padding-bottom:0.25rem !important}.pl-1,.px-1{padding-left:0.25rem !important}.p-2{padding:0.5rem !important}.pt-2,.py-2{padding-top:0.5rem !important}.pr-2,.px-2{padding-right:0.5rem !important}.pb-2,.py-2{padding-bottom:0.5rem !important}.pl-2,.px-2{padding-left:0.5rem !important}.p-3{padding:1rem !important}.pt-3,.py-3{padding-top:1rem !important}.pr-3,.px-3{padding-right:1rem !important}.pb-3,.py-3{padding-bottom:1rem !important}.pl-3,.px-3{padding-left:1rem !important}.p-4{padding:1.5rem !important}.pt-4,.py-4{padding-top:1.5rem !important}.pr-4,.px-4{padding-right:1.5rem !important}.pb-4,.py-4{padding-bottom:1.5rem !important}.pl-4,.px-4{padding-left:1.5rem !important}.p-5{padding:3rem !important}.pt-5,.py-5{padding-top:3rem !important}.pr-5,.px-5{padding-right:3rem !important}.pb-5,.py-5{padding-bottom:3rem !important}.pl-5,.px-5{padding-left:3rem !important}.m-n1{margin:-0.25rem !important}.mt-n1,.my-n1{margin-top:-0.25rem !important}.mr-n1,.mx-n1{margin-right:-0.25rem !important}.mb-n1,.my-n1{margin-bottom:-0.25rem !important}.ml-n1,.mx-n1{margin-left:-0.25rem !important}.m-n2{margin:-0.5rem !important}.mt-n2,.my-n2{margin-top:-0.5rem !important}.mr-n2,.mx-n2{margin-right:-0.5rem !important}.mb-n2,.my-n2{margin-bottom:-0.5rem !important}.ml-n2,.mx-n2{margin-left:-0.5rem !important}.m-n3{margin:-1rem !important}.mt-n3,.my-n3{margin-top:-1rem !important}.mr-n3,.mx-n3{margin-right:-1rem !important}.mb-n3,.my-n3{margin-bottom:-1rem !important}.ml-n3,.mx-n3{margin-left:-1rem !important}.m-n4{margin:-1.5rem !important}.mt-n4,.my-n4{margin-top:-1.5rem !important}.mr-n4,.mx-n4{margin-right:-1.5rem !important}.mb-n4,.my-n4{margin-bottom:-1.5rem !important}.ml-n4,.mx-n4{margin-left:-1.5rem !important}.m-n5{margin:-3rem !important}.mt-n5,.my-n5{margin-top:-3rem !important}.mr-n5,.mx-n5{margin-right:-3rem !important}.mb-n5,.my-n5{margin-bottom:-3rem !important}.ml-n5,.mx-n5{margin-left:-3rem !important}.m-auto{margin:auto !important}.mt-auto,.my-auto{margin-top:auto !important}.mr-auto,.mx-auto{margin-right:auto !important}.mb-auto,.my-auto{margin-bottom:auto !important}.ml-auto,.mx-auto{margin-left:auto !important}@media (min-width: 576px){.m-sm-0{margin:0 !important}.mt-sm-0,.my-sm-0{margin-top:0 !important}.mr-sm-0,.mx-sm-0{margin-right:0 !important}.mb-sm-0,.my-sm-0{margin-bottom:0 !important}.ml-sm-0,.mx-sm-0{margin-left:0 !important}.m-sm-1{margin:0.25rem !important}.mt-sm-1,.my-sm-1{margin-top:0.25rem !important}.mr-sm-1,.mx-sm-1{margin-right:0.25rem !important}.mb-sm-1,.my-sm-1{margin-bottom:0.25rem !important}.ml-sm-1,.mx-sm-1{margin-left:0.25rem !important}.m-sm-2{margin:0.5rem !important}.mt-sm-2,.my-sm-2{margin-top:0.5rem !important}.mr-sm-2,.mx-sm-2{margin-right:0.5rem !important}.mb-sm-2,.my-sm-2{margin-bottom:0.5rem !important}.ml-sm-2,.mx-sm-2{margin-left:0.5rem !important}.m-sm-3{margin:1rem !important}.mt-sm-3,.my-sm-3{margin-top:1rem !important}.mr-sm-3,.mx-sm-3{margin-right:1rem !important}.mb-sm-3,.my-sm-3{margin-bottom:1rem !important}.ml-sm-3,.mx-sm-3{margin-left:1rem !important}.m-sm-4{margin:1.5rem !important}.mt-sm-4,.my-sm-4{margin-top:1.5rem !important}.mr-sm-4,.mx-sm-4{margin-right:1.5rem !important}.mb-sm-4,.my-sm-4{margin-bottom:1.5rem !important}.ml-sm-4,.mx-sm-4{margin-left:1.5rem !important}.m-sm-5{margin:3rem !important}.mt-sm-5,.my-sm-5{margin-top:3rem !important}.mr-sm-5,.mx-sm-5{margin-right:3rem !important}.mb-sm-5,.my-sm-5{margin-bottom:3rem !important}.ml-sm-5,.mx-sm-5{margin-left:3rem !important}.p-sm-0{padding:0 !important}.pt-sm-0,.py-sm-0{padding-top:0 !important}.pr-sm-0,.px-sm-0{padding-right:0 !important}.pb-sm-0,.py-sm-0{padding-bottom:0 !important}.pl-sm-0,.px-sm-0{padding-left:0 !important}.p-sm-1{padding:0.25rem !important}.pt-sm-1,.py-sm-1{padding-top:0.25rem !important}.pr-sm-1,.px-sm-1{padding-right:0.25rem !important}.pb-sm-1,.py-sm-1{padding-bottom:0.25rem !important}.pl-sm-1,.px-sm-1{padding-left:0.25rem !important}.p-sm-2{padding:0.5rem !important}.pt-sm-2,.py-sm-2{padding-top:0.5rem !important}.pr-sm-2,.px-sm-2{padding-right:0.5rem !important}.pb-sm-2,.py-sm-2{padding-bottom:0.5rem !important}.pl-sm-2,.px-sm-2{padding-left:0.5rem !important}.p-sm-3{padding:1rem !important}.pt-sm-3,.py-sm-3{padding-top:1rem !important}.pr-sm-3,.px-sm-3{padding-right:1rem !important}.pb-sm-3,.py-sm-3{padding-bottom:1rem !important}.pl-sm-3,.px-sm-3{padding-left:1rem !important}.p-sm-4{padding:1.5rem !important}.pt-sm-4,.py-sm-4{padding-top:1.5rem !important}.pr-sm-4,.px-sm-4{padding-right:1.5rem !important}.pb-sm-4,.py-sm-4{padding-bottom:1.5rem !important}.pl-sm-4,.px-sm-4{padding-left:1.5rem !important}.p-sm-5{padding:3rem !important}.pt-sm-5,.py-sm-5{padding-top:3rem !important}.pr-sm-5,.px-sm-5{padding-right:3rem !important}.pb-sm-5,.py-sm-5{padding-bottom:3rem !important}.pl-sm-5,.px-sm-5{padding-left:3rem !important}.m-sm-n1{margin:-0.25rem !important}.mt-sm-n1,.my-sm-n1{margin-top:-0.25rem !important}.mr-sm-n1,.mx-sm-n1{margin-right:-0.25rem !important}.mb-sm-n1,.my-sm-n1{margin-bottom:-0.25rem !important}.ml-sm-n1,.mx-sm-n1{margin-left:-0.25rem !important}.m-sm-n2{margin:-0.5rem !important}.mt-sm-n2,.my-sm-n2{margin-top:-0.5rem !important}.mr-sm-n2,.mx-sm-n2{margin-right:-0.5rem !important}.mb-sm-n2,.my-sm-n2{margin-bottom:-0.5rem !important}.ml-sm-n2,.mx-sm-n2{margin-left:-0.5rem !important}.m-sm-n3{margin:-1rem !important}.mt-sm-n3,.my-sm-n3{margin-top:-1rem !important}.mr-sm-n3,.mx-sm-n3{margin-right:-1rem !important}.mb-sm-n3,.my-sm-n3{margin-bottom:-1rem !important}.ml-sm-n3,.mx-sm-n3{margin-left:-1rem !important}.m-sm-n4{margin:-1.5rem !important}.mt-sm-n4,.my-sm-n4{margin-top:-1.5rem !important}.mr-sm-n4,.mx-sm-n4{margin-right:-1.5rem !important}.mb-sm-n4,.my-sm-n4{margin-bottom:-1.5rem !important}.ml-sm-n4,.mx-sm-n4{margin-left:-1.5rem !important}.m-sm-n5{margin:-3rem !important}.mt-sm-n5,.my-sm-n5{margin-top:-3rem !important}.mr-sm-n5,.mx-sm-n5{margin-right:-3rem !important}.mb-sm-n5,.my-sm-n5{margin-bottom:-3rem !important}.ml-sm-n5,.mx-sm-n5{margin-left:-3rem !important}.m-sm-auto{margin:auto !important}.mt-sm-auto,.my-sm-auto{margin-top:auto !important}.mr-sm-auto,.mx-sm-auto{margin-right:auto !important}.mb-sm-auto,.my-sm-auto{margin-bottom:auto !important}.ml-sm-auto,.mx-sm-auto{margin-left:auto !important}}@media (min-width: 768px){.m-md-0{margin:0 !important}.mt-md-0,.my-md-0{margin-top:0 !important}.mr-md-0,.mx-md-0{margin-right:0 !important}.mb-md-0,.my-md-0{margin-bottom:0 !important}.ml-md-0,.mx-md-0{margin-left:0 !important}.m-md-1{margin:0.25rem !important}.mt-md-1,.my-md-1{margin-top:0.25rem !important}.mr-md-1,.mx-md-1{margin-right:0.25rem !important}.mb-md-1,.my-md-1{margin-bottom:0.25rem !important}.ml-md-1,.mx-md-1{margin-left:0.25rem !important}.m-md-2{margin:0.5rem !important}.mt-md-2,.my-md-2{margin-top:0.5rem !important}.mr-md-2,.mx-md-2{margin-right:0.5rem !important}.mb-md-2,.my-md-2{margin-bottom:0.5rem !important}.ml-md-2,.mx-md-2{margin-left:0.5rem !important}.m-md-3{margin:1rem !important}.mt-md-3,.my-md-3{margin-top:1rem !important}.mr-md-3,.mx-md-3{margin-right:1rem !important}.mb-md-3,.my-md-3{margin-bottom:1rem !important}.ml-md-3,.mx-md-3{margin-left:1rem !important}.m-md-4{margin:1.5rem !important}.mt-md-4,.my-md-4{margin-top:1.5rem !important}.mr-md-4,.mx-md-4{margin-right:1.5rem !important}.mb-md-4,.my-md-4{margin-bottom:1.5rem !important}.ml-md-4,.mx-md-4{margin-left:1.5rem !important}.m-md-5{margin:3rem !important}.mt-md-5,.my-md-5{margin-top:3rem !important}.mr-md-5,.mx-md-5{margin-right:3rem !important}.mb-md-5,.my-md-5{margin-bottom:3rem !important}.ml-md-5,.mx-md-5{margin-left:3rem !important}.p-md-0{padding:0 !important}.pt-md-0,.py-md-0{padding-top:0 !important}.pr-md-0,.px-md-0{padding-right:0 !important}.pb-md-0,.py-md-0{padding-bottom:0 !important}.pl-md-0,.px-md-0{padding-left:0 !important}.p-md-1{padding:0.25rem !important}.pt-md-1,.py-md-1{padding-top:0.25rem !important}.pr-md-1,.px-md-1{padding-right:0.25rem !important}.pb-md-1,.py-md-1{padding-bottom:0.25rem !important}.pl-md-1,.px-md-1{padding-left:0.25rem !important}.p-md-2{padding:0.5rem !important}.pt-md-2,.py-md-2{padding-top:0.5rem !important}.pr-md-2,.px-md-2{padding-right:0.5rem !important}.pb-md-2,.py-md-2{padding-bottom:0.5rem !important}.pl-md-2,.px-md-2{padding-left:0.5rem !important}.p-md-3{padding:1rem !important}.pt-md-3,.py-md-3{padding-top:1rem !important}.pr-md-3,.px-md-3{padding-right:1rem !important}.pb-md-3,.py-md-3{padding-bottom:1rem !important}.pl-md-3,.px-md-3{padding-left:1rem !important}.p-md-4{padding:1.5rem !important}.pt-md-4,.py-md-4{padding-top:1.5rem !important}.pr-md-4,.px-md-4{padding-right:1.5rem !important}.pb-md-4,.py-md-4{padding-bottom:1.5rem !important}.pl-md-4,.px-md-4{padding-left:1.5rem !important}.p-md-5{padding:3rem !important}.pt-md-5,.py-md-5{padding-top:3rem !important}.pr-md-5,.px-md-5{padding-right:3rem !important}.pb-md-5,.py-md-5{padding-bottom:3rem !important}.pl-md-5,.px-md-5{padding-left:3rem !important}.m-md-n1{margin:-0.25rem !important}.mt-md-n1,.my-md-n1{margin-top:-0.25rem !important}.mr-md-n1,.mx-md-n1{margin-right:-0.25rem !important}.mb-md-n1,.my-md-n1{margin-bottom:-0.25rem !important}.ml-md-n1,.mx-md-n1{margin-left:-0.25rem !important}.m-md-n2{margin:-0.5rem !important}.mt-md-n2,.my-md-n2{margin-top:-0.5rem !important}.mr-md-n2,.mx-md-n2{margin-right:-0.5rem !important}.mb-md-n2,.my-md-n2{margin-bottom:-0.5rem !important}.ml-md-n2,.mx-md-n2{margin-left:-0.5rem !important}.m-md-n3{margin:-1rem !important}.mt-md-n3,.my-md-n3{margin-top:-1rem !important}.mr-md-n3,.mx-md-n3{margin-right:-1rem !important}.mb-md-n3,.my-md-n3{margin-bottom:-1rem !important}.ml-md-n3,.mx-md-n3{margin-left:-1rem !important}.m-md-n4{margin:-1.5rem !important}.mt-md-n4,.my-md-n4{margin-top:-1.5rem !important}.mr-md-n4,.mx-md-n4{margin-right:-1.5rem !important}.mb-md-n4,.my-md-n4{margin-bottom:-1.5rem !important}.ml-md-n4,.mx-md-n4{margin-left:-1.5rem !important}.m-md-n5{margin:-3rem !important}.mt-md-n5,.my-md-n5{margin-top:-3rem !important}.mr-md-n5,.mx-md-n5{margin-right:-3rem !important}.mb-md-n5,.my-md-n5{margin-bottom:-3rem !important}.ml-md-n5,.mx-md-n5{margin-left:-3rem !important}.m-md-auto{margin:auto !important}.mt-md-auto,.my-md-auto{margin-top:auto !important}.mr-md-auto,.mx-md-auto{margin-right:auto !important}.mb-md-auto,.my-md-auto{margin-bottom:auto !important}.ml-md-auto,.mx-md-auto{margin-left:auto !important}}@media (min-width: 992px){.m-lg-0{margin:0 !important}.mt-lg-0,.my-lg-0{margin-top:0 !important}.mr-lg-0,.mx-lg-0{margin-right:0 !important}.mb-lg-0,.my-lg-0{margin-bottom:0 !important}.ml-lg-0,.mx-lg-0{margin-left:0 !important}.m-lg-1{margin:0.25rem !important}.mt-lg-1,.my-lg-1{margin-top:0.25rem !important}.mr-lg-1,.mx-lg-1{margin-right:0.25rem !important}.mb-lg-1,.my-lg-1{margin-bottom:0.25rem !important}.ml-lg-1,.mx-lg-1{margin-left:0.25rem !important}.m-lg-2{margin:0.5rem !important}.mt-lg-2,.my-lg-2{margin-top:0.5rem !important}.mr-lg-2,.mx-lg-2{margin-right:0.5rem !important}.mb-lg-2,.my-lg-2{margin-bottom:0.5rem !important}.ml-lg-2,.mx-lg-2{margin-left:0.5rem !important}.m-lg-3{margin:1rem !important}.mt-lg-3,.my-lg-3{margin-top:1rem !important}.mr-lg-3,.mx-lg-3{margin-right:1rem !important}.mb-lg-3,.my-lg-3{margin-bottom:1rem !important}.ml-lg-3,.mx-lg-3{margin-left:1rem !important}.m-lg-4{margin:1.5rem !important}.mt-lg-4,.my-lg-4{margin-top:1.5rem !important}.mr-lg-4,.mx-lg-4{margin-right:1.5rem !important}.mb-lg-4,.my-lg-4{margin-bottom:1.5rem !important}.ml-lg-4,.mx-lg-4{margin-left:1.5rem !important}.m-lg-5{margin:3rem !important}.mt-lg-5,.my-lg-5{margin-top:3rem !important}.mr-lg-5,.mx-lg-5{margin-right:3rem !important}.mb-lg-5,.my-lg-5{margin-bottom:3rem !important}.ml-lg-5,.mx-lg-5{margin-left:3rem !important}.p-lg-0{padding:0 !important}.pt-lg-0,.py-lg-0{padding-top:0 !important}.pr-lg-0,.px-lg-0{padding-right:0 !important}.pb-lg-0,.py-lg-0{padding-bottom:0 !important}.pl-lg-0,.px-lg-0{padding-left:0 !important}.p-lg-1{padding:0.25rem !important}.pt-lg-1,.py-lg-1{padding-top:0.25rem !important}.pr-lg-1,.px-lg-1{padding-right:0.25rem !important}.pb-lg-1,.py-lg-1{padding-bottom:0.25rem !important}.pl-lg-1,.px-lg-1{padding-left:0.25rem !important}.p-lg-2{padding:0.5rem !important}.pt-lg-2,.py-lg-2{padding-top:0.5rem !important}.pr-lg-2,.px-lg-2{padding-right:0.5rem !important}.pb-lg-2,.py-lg-2{padding-bottom:0.5rem !important}.pl-lg-2,.px-lg-2{padding-left:0.5rem !important}.p-lg-3{padding:1rem !important}.pt-lg-3,.py-lg-3{padding-top:1rem !important}.pr-lg-3,.px-lg-3{padding-right:1rem !important}.pb-lg-3,.py-lg-3{padding-bottom:1rem !important}.pl-lg-3,.px-lg-3{padding-left:1rem !important}.p-lg-4{padding:1.5rem !important}.pt-lg-4,.py-lg-4{padding-top:1.5rem !important}.pr-lg-4,.px-lg-4{padding-right:1.5rem !important}.pb-lg-4,.py-lg-4{padding-bottom:1.5rem !important}.pl-lg-4,.px-lg-4{padding-left:1.5rem !important}.p-lg-5{padding:3rem !important}.pt-lg-5,.py-lg-5{padding-top:3rem !important}.pr-lg-5,.px-lg-5{padding-right:3rem !important}.pb-lg-5,.py-lg-5{padding-bottom:3rem !important}.pl-lg-5,.px-lg-5{padding-left:3rem !important}.m-lg-n1{margin:-0.25rem !important}.mt-lg-n1,.my-lg-n1{margin-top:-0.25rem !important}.mr-lg-n1,.mx-lg-n1{margin-right:-0.25rem !important}.mb-lg-n1,.my-lg-n1{margin-bottom:-0.25rem !important}.ml-lg-n1,.mx-lg-n1{margin-left:-0.25rem !important}.m-lg-n2{margin:-0.5rem !important}.mt-lg-n2,.my-lg-n2{margin-top:-0.5rem !important}.mr-lg-n2,.mx-lg-n2{margin-right:-0.5rem !important}.mb-lg-n2,.my-lg-n2{margin-bottom:-0.5rem !important}.ml-lg-n2,.mx-lg-n2{margin-left:-0.5rem !important}.m-lg-n3{margin:-1rem !important}.mt-lg-n3,.my-lg-n3{margin-top:-1rem !important}.mr-lg-n3,.mx-lg-n3{margin-right:-1rem !important}.mb-lg-n3,.my-lg-n3{margin-bottom:-1rem !important}.ml-lg-n3,.mx-lg-n3{margin-left:-1rem !important}.m-lg-n4{margin:-1.5rem !important}.mt-lg-n4,.my-lg-n4{margin-top:-1.5rem !important}.mr-lg-n4,.mx-lg-n4{margin-right:-1.5rem !important}.mb-lg-n4,.my-lg-n4{margin-bottom:-1.5rem !important}.ml-lg-n4,.mx-lg-n4{margin-left:-1.5rem !important}.m-lg-n5{margin:-3rem !important}.mt-lg-n5,.my-lg-n5{margin-top:-3rem !important}.mr-lg-n5,.mx-lg-n5{margin-right:-3rem !important}.mb-lg-n5,.my-lg-n5{margin-bottom:-3rem !important}.ml-lg-n5,.mx-lg-n5{margin-left:-3rem !important}.m-lg-auto{margin:auto !important}.mt-lg-auto,.my-lg-auto{margin-top:auto !important}.mr-lg-auto,.mx-lg-auto{margin-right:auto !important}.mb-lg-auto,.my-lg-auto{margin-bottom:auto !important}.ml-lg-auto,.mx-lg-auto{margin-left:auto !important}}@media (min-width: 1200px){.m-xl-0{margin:0 !important}.mt-xl-0,.my-xl-0{margin-top:0 !important}.mr-xl-0,.mx-xl-0{margin-right:0 !important}.mb-xl-0,.my-xl-0{margin-bottom:0 !important}.ml-xl-0,.mx-xl-0{margin-left:0 !important}.m-xl-1{margin:0.25rem !important}.mt-xl-1,.my-xl-1{margin-top:0.25rem !important}.mr-xl-1,.mx-xl-1{margin-right:0.25rem !important}.mb-xl-1,.my-xl-1{margin-bottom:0.25rem !important}.ml-xl-1,.mx-xl-1{margin-left:0.25rem !important}.m-xl-2{margin:0.5rem !important}.mt-xl-2,.my-xl-2{margin-top:0.5rem !important}.mr-xl-2,.mx-xl-2{margin-right:0.5rem !important}.mb-xl-2,.my-xl-2{margin-bottom:0.5rem !important}.ml-xl-2,.mx-xl-2{margin-left:0.5rem !important}.m-xl-3{margin:1rem !important}.mt-xl-3,.my-xl-3{margin-top:1rem !important}.mr-xl-3,.mx-xl-3{margin-right:1rem !important}.mb-xl-3,.my-xl-3{margin-bottom:1rem !important}.ml-xl-3,.mx-xl-3{margin-left:1rem !important}.m-xl-4{margin:1.5rem !important}.mt-xl-4,.my-xl-4{margin-top:1.5rem !important}.mr-xl-4,.mx-xl-4{margin-right:1.5rem !important}.mb-xl-4,.my-xl-4{margin-bottom:1.5rem !important}.ml-xl-4,.mx-xl-4{margin-left:1.5rem !important}.m-xl-5{margin:3rem !important}.mt-xl-5,.my-xl-5{margin-top:3rem !important}.mr-xl-5,.mx-xl-5{margin-right:3rem !important}.mb-xl-5,.my-xl-5{margin-bottom:3rem !important}.ml-xl-5,.mx-xl-5{margin-left:3rem !important}.p-xl-0{padding:0 !important}.pt-xl-0,.py-xl-0{padding-top:0 !important}.pr-xl-0,.px-xl-0{padding-right:0 !important}.pb-xl-0,.py-xl-0{padding-bottom:0 !important}.pl-xl-0,.px-xl-0{padding-left:0 !important}.p-xl-1{padding:0.25rem !important}.pt-xl-1,.py-xl-1{padding-top:0.25rem !important}.pr-xl-1,.px-xl-1{padding-right:0.25rem !important}.pb-xl-1,.py-xl-1{padding-bottom:0.25rem !important}.pl-xl-1,.px-xl-1{padding-left:0.25rem !important}.p-xl-2{padding:0.5rem !important}.pt-xl-2,.py-xl-2{padding-top:0.5rem !important}.pr-xl-2,.px-xl-2{padding-right:0.5rem !important}.pb-xl-2,.py-xl-2{padding-bottom:0.5rem !important}.pl-xl-2,.px-xl-2{padding-left:0.5rem !important}.p-xl-3{padding:1rem !important}.pt-xl-3,.py-xl-3{padding-top:1rem !important}.pr-xl-3,.px-xl-3{padding-right:1rem !important}.pb-xl-3,.py-xl-3{padding-bottom:1rem !important}.pl-xl-3,.px-xl-3{padding-left:1rem !important}.p-xl-4{padding:1.5rem !important}.pt-xl-4,.py-xl-4{padding-top:1.5rem !important}.pr-xl-4,.px-xl-4{padding-right:1.5rem !important}.pb-xl-4,.py-xl-4{padding-bottom:1.5rem !important}.pl-xl-4,.px-xl-4{padding-left:1.5rem !important}.p-xl-5{padding:3rem !important}.pt-xl-5,.py-xl-5{padding-top:3rem !important}.pr-xl-5,.px-xl-5{padding-right:3rem !important}.pb-xl-5,.py-xl-5{padding-bottom:3rem !important}.pl-xl-5,.px-xl-5{padding-left:3rem !important}.m-xl-n1{margin:-0.25rem !important}.mt-xl-n1,.my-xl-n1{margin-top:-0.25rem !important}.mr-xl-n1,.mx-xl-n1{margin-right:-0.25rem !important}.mb-xl-n1,.my-xl-n1{margin-bottom:-0.25rem !important}.ml-xl-n1,.mx-xl-n1{margin-left:-0.25rem !important}.m-xl-n2{margin:-0.5rem !important}.mt-xl-n2,.my-xl-n2{margin-top:-0.5rem !important}.mr-xl-n2,.mx-xl-n2{margin-right:-0.5rem !important}.mb-xl-n2,.my-xl-n2{margin-bottom:-0.5rem !important}.ml-xl-n2,.mx-xl-n2{margin-left:-0.5rem !important}.m-xl-n3{margin:-1rem !important}.mt-xl-n3,.my-xl-n3{margin-top:-1rem !important}.mr-xl-n3,.mx-xl-n3{margin-right:-1rem !important}.mb-xl-n3,.my-xl-n3{margin-bottom:-1rem !important}.ml-xl-n3,.mx-xl-n3{margin-left:-1rem !important}.m-xl-n4{margin:-1.5rem !important}.mt-xl-n4,.my-xl-n4{margin-top:-1.5rem !important}.mr-xl-n4,.mx-xl-n4{margin-right:-1.5rem !important}.mb-xl-n4,.my-xl-n4{margin-bottom:-1.5rem !important}.ml-xl-n4,.mx-xl-n4{margin-left:-1.5rem !important}.m-xl-n5{margin:-3rem !important}.mt-xl-n5,.my-xl-n5{margin-top:-3rem !important}.mr-xl-n5,.mx-xl-n5{margin-right:-3rem !important}.mb-xl-n5,.my-xl-n5{margin-bottom:-3rem !important}.ml-xl-n5,.mx-xl-n5{margin-left:-3rem !important}.m-xl-auto{margin:auto !important}.mt-xl-auto,.my-xl-auto{margin-top:auto !important}.mr-xl-auto,.mx-xl-auto{margin-right:auto !important}.mb-xl-auto,.my-xl-auto{margin-bottom:auto !important}.ml-xl-auto,.mx-xl-auto{margin-left:auto !important}}.text-monospace{font-family:SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace !important}.text-justify{text-align:justify !important}.text-wrap{white-space:normal !important}.text-nowrap{white-space:nowrap !important}.text-truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.text-left{text-align:left !important}.text-right{text-align:right !important}.text-center{text-align:center !important}@media (min-width: 576px){.text-sm-left{text-align:left !important}.text-sm-right{text-align:right !important}.text-sm-center{text-align:center !important}}@media (min-width: 768px){.text-md-left{text-align:left !important}.text-md-right{text-align:right !important}.text-md-center{text-align:center !important}}@media (min-width: 992px){.text-lg-left{text-align:left !important}.text-lg-right{text-align:right !important}.text-lg-center{text-align:center !important}}@media (min-width: 1200px){.text-xl-left{text-align:left !important}.text-xl-right{text-align:right !important}.text-xl-center{text-align:center !important}}.text-lowercase{text-transform:lowercase !important}.text-uppercase{text-transform:uppercase !important}.text-capitalize{text-transform:capitalize !important}.font-weight-light{font-weight:300 !important}.font-weight-lighter{font-weight:lighter !important}.font-weight-normal{font-weight:400 !important}.font-weight-bold{font-weight:700 !important}.font-weight-bolder{font-weight:bolder !important}.font-italic{font-style:italic !important}.text-white{color:#fff !important}.text-primary{color:#375a7f !important}a.text-primary:hover,a.text-primary:focus{color:#20344a !important}.text-secondary{color:#444 !important}a.text-secondary:hover,a.text-secondary:focus{color:#1e1e1e !important}.text-success{color:#00bc8c !important}a.text-success:hover,a.text-success:focus{color:#007053 !important}.text-info{color:#3498DB !important}a.text-info:hover,a.text-info:focus{color:#1d6fa5 !important}.text-warning{color:#F39C12 !important}a.text-warning:hover,a.text-warning:focus{color:#b06f09 !important}.text-danger{color:#E74C3C !important}a.text-danger:hover,a.text-danger:focus{color:#bf2718 !important}.text-light{color:#303030 !important}a.text-light:hover,a.text-light:focus{color:#0a0a0a !important}.text-dark{color:#adb5bd !important}a.text-dark:hover,a.text-dark:focus{color:#838f9b !important}.text-body{color:#fff !important}.text-muted{color:#999 !important}.text-black-50{color:rgba(0,0,0,0.5) !important}.text-white-50{color:rgba(255,255,255,0.5) !important}.text-hide{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.text-decoration-none{text-decoration:none !important}.text-break{word-break:break-word !important;overflow-wrap:break-word !important}.text-reset{color:inherit !important}.visible{visibility:visible !important}.invisible{visibility:hidden !important}@media print{*,*::before,*::after{text-shadow:none !important;-webkit-box-shadow:none !important;box-shadow:none !important}a:not(.btn){text-decoration:underline}abbr[title]::after{content:" (" attr(title) ")"}pre{white-space:pre-wrap !important}pre,blockquote{border:1px solid #adb5bd;page-break-inside:avoid}thead{display:table-header-group}tr,img{page-break-inside:avoid}p,h2,h3{orphans:3;widows:3}h2,h3{page-break-after:avoid}@page{size:a3}body{min-width:992px !important}.container{min-width:992px !important}.navbar{display:none}.badge{border:1px solid #000}.table{border-collapse:collapse !important}.table td,.table th{background-color:#fff !important}.table-bordered th,.table-bordered td{border:1px solid #dee2e6 !important}.table-dark{color:inherit}.table-dark th,.table-dark td,.table-dark thead th,.table-dark tbody+tbody{border-color:#444}.table .thead-dark th{color:inherit;border-color:#444}}.bg-primary .navbar-nav .active>.nav-link{color:#00bc8c !important}.bg-dark{background-color:#00bc8c !important}.bg-dark.navbar-dark .navbar-nav .nav-link:focus,.bg-dark.navbar-dark .navbar-nav .nav-link:hover,.bg-dark.navbar-dark .navbar-nav .active>.nav-link{color:#375a7f !important}.blockquote-footer{color:#999}.table-primary,.table-primary>th,.table-primary>td{background-color:#375a7f}.table-secondary,.table-secondary>th,.table-secondary>td{background-color:#444}.table-light,.table-light>th,.table-light>td{background-color:#303030}.table-dark,.table-dark>th,.table-dark>td{background-color:#adb5bd}.table-success,.table-success>th,.table-success>td{background-color:#00bc8c}.table-info,.table-info>th,.table-info>td{background-color:#3498DB}.table-danger,.table-danger>th,.table-danger>td{background-color:#E74C3C}.table-warning,.table-warning>th,.table-warning>td{background-color:#F39C12}.table-active,.table-active>th,.table-active>td{background-color:rgba(0,0,0,0.075)}.table-hover .table-primary:hover,.table-hover .table-primary:hover>th,.table-hover .table-primary:hover>td{background-color:#2f4d6d}.table-hover .table-secondary:hover,.table-hover .table-secondary:hover>th,.table-hover .table-secondary:hover>td{background-color:#373737}.table-hover .table-light:hover,.table-hover .table-light:hover>th,.table-hover .table-light:hover>td{background-color:#232323}.table-hover .table-dark:hover,.table-hover .table-dark:hover>th,.table-hover .table-dark:hover>td{background-color:#9fa8b2}.table-hover .table-success:hover,.table-hover .table-success:hover>th,.table-hover .table-success:hover>td{background-color:#00a379}.table-hover .table-info:hover,.table-hover .table-info:hover>th,.table-hover .table-info:hover>td{background-color:#258cd1}.table-hover .table-danger:hover,.table-hover .table-danger:hover>th,.table-hover .table-danger:hover>td{background-color:#e43725}.table-hover .table-warning:hover,.table-hover .table-warning:hover>th,.table-hover .table-warning:hover>td{background-color:#e08e0b}.table-hover .table-active:hover,.table-hover .table-active:hover>th,.table-hover .table-active:hover>td{background-color:rgba(0,0,0,0.075)}.input-group-addon{color:#fff}.nav-tabs .nav-link,.nav-tabs .nav-link.active,.nav-tabs .nav-link.active:focus,.nav-tabs .nav-link.active:hover,.nav-tabs .nav-item.open .nav-link,.nav-tabs .nav-item.open .nav-link:focus,.nav-tabs .nav-item.open .nav-link:hover,.nav-pills .nav-link,.nav-pills .nav-link.active,.nav-pills .nav-link.active:focus,.nav-pills .nav-link.active:hover,.nav-pills .nav-item.open .nav-link,.nav-pills .nav-item.open .nav-link:focus,.nav-pills .nav-item.open .nav-link:hover{color:#fff}.breadcrumb a{color:#fff}.pagination a:hover{text-decoration:none}.close{opacity:0.4}.close:hover,.close:focus{opacity:1}.alert{border:none;color:#fff}.alert a,.alert .alert-link{color:#fff;text-decoration:underline}.alert-primary{background-color:#375a7f}.alert-secondary{background-color:#444}.alert-success{background-color:#00bc8c}.alert-info{background-color:#3498DB}.alert-warning{background-color:#F39C12}.alert-danger{background-color:#E74C3C}.alert-light{background-color:#303030}.alert-dark{background-color:#adb5bd}.list-group-item-action{color:#fff}.list-group-item-action:hover,.list-group-item-action:focus{background-color:#444;color:#fff}.list-group-item-action .list-group-item-heading{color:#fff}
-
-body, .text-body, .navbar-brand, .badge-light, .btn-secondary {
-  color: #dedede !important;
-}
-
-.form-control, .form-control:focus {
-  background-color: var(--secondary);
-  color: #fff;
-}
-
-.form-control:disabled {
-  background-color: var(--secondary);
-  opacity: .5;
-}
-
-.custom-select {
-  color: #fff;
-  background-color: var(--secondary);
-}
-
-.mark {
-  background-color: #333;
-}
+:root{--blue:#375a7f;--indigo:#6610f2;--purple:#6f42c1;--pink:#e83e8c;--red:#e74c3c;--orange:#fd7e14;--yellow:#f39c12;--green:#00bc8c;--teal:#20c997;--cyan:#3498db;--white:#fff;--gray:#888;--gray-dark:#303030;--primary:#375a7f;--secondary:#444;--success:#00bc8c;--info:#3498db;--warning:#f39c12;--danger:#e74c3c;--light:#303030;--dark:#dee2e6;--breakpoint-xs:0;--breakpoint-sm:576px;--breakpoint-md:768px;--breakpoint-lg:992px;--breakpoint-xl:1200px;--font-family-sans-serif:"Lato",-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol";--font-family-monospace:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace}*,::after,::before{box-sizing:border-box}html{font-family:sans-serif;line-height:1.15;-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:transparent}article,aside,figcaption,figure,footer,header,hgroup,main,nav,section{display:block}body{margin:0;font-family:Lato,-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol";font-size:.9375rem;font-weight:400;line-height:1.5;color:#dee2e6;text-align:left;background-color:#222}[tabindex="-1"]:focus:not(:focus-visible){outline:0!important}hr{box-sizing:content-box;height:0;overflow:visible}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem}p{margin-top:0;margin-bottom:1rem}abbr[data-original-title],abbr[title]{text-decoration:underline;text-decoration:underline dotted;cursor:help;border-bottom:0;text-decoration-skip-ink:none}address{margin-bottom:1rem;font-style:normal;line-height:inherit}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}b,strong{font-weight:bolder}small{font-size:80%}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:#00bc8c;text-decoration:none;background-color:transparent}a:hover{color:#007053;text-decoration:underline}a:not([href]){color:inherit;text-decoration:none}a:not([href]):hover{color:inherit;text-decoration:none}code,kbd,pre,samp{font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;font-size:1em}pre{margin-top:0;margin-bottom:1rem;overflow:auto;-ms-overflow-style:scrollbar}figure{margin:0 0 1rem}img{vertical-align:middle;border-style:none}svg{overflow:hidden;vertical-align:middle}table{border-collapse:collapse}caption{padding-top:.75rem;padding-bottom:.75rem;color:#888;text-align:left;caption-side:bottom}th{text-align:inherit}label{display:inline-block;margin-bottom:.5rem}button{border-radius:0}button:focus{outline:1px dotted;outline:5px auto -webkit-focus-ring-color}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,input{overflow:visible}button,select{text-transform:none}[role=button]{cursor:pointer}select{word-wrap:normal}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]:not(:disabled),[type=reset]:not(:disabled),[type=submit]:not(:disabled),button:not(:disabled){cursor:pointer}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{padding:0;border-style:none}input[type=checkbox],input[type=radio]{box-sizing:border-box;padding:0}textarea{overflow:auto;resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{display:block;width:100%;max-width:100%;padding:0;margin-bottom:.5rem;font-size:1.5rem;line-height:inherit;color:inherit;white-space:normal}progress{vertical-align:baseline}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:none}[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}output{display:inline-block}summary{display:list-item;cursor:pointer}template{display:none}[hidden]{display:none!important}.h1,.h2,.h3,.h4,.h5,.h6,h1,h2,h3,h4,h5,h6{margin-bottom:.5rem;font-weight:500;line-height:1.2}.h1,h1{font-size:3rem}.h2,h2{font-size:2.5rem}.h3,h3{font-size:2rem}.h4,h4{font-size:1.40625rem}.h5,h5{font-size:1.17188rem}.h6,h6{font-size:.9375rem}.lead{font-size:1.17188rem;font-weight:300}.display-1{font-size:6rem;font-weight:300;line-height:1.2}.display-2{font-size:5.5rem;font-weight:300;line-height:1.2}.display-3{font-size:4.5rem;font-weight:300;line-height:1.2}.display-4{font-size:3.5rem;font-weight:300;line-height:1.2}hr{margin-top:1rem;margin-bottom:1rem;border:0;border-top:1px solid rgba(0,0,0,.1)}.small,small{font-size:80%;font-weight:400}.mark,mark{padding:.2em;background-color:#333}.list-unstyled{padding-left:0;list-style:none}.list-inline{padding-left:0;list-style:none}.list-inline-item{display:inline-block}.list-inline-item:not(:last-child){margin-right:.5rem}.initialism{font-size:90%;text-transform:uppercase}.blockquote{margin-bottom:1rem;font-size:1.17188rem}.blockquote-footer{display:block;font-size:80%;color:#888}.blockquote-footer::before{content:"\2014\00A0"}.img-fluid{max-width:100%;height:auto}.img-thumbnail{padding:.25rem;background-color:#222;border:1px solid #dee2e6;border-radius:.25rem;max-width:100%;height:auto}.figure{display:inline-block}.figure-img{margin-bottom:.5rem;line-height:1}.figure-caption{font-size:90%;color:#888}code{font-size:87.5%;color:#e83e8c;word-wrap:break-word}a>code{color:inherit}kbd{padding:.2rem .4rem;font-size:87.5%;color:#fff;background-color:#222;border-radius:.2rem}kbd kbd{padding:0;font-size:100%;font-weight:700}pre{display:block;font-size:87.5%;color:inherit}pre code{font-size:inherit;color:inherit;word-break:normal}.pre-scrollable{max-height:340px;overflow-y:scroll}.container{width:100%;padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}@media (min-width:576px){.container{max-width:540px}}@media (min-width:768px){.container{max-width:720px}}@media (min-width:992px){.container{max-width:960px}}@media (min-width:1200px){.container{max-width:1140px}}.container-fluid,.container-lg,.container-md,.container-sm,.container-xl{width:100%;padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}@media (min-width:576px){.container,.container-sm{max-width:540px}}@media (min-width:768px){.container,.container-md,.container-sm{max-width:720px}}@media (min-width:992px){.container,.container-lg,.container-md,.container-sm{max-width:960px}}@media (min-width:1200px){.container,.container-lg,.container-md,.container-sm,.container-xl{max-width:1140px}}.row{display:flex;flex-wrap:wrap;margin-right:-15px;margin-left:-15px}.no-gutters{margin-right:0;margin-left:0}.no-gutters>.col,.no-gutters>[class*=col-]{padding-right:0;padding-left:0}.col,.col-1,.col-10,.col-11,.col-12,.col-2,.col-3,.col-4,.col-5,.col-6,.col-7,.col-8,.col-9,.col-auto,.col-lg,.col-lg-1,.col-lg-10,.col-lg-11,.col-lg-12,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9,.col-lg-auto,.col-md,.col-md-1,.col-md-10,.col-md-11,.col-md-12,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9,.col-md-auto,.col-sm,.col-sm-1,.col-sm-10,.col-sm-11,.col-sm-12,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9,.col-sm-auto,.col-xl,.col-xl-1,.col-xl-10,.col-xl-11,.col-xl-12,.col-xl-2,.col-xl-3,.col-xl-4,.col-xl-5,.col-xl-6,.col-xl-7,.col-xl-8,.col-xl-9,.col-xl-auto{position:relative;width:100%;padding-right:15px;padding-left:15px}.col{flex-basis:0;flex-grow:1;min-width:0;max-width:100%}.row-cols-1>*{flex:0 0 100%;max-width:100%}.row-cols-2>*{flex:0 0 50%;max-width:50%}.row-cols-3>*{flex:0 0 33.33333%;max-width:33.33333%}.row-cols-4>*{flex:0 0 25%;max-width:25%}.row-cols-5>*{flex:0 0 20%;max-width:20%}.row-cols-6>*{flex:0 0 16.66667%;max-width:16.66667%}.col-auto{flex:0 0 auto;width:auto;max-width:100%}.col-1{flex:0 0 8.33333%;max-width:8.33333%}.col-2{flex:0 0 16.66667%;max-width:16.66667%}.col-3{flex:0 0 25%;max-width:25%}.col-4{flex:0 0 33.33333%;max-width:33.33333%}.col-5{flex:0 0 41.66667%;max-width:41.66667%}.col-6{flex:0 0 50%;max-width:50%}.col-7{flex:0 0 58.33333%;max-width:58.33333%}.col-8{flex:0 0 66.66667%;max-width:66.66667%}.col-9{flex:0 0 75%;max-width:75%}.col-10{flex:0 0 83.33333%;max-width:83.33333%}.col-11{flex:0 0 91.66667%;max-width:91.66667%}.col-12{flex:0 0 100%;max-width:100%}.order-first{order:-1}.order-last{order:13}.order-0{order:0}.order-1{order:1}.order-2{order:2}.order-3{order:3}.order-4{order:4}.order-5{order:5}.order-6{order:6}.order-7{order:7}.order-8{order:8}.order-9{order:9}.order-10{order:10}.order-11{order:11}.order-12{order:12}.offset-1{margin-left:8.33333%}.offset-2{margin-left:16.66667%}.offset-3{margin-left:25%}.offset-4{margin-left:33.33333%}.offset-5{margin-left:41.66667%}.offset-6{margin-left:50%}.offset-7{margin-left:58.33333%}.offset-8{margin-left:66.66667%}.offset-9{margin-left:75%}.offset-10{margin-left:83.33333%}.offset-11{margin-left:91.66667%}@media (min-width:576px){.col-sm{flex-basis:0;flex-grow:1;min-width:0;max-width:100%}.row-cols-sm-1>*{flex:0 0 100%;max-width:100%}.row-cols-sm-2>*{flex:0 0 50%;max-width:50%}.row-cols-sm-3>*{flex:0 0 33.33333%;max-width:33.33333%}.row-cols-sm-4>*{flex:0 0 25%;max-width:25%}.row-cols-sm-5>*{flex:0 0 20%;max-width:20%}.row-cols-sm-6>*{flex:0 0 16.66667%;max-width:16.66667%}.col-sm-auto{flex:0 0 auto;width:auto;max-width:100%}.col-sm-1{flex:0 0 8.33333%;max-width:8.33333%}.col-sm-2{flex:0 0 16.66667%;max-width:16.66667%}.col-sm-3{flex:0 0 25%;max-width:25%}.col-sm-4{flex:0 0 33.33333%;max-width:33.33333%}.col-sm-5{flex:0 0 41.66667%;max-width:41.66667%}.col-sm-6{flex:0 0 50%;max-width:50%}.col-sm-7{flex:0 0 58.33333%;max-width:58.33333%}.col-sm-8{flex:0 0 66.66667%;max-width:66.66667%}.col-sm-9{flex:0 0 75%;max-width:75%}.col-sm-10{flex:0 0 83.33333%;max-width:83.33333%}.col-sm-11{flex:0 0 91.66667%;max-width:91.66667%}.col-sm-12{flex:0 0 100%;max-width:100%}.order-sm-first{order:-1}.order-sm-last{order:13}.order-sm-0{order:0}.order-sm-1{order:1}.order-sm-2{order:2}.order-sm-3{order:3}.order-sm-4{order:4}.order-sm-5{order:5}.order-sm-6{order:6}.order-sm-7{order:7}.order-sm-8{order:8}.order-sm-9{order:9}.order-sm-10{order:10}.order-sm-11{order:11}.order-sm-12{order:12}.offset-sm-0{margin-left:0}.offset-sm-1{margin-left:8.33333%}.offset-sm-2{margin-left:16.66667%}.offset-sm-3{margin-left:25%}.offset-sm-4{margin-left:33.33333%}.offset-sm-5{margin-left:41.66667%}.offset-sm-6{margin-left:50%}.offset-sm-7{margin-left:58.33333%}.offset-sm-8{margin-left:66.66667%}.offset-sm-9{margin-left:75%}.offset-sm-10{margin-left:83.33333%}.offset-sm-11{margin-left:91.66667%}}@media (min-width:768px){.col-md{flex-basis:0;flex-grow:1;min-width:0;max-width:100%}.row-cols-md-1>*{flex:0 0 100%;max-width:100%}.row-cols-md-2>*{flex:0 0 50%;max-width:50%}.row-cols-md-3>*{flex:0 0 33.33333%;max-width:33.33333%}.row-cols-md-4>*{flex:0 0 25%;max-width:25%}.row-cols-md-5>*{flex:0 0 20%;max-width:20%}.row-cols-md-6>*{flex:0 0 16.66667%;max-width:16.66667%}.col-md-auto{flex:0 0 auto;width:auto;max-width:100%}.col-md-1{flex:0 0 8.33333%;max-width:8.33333%}.col-md-2{flex:0 0 16.66667%;max-width:16.66667%}.col-md-3{flex:0 0 25%;max-width:25%}.col-md-4{flex:0 0 33.33333%;max-width:33.33333%}.col-md-5{flex:0 0 41.66667%;max-width:41.66667%}.col-md-6{flex:0 0 50%;max-width:50%}.col-md-7{flex:0 0 58.33333%;max-width:58.33333%}.col-md-8{flex:0 0 66.66667%;max-width:66.66667%}.col-md-9{flex:0 0 75%;max-width:75%}.col-md-10{flex:0 0 83.33333%;max-width:83.33333%}.col-md-11{flex:0 0 91.66667%;max-width:91.66667%}.col-md-12{flex:0 0 100%;max-width:100%}.order-md-first{order:-1}.order-md-last{order:13}.order-md-0{order:0}.order-md-1{order:1}.order-md-2{order:2}.order-md-3{order:3}.order-md-4{order:4}.order-md-5{order:5}.order-md-6{order:6}.order-md-7{order:7}.order-md-8{order:8}.order-md-9{order:9}.order-md-10{order:10}.order-md-11{order:11}.order-md-12{order:12}.offset-md-0{margin-left:0}.offset-md-1{margin-left:8.33333%}.offset-md-2{margin-left:16.66667%}.offset-md-3{margin-left:25%}.offset-md-4{margin-left:33.33333%}.offset-md-5{margin-left:41.66667%}.offset-md-6{margin-left:50%}.offset-md-7{margin-left:58.33333%}.offset-md-8{margin-left:66.66667%}.offset-md-9{margin-left:75%}.offset-md-10{margin-left:83.33333%}.offset-md-11{margin-left:91.66667%}}@media (min-width:992px){.col-lg{flex-basis:0;flex-grow:1;min-width:0;max-width:100%}.row-cols-lg-1>*{flex:0 0 100%;max-width:100%}.row-cols-lg-2>*{flex:0 0 50%;max-width:50%}.row-cols-lg-3>*{flex:0 0 33.33333%;max-width:33.33333%}.row-cols-lg-4>*{flex:0 0 25%;max-width:25%}.row-cols-lg-5>*{flex:0 0 20%;max-width:20%}.row-cols-lg-6>*{flex:0 0 16.66667%;max-width:16.66667%}.col-lg-auto{flex:0 0 auto;width:auto;max-width:100%}.col-lg-1{flex:0 0 8.33333%;max-width:8.33333%}.col-lg-2{flex:0 0 16.66667%;max-width:16.66667%}.col-lg-3{flex:0 0 25%;max-width:25%}.col-lg-4{flex:0 0 33.33333%;max-width:33.33333%}.col-lg-5{flex:0 0 41.66667%;max-width:41.66667%}.col-lg-6{flex:0 0 50%;max-width:50%}.col-lg-7{flex:0 0 58.33333%;max-width:58.33333%}.col-lg-8{flex:0 0 66.66667%;max-width:66.66667%}.col-lg-9{flex:0 0 75%;max-width:75%}.col-lg-10{flex:0 0 83.33333%;max-width:83.33333%}.col-lg-11{flex:0 0 91.66667%;max-width:91.66667%}.col-lg-12{flex:0 0 100%;max-width:100%}.order-lg-first{order:-1}.order-lg-last{order:13}.order-lg-0{order:0}.order-lg-1{order:1}.order-lg-2{order:2}.order-lg-3{order:3}.order-lg-4{order:4}.order-lg-5{order:5}.order-lg-6{order:6}.order-lg-7{order:7}.order-lg-8{order:8}.order-lg-9{order:9}.order-lg-10{order:10}.order-lg-11{order:11}.order-lg-12{order:12}.offset-lg-0{margin-left:0}.offset-lg-1{margin-left:8.33333%}.offset-lg-2{margin-left:16.66667%}.offset-lg-3{margin-left:25%}.offset-lg-4{margin-left:33.33333%}.offset-lg-5{margin-left:41.66667%}.offset-lg-6{margin-left:50%}.offset-lg-7{margin-left:58.33333%}.offset-lg-8{margin-left:66.66667%}.offset-lg-9{margin-left:75%}.offset-lg-10{margin-left:83.33333%}.offset-lg-11{margin-left:91.66667%}}@media (min-width:1200px){.col-xl{flex-basis:0;flex-grow:1;min-width:0;max-width:100%}.row-cols-xl-1>*{flex:0 0 100%;max-width:100%}.row-cols-xl-2>*{flex:0 0 50%;max-width:50%}.row-cols-xl-3>*{flex:0 0 33.33333%;max-width:33.33333%}.row-cols-xl-4>*{flex:0 0 25%;max-width:25%}.row-cols-xl-5>*{flex:0 0 20%;max-width:20%}.row-cols-xl-6>*{flex:0 0 16.66667%;max-width:16.66667%}.col-xl-auto{flex:0 0 auto;width:auto;max-width:100%}.col-xl-1{flex:0 0 8.33333%;max-width:8.33333%}.col-xl-2{flex:0 0 16.66667%;max-width:16.66667%}.col-xl-3{flex:0 0 25%;max-width:25%}.col-xl-4{flex:0 0 33.33333%;max-width:33.33333%}.col-xl-5{flex:0 0 41.66667%;max-width:41.66667%}.col-xl-6{flex:0 0 50%;max-width:50%}.col-xl-7{flex:0 0 58.33333%;max-width:58.33333%}.col-xl-8{flex:0 0 66.66667%;max-width:66.66667%}.col-xl-9{flex:0 0 75%;max-width:75%}.col-xl-10{flex:0 0 83.33333%;max-width:83.33333%}.col-xl-11{flex:0 0 91.66667%;max-width:91.66667%}.col-xl-12{flex:0 0 100%;max-width:100%}.order-xl-first{order:-1}.order-xl-last{order:13}.order-xl-0{order:0}.order-xl-1{order:1}.order-xl-2{order:2}.order-xl-3{order:3}.order-xl-4{order:4}.order-xl-5{order:5}.order-xl-6{order:6}.order-xl-7{order:7}.order-xl-8{order:8}.order-xl-9{order:9}.order-xl-10{order:10}.order-xl-11{order:11}.order-xl-12{order:12}.offset-xl-0{margin-left:0}.offset-xl-1{margin-left:8.33333%}.offset-xl-2{margin-left:16.66667%}.offset-xl-3{margin-left:25%}.offset-xl-4{margin-left:33.33333%}.offset-xl-5{margin-left:41.66667%}.offset-xl-6{margin-left:50%}.offset-xl-7{margin-left:58.33333%}.offset-xl-8{margin-left:66.66667%}.offset-xl-9{margin-left:75%}.offset-xl-10{margin-left:83.33333%}.offset-xl-11{margin-left:91.66667%}}.table{width:100%;margin-bottom:1rem;color:#dee2e6}.table td,.table th{padding:.75rem;vertical-align:top;border-top:1px solid #444}.table thead th{vertical-align:bottom;border-bottom:2px solid #444}.table tbody+tbody{border-top:2px solid #444}.table-sm td,.table-sm th{padding:.3rem}.table-bordered{border:1px solid #444}.table-bordered td,.table-bordered th{border:1px solid #444}.table-bordered thead td,.table-bordered thead th{border-bottom-width:2px}.table-borderless tbody+tbody,.table-borderless td,.table-borderless th,.table-borderless thead th{border:0}.table-striped tbody tr:nth-of-type(odd){background-color:#303030}.table-hover tbody tr:hover{color:#dee2e6;background-color:rgba(0,0,0,.075)}.table-primary,.table-primary>td,.table-primary>th{background-color:#c7d1db}.table-primary tbody+tbody,.table-primary td,.table-primary th,.table-primary thead th{border-color:#97a9bc}.table-hover .table-primary:hover{background-color:#b7c4d1}.table-hover .table-primary:hover>td,.table-hover .table-primary:hover>th{background-color:#b7c4d1}.table-secondary,.table-secondary>td,.table-secondary>th{background-color:#cbcbcb}.table-secondary tbody+tbody,.table-secondary td,.table-secondary th,.table-secondary thead th{border-color:#9e9e9e}.table-hover .table-secondary:hover{background-color:#bebebe}.table-hover .table-secondary:hover>td,.table-hover .table-secondary:hover>th{background-color:#bebebe}.table-success,.table-success>td,.table-success>th{background-color:#b8ecdf}.table-success tbody+tbody,.table-success td,.table-success th,.table-success thead th{border-color:#7adcc3}.table-hover .table-success:hover{background-color:#a4e7d6}.table-hover .table-success:hover>td,.table-hover .table-success:hover>th{background-color:#a4e7d6}.table-info,.table-info>td,.table-info>th{background-color:#c6e2f5}.table-info tbody+tbody,.table-info td,.table-info th,.table-info thead th{border-color:#95c9ec}.table-hover .table-info:hover{background-color:#b0d7f1}.table-hover .table-info:hover>td,.table-hover .table-info:hover>th{background-color:#b0d7f1}.table-warning,.table-warning>td,.table-warning>th{background-color:#fce3bd}.table-warning tbody+tbody,.table-warning td,.table-warning th,.table-warning thead th{border-color:#f9cc84}.table-hover .table-warning:hover{background-color:#fbd9a5}.table-hover .table-warning:hover>td,.table-hover .table-warning:hover>th{background-color:#fbd9a5}.table-danger,.table-danger>td,.table-danger>th{background-color:#f8cdc8}.table-danger tbody+tbody,.table-danger td,.table-danger th,.table-danger thead th{border-color:#f3a29a}.table-hover .table-danger:hover{background-color:#f5b8b1}.table-hover .table-danger:hover>td,.table-hover .table-danger:hover>th{background-color:#f5b8b1}.table-light,.table-light>td,.table-light>th{background-color:#c5c5c5}.table-light tbody+tbody,.table-light td,.table-light th,.table-light thead th{border-color:#939393}.table-hover .table-light:hover{background-color:#b8b8b8}.table-hover .table-light:hover>td,.table-hover .table-light:hover>th{background-color:#b8b8b8}.table-dark,.table-dark>td,.table-dark>th{background-color:#f6f7f8}.table-dark tbody+tbody,.table-dark td,.table-dark th,.table-dark thead th{border-color:#eef0f2}.table-hover .table-dark:hover{background-color:#e8eaed}.table-hover .table-dark:hover>td,.table-hover .table-dark:hover>th{background-color:#e8eaed}.table-active,.table-active>td,.table-active>th{background-color:rgba(0,0,0,.075)}.table-hover .table-active:hover{background-color:rgba(0,0,0,.075)}.table-hover .table-active:hover>td,.table-hover .table-active:hover>th{background-color:rgba(0,0,0,.075)}.table .thead-dark th{color:#fff;background-color:#303030;border-color:#434343}.table .thead-light th{color:#444;background-color:#ebebeb;border-color:#444}.table-dark{color:#fff;background-color:#303030}.table-dark td,.table-dark th,.table-dark thead th{border-color:#434343}.table-dark.table-bordered{border:0}.table-dark.table-striped tbody tr:nth-of-type(odd){background-color:rgba(255,255,255,.05)}.table-dark.table-hover tbody tr:hover{color:#fff;background-color:rgba(255,255,255,.075)}@media (max-width:575.98px){.table-responsive-sm{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch}.table-responsive-sm>.table-bordered{border:0}}@media (max-width:767.98px){.table-responsive-md{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch}.table-responsive-md>.table-bordered{border:0}}@media (max-width:991.98px){.table-responsive-lg{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch}.table-responsive-lg>.table-bordered{border:0}}@media (max-width:1199.98px){.table-responsive-xl{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch}.table-responsive-xl>.table-bordered{border:0}}.table-responsive{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch}.table-responsive>.table-bordered{border:0}.form-control{display:block;width:100%;height:calc(1.5em + .75rem + 2px);padding:.375rem .75rem;font-size:.9375rem;font-weight:400;line-height:1.5;color:#fff;background-color:#444;background-clip:padding-box;border:1px solid #222;border-radius:.25rem;transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-control{transition:none}}.form-control::-ms-expand{background-color:transparent;border:0}.form-control:-moz-focusring{color:transparent;text-shadow:0 0 0 #fff}.form-control:focus{color:#fff;background-color:#444;border-color:#739ac2;outline:0;box-shadow:0 0 0 .2rem rgba(55,90,127,.25)}.form-control::placeholder{color:#888;opacity:1}.form-control:disabled,.form-control[readonly]{background-color:#2b2b2b;opacity:1}input[type=date].form-control,input[type=datetime-local].form-control,input[type=month].form-control,input[type=time].form-control{appearance:none}select.form-control:focus::-ms-value{color:#fff;background-color:#444}.form-control-file,.form-control-range{display:block;width:100%}.col-form-label{padding-top:calc(.375rem + 1px);padding-bottom:calc(.375rem + 1px);margin-bottom:0;font-size:inherit;line-height:1.5}.col-form-label-lg{padding-top:calc(.5rem + 1px);padding-bottom:calc(.5rem + 1px);font-size:1.17188rem;line-height:1.5}.col-form-label-sm{padding-top:calc(.25rem + 1px);padding-bottom:calc(.25rem + 1px);font-size:.82031rem;line-height:1.5}.form-control-plaintext{display:block;width:100%;padding:.375rem 0;margin-bottom:0;font-size:.9375rem;line-height:1.5;color:#dee2e6;background-color:transparent;border:solid transparent;border-width:1px 0}.form-control-plaintext.form-control-lg,.form-control-plaintext.form-control-sm{padding-right:0;padding-left:0}.form-control-sm{height:calc(1.5em + .5rem + 2px);padding:.25rem .5rem;font-size:.82031rem;line-height:1.5;border-radius:.2rem}.form-control-lg{height:calc(1.5em + 1rem + 2px);padding:.5rem 1rem;font-size:1.17188rem;line-height:1.5;border-radius:.3rem}select.form-control[multiple],select.form-control[size]{height:auto}textarea.form-control{height:auto}.form-group{margin-bottom:1rem}.form-text{display:block;margin-top:.25rem}.form-row{display:flex;flex-wrap:wrap;margin-right:-5px;margin-left:-5px}.form-row>.col,.form-row>[class*=col-]{padding-right:5px;padding-left:5px}.form-check{position:relative;display:block;padding-left:1.25rem}.form-check-input{position:absolute;margin-top:.3rem;margin-left:-1.25rem}.form-check-input:disabled~.form-check-label,.form-check-input[disabled]~.form-check-label{color:#888}.form-check-label{margin-bottom:0}.form-check-inline{display:inline-flex;align-items:center;padding-left:0;margin-right:.75rem}.form-check-inline .form-check-input{position:static;margin-top:0;margin-right:.3125rem;margin-left:0}.valid-feedback{display:none;width:100%;margin-top:.25rem;font-size:80%;color:#00bc8c}.valid-tooltip{position:absolute;top:100%;z-index:5;display:none;max-width:100%;padding:.25rem .5rem;margin-top:.1rem;font-size:.82031rem;line-height:1.5;color:#fff;background-color:rgba(0,188,140,.9);border-radius:.25rem}.is-valid~.valid-feedback,.is-valid~.valid-tooltip,.was-validated :valid~.valid-feedback,.was-validated :valid~.valid-tooltip{display:block}.form-control.is-valid,.was-validated .form-control:valid{border-color:#00bc8c;padding-right:calc(1.5em + .75rem);background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='8' height='8' viewBox='0 0 8 8'%3e%3cpath fill='%2300bc8c' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right calc(.375em + .1875rem) center;background-size:calc(.75em + .375rem) calc(.75em + .375rem)}.form-control.is-valid:focus,.was-validated .form-control:valid:focus{border-color:#00bc8c;box-shadow:0 0 0 .2rem rgba(0,188,140,.25)}.was-validated textarea.form-control:valid,textarea.form-control.is-valid{padding-right:calc(1.5em + .75rem);background-position:top calc(.375em + .1875rem) right calc(.375em + .1875rem)}.custom-select.is-valid,.was-validated .custom-select:valid{border-color:#00bc8c;padding-right:calc(.75em + 2.3125rem);background:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='4' height='5' viewBox='0 0 4 5'%3e%3cpath fill='%23303030' d='M2 0L0 2h4zm0 5L0 3h4z'/%3e%3c/svg%3e") no-repeat right .75rem center/8px 10px,url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='8' height='8' viewBox='0 0 8 8'%3e%3cpath fill='%2300bc8c' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e") #444 no-repeat center right 1.75rem/calc(.75em + .375rem) calc(.75em + .375rem)}.custom-select.is-valid:focus,.was-validated .custom-select:valid:focus{border-color:#00bc8c;box-shadow:0 0 0 .2rem rgba(0,188,140,.25)}.form-check-input.is-valid~.form-check-label,.was-validated .form-check-input:valid~.form-check-label{color:#00bc8c}.form-check-input.is-valid~.valid-feedback,.form-check-input.is-valid~.valid-tooltip,.was-validated .form-check-input:valid~.valid-feedback,.was-validated .form-check-input:valid~.valid-tooltip{display:block}.custom-control-input.is-valid~.custom-control-label,.was-validated .custom-control-input:valid~.custom-control-label{color:#00bc8c}.custom-control-input.is-valid~.custom-control-label::before,.was-validated .custom-control-input:valid~.custom-control-label::before{border-color:#00bc8c}.custom-control-input.is-valid:checked~.custom-control-label::before,.was-validated .custom-control-input:valid:checked~.custom-control-label::before{border-color:#00efb2;background-color:#00efb2}.custom-control-input.is-valid:focus~.custom-control-label::before,.was-validated .custom-control-input:valid:focus~.custom-control-label::before{box-shadow:0 0 0 .2rem rgba(0,188,140,.25)}.custom-control-input.is-valid:focus:not(:checked)~.custom-control-label::before,.was-validated .custom-control-input:valid:focus:not(:checked)~.custom-control-label::before{border-color:#00bc8c}.custom-file-input.is-valid~.custom-file-label,.was-validated .custom-file-input:valid~.custom-file-label{border-color:#00bc8c}.custom-file-input.is-valid:focus~.custom-file-label,.was-validated .custom-file-input:valid:focus~.custom-file-label{border-color:#00bc8c;box-shadow:0 0 0 .2rem rgba(0,188,140,.25)}.invalid-feedback{display:none;width:100%;margin-top:.25rem;font-size:80%;color:#e74c3c}.invalid-tooltip{position:absolute;top:100%;z-index:5;display:none;max-width:100%;padding:.25rem .5rem;margin-top:.1rem;font-size:.82031rem;line-height:1.5;color:#fff;background-color:rgba(231,76,60,.9);border-radius:.25rem}.is-invalid~.invalid-feedback,.is-invalid~.invalid-tooltip,.was-validated :invalid~.invalid-feedback,.was-validated :invalid~.invalid-tooltip{display:block}.form-control.is-invalid,.was-validated .form-control:invalid{border-color:#e74c3c;padding-right:calc(1.5em + .75rem);background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='none' stroke='%23e74c3c' viewBox='0 0 12 12'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23e74c3c' stroke='none'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right calc(.375em + .1875rem) center;background-size:calc(.75em + .375rem) calc(.75em + .375rem)}.form-control.is-invalid:focus,.was-validated .form-control:invalid:focus{border-color:#e74c3c;box-shadow:0 0 0 .2rem rgba(231,76,60,.25)}.was-validated textarea.form-control:invalid,textarea.form-control.is-invalid{padding-right:calc(1.5em + .75rem);background-position:top calc(.375em + .1875rem) right calc(.375em + .1875rem)}.custom-select.is-invalid,.was-validated .custom-select:invalid{border-color:#e74c3c;padding-right:calc(.75em + 2.3125rem);background:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='4' height='5' viewBox='0 0 4 5'%3e%3cpath fill='%23303030' d='M2 0L0 2h4zm0 5L0 3h4z'/%3e%3c/svg%3e") no-repeat right .75rem center/8px 10px,url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='none' stroke='%23e74c3c' viewBox='0 0 12 12'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23e74c3c' stroke='none'/%3e%3c/svg%3e") #444 no-repeat center right 1.75rem/calc(.75em + .375rem) calc(.75em + .375rem)}.custom-select.is-invalid:focus,.was-validated .custom-select:invalid:focus{border-color:#e74c3c;box-shadow:0 0 0 .2rem rgba(231,76,60,.25)}.form-check-input.is-invalid~.form-check-label,.was-validated .form-check-input:invalid~.form-check-label{color:#e74c3c}.form-check-input.is-invalid~.invalid-feedback,.form-check-input.is-invalid~.invalid-tooltip,.was-validated .form-check-input:invalid~.invalid-feedback,.was-validated .form-check-input:invalid~.invalid-tooltip{display:block}.custom-control-input.is-invalid~.custom-control-label,.was-validated .custom-control-input:invalid~.custom-control-label{color:#e74c3c}.custom-control-input.is-invalid~.custom-control-label::before,.was-validated .custom-control-input:invalid~.custom-control-label::before{border-color:#e74c3c}.custom-control-input.is-invalid:checked~.custom-control-label::before,.was-validated .custom-control-input:invalid:checked~.custom-control-label::before{border-color:#ed7669;background-color:#ed7669}.custom-control-input.is-invalid:focus~.custom-control-label::before,.was-validated .custom-control-input:invalid:focus~.custom-control-label::before{box-shadow:0 0 0 .2rem rgba(231,76,60,.25)}.custom-control-input.is-invalid:focus:not(:checked)~.custom-control-label::before,.was-validated .custom-control-input:invalid:focus:not(:checked)~.custom-control-label::before{border-color:#e74c3c}.custom-file-input.is-invalid~.custom-file-label,.was-validated .custom-file-input:invalid~.custom-file-label{border-color:#e74c3c}.custom-file-input.is-invalid:focus~.custom-file-label,.was-validated .custom-file-input:invalid:focus~.custom-file-label{border-color:#e74c3c;box-shadow:0 0 0 .2rem rgba(231,76,60,.25)}.form-inline{display:flex;flex-flow:row wrap;align-items:center}.form-inline .form-check{width:100%}@media (min-width:576px){.form-inline label{display:flex;align-items:center;justify-content:center;margin-bottom:0}.form-inline .form-group{display:flex;flex:0 0 auto;flex-flow:row wrap;align-items:center;margin-bottom:0}.form-inline .form-control{display:inline-block;width:auto;vertical-align:middle}.form-inline .form-control-plaintext{display:inline-block}.form-inline .custom-select,.form-inline .input-group{width:auto}.form-inline .form-check{display:flex;align-items:center;justify-content:center;width:auto;padding-left:0}.form-inline .form-check-input{position:relative;flex-shrink:0;margin-top:0;margin-right:.25rem;margin-left:0}.form-inline .custom-control{align-items:center;justify-content:center}.form-inline .custom-control-label{margin-bottom:0}}.btn{display:inline-block;font-weight:400;color:#dee2e6;text-align:center;vertical-align:middle;user-select:none;background-color:transparent;border:1px solid transparent;padding:.375rem .75rem;font-size:.9375rem;line-height:1.5;border-radius:.25rem;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.btn{transition:none}}.btn:hover{color:#dee2e6;text-decoration:none}.btn.focus,.btn:focus{outline:0;box-shadow:0 0 0 .2rem rgba(55,90,127,.25)}.btn.disabled,.btn:disabled{opacity:.65}.btn:not(:disabled):not(.disabled){cursor:pointer}a.btn.disabled,fieldset:disabled a.btn{pointer-events:none}.btn-primary{color:#fff;background-color:#375a7f;border-color:#375a7f}.btn-primary:hover{color:#fff;background-color:#2b4764;border-color:#28415b}.btn-primary.focus,.btn-primary:focus{color:#fff;background-color:#2b4764;border-color:#28415b;box-shadow:0 0 0 .2rem rgba(85,115,146,.5)}.btn-primary.disabled,.btn-primary:disabled{color:#fff;background-color:#375a7f;border-color:#375a7f}.btn-primary:not(:disabled):not(.disabled).active,.btn-primary:not(:disabled):not(.disabled):active,.show>.btn-primary.dropdown-toggle{color:#fff;background-color:#28415b;border-color:#243a53}.btn-primary:not(:disabled):not(.disabled).active:focus,.btn-primary:not(:disabled):not(.disabled):active:focus,.show>.btn-primary.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(85,115,146,.5)}.btn-secondary{color:#fff;background-color:#444;border-color:#444}.btn-secondary:hover{color:#fff;background-color:#313131;border-color:#2b2b2b}.btn-secondary.focus,.btn-secondary:focus{color:#fff;background-color:#313131;border-color:#2b2b2b;box-shadow:0 0 0 .2rem rgba(96,96,96,.5)}.btn-secondary.disabled,.btn-secondary:disabled{color:#fff;background-color:#444;border-color:#444}.btn-secondary:not(:disabled):not(.disabled).active,.btn-secondary:not(:disabled):not(.disabled):active,.show>.btn-secondary.dropdown-toggle{color:#fff;background-color:#2b2b2b;border-color:#242424}.btn-secondary:not(:disabled):not(.disabled).active:focus,.btn-secondary:not(:disabled):not(.disabled):active:focus,.show>.btn-secondary.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(96,96,96,.5)}.btn-success{color:#fff;background-color:#00bc8c;border-color:#00bc8c}.btn-success:hover{color:#fff;background-color:#009670;border-color:#008966}.btn-success.focus,.btn-success:focus{color:#fff;background-color:#009670;border-color:#008966;box-shadow:0 0 0 .2rem rgba(38,198,157,.5)}.btn-success.disabled,.btn-success:disabled{color:#fff;background-color:#00bc8c;border-color:#00bc8c}.btn-success:not(:disabled):not(.disabled).active,.btn-success:not(:disabled):not(.disabled):active,.show>.btn-success.dropdown-toggle{color:#fff;background-color:#008966;border-color:#007c5d}.btn-success:not(:disabled):not(.disabled).active:focus,.btn-success:not(:disabled):not(.disabled):active:focus,.show>.btn-success.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(38,198,157,.5)}.btn-info{color:#fff;background-color:#3498db;border-color:#3498db}.btn-info:hover{color:#fff;background-color:#2384c6;border-color:#217dbb}.btn-info.focus,.btn-info:focus{color:#fff;background-color:#2384c6;border-color:#217dbb;box-shadow:0 0 0 .2rem rgba(82,167,224,.5)}.btn-info.disabled,.btn-info:disabled{color:#fff;background-color:#3498db;border-color:#3498db}.btn-info:not(:disabled):not(.disabled).active,.btn-info:not(:disabled):not(.disabled):active,.show>.btn-info.dropdown-toggle{color:#fff;background-color:#217dbb;border-color:#1f76b0}.btn-info:not(:disabled):not(.disabled).active:focus,.btn-info:not(:disabled):not(.disabled):active:focus,.show>.btn-info.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(82,167,224,.5)}.btn-warning{color:#fff;background-color:#f39c12;border-color:#f39c12}.btn-warning:hover{color:#fff;background-color:#d4860b;border-color:#c87f0a}.btn-warning.focus,.btn-warning:focus{color:#fff;background-color:#d4860b;border-color:#c87f0a;box-shadow:0 0 0 .2rem rgba(245,171,54,.5)}.btn-warning.disabled,.btn-warning:disabled{color:#fff;background-color:#f39c12;border-color:#f39c12}.btn-warning:not(:disabled):not(.disabled).active,.btn-warning:not(:disabled):not(.disabled):active,.show>.btn-warning.dropdown-toggle{color:#fff;background-color:#c87f0a;border-color:#bc770a}.btn-warning:not(:disabled):not(.disabled).active:focus,.btn-warning:not(:disabled):not(.disabled):active:focus,.show>.btn-warning.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(245,171,54,.5)}.btn-danger{color:#fff;background-color:#e74c3c;border-color:#e74c3c}.btn-danger:hover{color:#fff;background-color:#e12e1c;border-color:#d62c1a}.btn-danger.focus,.btn-danger:focus{color:#fff;background-color:#e12e1c;border-color:#d62c1a;box-shadow:0 0 0 .2rem rgba(235,103,89,.5)}.btn-danger.disabled,.btn-danger:disabled{color:#fff;background-color:#e74c3c;border-color:#e74c3c}.btn-danger:not(:disabled):not(.disabled).active,.btn-danger:not(:disabled):not(.disabled):active,.show>.btn-danger.dropdown-toggle{color:#fff;background-color:#d62c1a;border-color:#ca2a19}.btn-danger:not(:disabled):not(.disabled).active:focus,.btn-danger:not(:disabled):not(.disabled):active:focus,.show>.btn-danger.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(235,103,89,.5)}.btn-light{color:#fff;background-color:#303030;border-color:#303030}.btn-light:hover{color:#fff;background-color:#1d1d1d;border-color:#171717}.btn-light.focus,.btn-light:focus{color:#fff;background-color:#1d1d1d;border-color:#171717;box-shadow:0 0 0 .2rem rgba(79,79,79,.5)}.btn-light.disabled,.btn-light:disabled{color:#fff;background-color:#303030;border-color:#303030}.btn-light:not(:disabled):not(.disabled).active,.btn-light:not(:disabled):not(.disabled):active,.show>.btn-light.dropdown-toggle{color:#fff;background-color:#171717;border-color:#101010}.btn-light:not(:disabled):not(.disabled).active:focus,.btn-light:not(:disabled):not(.disabled):active:focus,.show>.btn-light.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(79,79,79,.5)}.btn-dark{color:#222;background-color:#dee2e6;border-color:#dee2e6}.btn-dark:hover{color:#222;background-color:#c8cfd6;border-color:#c1c9d0}.btn-dark.focus,.btn-dark:focus{color:#222;background-color:#c8cfd6;border-color:#c1c9d0;box-shadow:0 0 0 .2rem rgba(194,197,201,.5)}.btn-dark.disabled,.btn-dark:disabled{color:#222;background-color:#dee2e6;border-color:#dee2e6}.btn-dark:not(:disabled):not(.disabled).active,.btn-dark:not(:disabled):not(.disabled):active,.show>.btn-dark.dropdown-toggle{color:#222;background-color:#c1c9d0;border-color:#bac2cb}.btn-dark:not(:disabled):not(.disabled).active:focus,.btn-dark:not(:disabled):not(.disabled):active:focus,.show>.btn-dark.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(194,197,201,.5)}.btn-outline-primary{color:#375a7f;border-color:#375a7f}.btn-outline-primary:hover{color:#fff;background-color:#375a7f;border-color:#375a7f}.btn-outline-primary.focus,.btn-outline-primary:focus{box-shadow:0 0 0 .2rem rgba(55,90,127,.5)}.btn-outline-primary.disabled,.btn-outline-primary:disabled{color:#375a7f;background-color:transparent}.btn-outline-primary:not(:disabled):not(.disabled).active,.btn-outline-primary:not(:disabled):not(.disabled):active,.show>.btn-outline-primary.dropdown-toggle{color:#fff;background-color:#375a7f;border-color:#375a7f}.btn-outline-primary:not(:disabled):not(.disabled).active:focus,.btn-outline-primary:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-primary.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(55,90,127,.5)}.btn-outline-secondary{color:#444;border-color:#444}.btn-outline-secondary:hover{color:#fff;background-color:#444;border-color:#444}.btn-outline-secondary.focus,.btn-outline-secondary:focus{box-shadow:0 0 0 .2rem rgba(68,68,68,.5)}.btn-outline-secondary.disabled,.btn-outline-secondary:disabled{color:#444;background-color:transparent}.btn-outline-secondary:not(:disabled):not(.disabled).active,.btn-outline-secondary:not(:disabled):not(.disabled):active,.show>.btn-outline-secondary.dropdown-toggle{color:#fff;background-color:#444;border-color:#444}.btn-outline-secondary:not(:disabled):not(.disabled).active:focus,.btn-outline-secondary:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-secondary.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(68,68,68,.5)}.btn-outline-success{color:#00bc8c;border-color:#00bc8c}.btn-outline-success:hover{color:#fff;background-color:#00bc8c;border-color:#00bc8c}.btn-outline-success.focus,.btn-outline-success:focus{box-shadow:0 0 0 .2rem rgba(0,188,140,.5)}.btn-outline-success.disabled,.btn-outline-success:disabled{color:#00bc8c;background-color:transparent}.btn-outline-success:not(:disabled):not(.disabled).active,.btn-outline-success:not(:disabled):not(.disabled):active,.show>.btn-outline-success.dropdown-toggle{color:#fff;background-color:#00bc8c;border-color:#00bc8c}.btn-outline-success:not(:disabled):not(.disabled).active:focus,.btn-outline-success:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-success.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(0,188,140,.5)}.btn-outline-info{color:#3498db;border-color:#3498db}.btn-outline-info:hover{color:#fff;background-color:#3498db;border-color:#3498db}.btn-outline-info.focus,.btn-outline-info:focus{box-shadow:0 0 0 .2rem rgba(52,152,219,.5)}.btn-outline-info.disabled,.btn-outline-info:disabled{color:#3498db;background-color:transparent}.btn-outline-info:not(:disabled):not(.disabled).active,.btn-outline-info:not(:disabled):not(.disabled):active,.show>.btn-outline-info.dropdown-toggle{color:#fff;background-color:#3498db;border-color:#3498db}.btn-outline-info:not(:disabled):not(.disabled).active:focus,.btn-outline-info:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-info.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(52,152,219,.5)}.btn-outline-warning{color:#f39c12;border-color:#f39c12}.btn-outline-warning:hover{color:#fff;background-color:#f39c12;border-color:#f39c12}.btn-outline-warning.focus,.btn-outline-warning:focus{box-shadow:0 0 0 .2rem rgba(243,156,18,.5)}.btn-outline-warning.disabled,.btn-outline-warning:disabled{color:#f39c12;background-color:transparent}.btn-outline-warning:not(:disabled):not(.disabled).active,.btn-outline-warning:not(:disabled):not(.disabled):active,.show>.btn-outline-warning.dropdown-toggle{color:#fff;background-color:#f39c12;border-color:#f39c12}.btn-outline-warning:not(:disabled):not(.disabled).active:focus,.btn-outline-warning:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-warning.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(243,156,18,.5)}.btn-outline-danger{color:#e74c3c;border-color:#e74c3c}.btn-outline-danger:hover{color:#fff;background-color:#e74c3c;border-color:#e74c3c}.btn-outline-danger.focus,.btn-outline-danger:focus{box-shadow:0 0 0 .2rem rgba(231,76,60,.5)}.btn-outline-danger.disabled,.btn-outline-danger:disabled{color:#e74c3c;background-color:transparent}.btn-outline-danger:not(:disabled):not(.disabled).active,.btn-outline-danger:not(:disabled):not(.disabled):active,.show>.btn-outline-danger.dropdown-toggle{color:#fff;background-color:#e74c3c;border-color:#e74c3c}.btn-outline-danger:not(:disabled):not(.disabled).active:focus,.btn-outline-danger:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-danger.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(231,76,60,.5)}.btn-outline-light{color:#303030;border-color:#303030}.btn-outline-light:hover{color:#fff;background-color:#303030;border-color:#303030}.btn-outline-light.focus,.btn-outline-light:focus{box-shadow:0 0 0 .2rem rgba(48,48,48,.5)}.btn-outline-light.disabled,.btn-outline-light:disabled{color:#303030;background-color:transparent}.btn-outline-light:not(:disabled):not(.disabled).active,.btn-outline-light:not(:disabled):not(.disabled):active,.show>.btn-outline-light.dropdown-toggle{color:#fff;background-color:#303030;border-color:#303030}.btn-outline-light:not(:disabled):not(.disabled).active:focus,.btn-outline-light:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-light.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(48,48,48,.5)}.btn-outline-dark{color:#dee2e6;border-color:#dee2e6}.btn-outline-dark:hover{color:#222;background-color:#dee2e6;border-color:#dee2e6}.btn-outline-dark.focus,.btn-outline-dark:focus{box-shadow:0 0 0 .2rem rgba(222,226,230,.5)}.btn-outline-dark.disabled,.btn-outline-dark:disabled{color:#dee2e6;background-color:transparent}.btn-outline-dark:not(:disabled):not(.disabled).active,.btn-outline-dark:not(:disabled):not(.disabled):active,.show>.btn-outline-dark.dropdown-toggle{color:#222;background-color:#dee2e6;border-color:#dee2e6}.btn-outline-dark:not(:disabled):not(.disabled).active:focus,.btn-outline-dark:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-dark.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(222,226,230,.5)}.btn-link{font-weight:400;color:#00bc8c;text-decoration:none}.btn-link:hover{color:#007053;text-decoration:underline}.btn-link.focus,.btn-link:focus{text-decoration:underline}.btn-link.disabled,.btn-link:disabled{color:#888;pointer-events:none}.btn-group-lg>.btn,.btn-lg{padding:.5rem 1rem;font-size:1.17188rem;line-height:1.5;border-radius:.3rem}.btn-group-sm>.btn,.btn-sm{padding:.25rem .5rem;font-size:.82031rem;line-height:1.5;border-radius:.2rem}.btn-block{display:block;width:100%}.btn-block+.btn-block{margin-top:.5rem}input[type=button].btn-block,input[type=reset].btn-block,input[type=submit].btn-block{width:100%}.fade{transition:opacity .15s linear}@media (prefers-reduced-motion:reduce){.fade{transition:none}}.fade:not(.show){opacity:0}.collapse:not(.show){display:none}.collapsing{position:relative;height:0;overflow:hidden;transition:height .35s ease}@media (prefers-reduced-motion:reduce){.collapsing{transition:none}}.dropdown,.dropleft,.dropright,.dropup{position:relative}.dropdown-toggle{white-space:nowrap}.dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:.3em solid;border-right:.3em solid transparent;border-bottom:0;border-left:.3em solid transparent}.dropdown-toggle:empty::after{margin-left:0}.dropdown-menu{position:absolute;top:100%;left:0;z-index:1000;display:none;float:left;min-width:10rem;padding:.5rem 0;margin:.125rem 0 0;font-size:.9375rem;color:#dee2e6;text-align:left;list-style:none;background-color:#222;background-clip:padding-box;border:1px solid #444;border-radius:.25rem}.dropdown-menu-left{right:auto;left:0}.dropdown-menu-right{right:0;left:auto}@media (min-width:576px){.dropdown-menu-sm-left{right:auto;left:0}.dropdown-menu-sm-right{right:0;left:auto}}@media (min-width:768px){.dropdown-menu-md-left{right:auto;left:0}.dropdown-menu-md-right{right:0;left:auto}}@media (min-width:992px){.dropdown-menu-lg-left{right:auto;left:0}.dropdown-menu-lg-right{right:0;left:auto}}@media (min-width:1200px){.dropdown-menu-xl-left{right:auto;left:0}.dropdown-menu-xl-right{right:0;left:auto}}.dropup .dropdown-menu{top:auto;bottom:100%;margin-top:0;margin-bottom:.125rem}.dropup .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:0;border-right:.3em solid transparent;border-bottom:.3em solid;border-left:.3em solid transparent}.dropup .dropdown-toggle:empty::after{margin-left:0}.dropright .dropdown-menu{top:0;right:auto;left:100%;margin-top:0;margin-left:.125rem}.dropright .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:.3em solid transparent;border-right:0;border-bottom:.3em solid transparent;border-left:.3em solid}.dropright .dropdown-toggle:empty::after{margin-left:0}.dropright .dropdown-toggle::after{vertical-align:0}.dropleft .dropdown-menu{top:0;right:100%;left:auto;margin-top:0;margin-right:.125rem}.dropleft .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:""}.dropleft .dropdown-toggle::after{display:none}.dropleft .dropdown-toggle::before{display:inline-block;margin-right:.255em;vertical-align:.255em;content:"";border-top:.3em solid transparent;border-right:.3em solid;border-bottom:.3em solid transparent}.dropleft .dropdown-toggle:empty::after{margin-left:0}.dropleft .dropdown-toggle::before{vertical-align:0}.dropdown-menu[x-placement^=bottom],.dropdown-menu[x-placement^=left],.dropdown-menu[x-placement^=right],.dropdown-menu[x-placement^=top]{right:auto;bottom:auto}.dropdown-divider{height:0;margin:.5rem 0;overflow:hidden;border-top:1px solid #444}.dropdown-item{display:block;width:100%;padding:.25rem 1.5rem;clear:both;font-weight:400;color:#fff;text-align:inherit;white-space:nowrap;background-color:transparent;border:0}.dropdown-item:focus,.dropdown-item:hover{color:#fff;text-decoration:none;background-color:#375a7f}.dropdown-item.active,.dropdown-item:active{color:#fff;text-decoration:none;background-color:#375a7f}.dropdown-item.disabled,.dropdown-item:disabled{color:#888;pointer-events:none;background-color:transparent}.dropdown-menu.show{display:block}.dropdown-header{display:block;padding:.5rem 1.5rem;margin-bottom:0;font-size:.82031rem;color:#888;white-space:nowrap}.dropdown-item-text{display:block;padding:.25rem 1.5rem;color:#fff}.btn-group,.btn-group-vertical{position:relative;display:inline-flex;vertical-align:middle}.btn-group-vertical>.btn,.btn-group>.btn{position:relative;flex:1 1 auto}.btn-group-vertical>.btn:hover,.btn-group>.btn:hover{z-index:1}.btn-group-vertical>.btn.active,.btn-group-vertical>.btn:active,.btn-group-vertical>.btn:focus,.btn-group>.btn.active,.btn-group>.btn:active,.btn-group>.btn:focus{z-index:1}.btn-toolbar{display:flex;flex-wrap:wrap;justify-content:flex-start}.btn-toolbar .input-group{width:auto}.btn-group>.btn-group:not(:first-child),.btn-group>.btn:not(:first-child){margin-left:-1px}.btn-group>.btn-group:not(:last-child)>.btn,.btn-group>.btn:not(:last-child):not(.dropdown-toggle){border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn-group:not(:first-child)>.btn,.btn-group>.btn:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.dropdown-toggle-split{padding-right:.5625rem;padding-left:.5625rem}.dropdown-toggle-split::after,.dropright .dropdown-toggle-split::after,.dropup .dropdown-toggle-split::after{margin-left:0}.dropleft .dropdown-toggle-split::before{margin-right:0}.btn-group-sm>.btn+.dropdown-toggle-split,.btn-sm+.dropdown-toggle-split{padding-right:.375rem;padding-left:.375rem}.btn-group-lg>.btn+.dropdown-toggle-split,.btn-lg+.dropdown-toggle-split{padding-right:.75rem;padding-left:.75rem}.btn-group-vertical{flex-direction:column;align-items:flex-start;justify-content:center}.btn-group-vertical>.btn,.btn-group-vertical>.btn-group{width:100%}.btn-group-vertical>.btn-group:not(:first-child),.btn-group-vertical>.btn:not(:first-child){margin-top:-1px}.btn-group-vertical>.btn-group:not(:last-child)>.btn,.btn-group-vertical>.btn:not(:last-child):not(.dropdown-toggle){border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn-group:not(:first-child)>.btn,.btn-group-vertical>.btn:not(:first-child){border-top-left-radius:0;border-top-right-radius:0}.btn-group-toggle>.btn,.btn-group-toggle>.btn-group>.btn{margin-bottom:0}.btn-group-toggle>.btn input[type=checkbox],.btn-group-toggle>.btn input[type=radio],.btn-group-toggle>.btn-group>.btn input[type=checkbox],.btn-group-toggle>.btn-group>.btn input[type=radio]{position:absolute;clip:rect(0,0,0,0);pointer-events:none}.input-group{position:relative;display:flex;flex-wrap:wrap;align-items:stretch;width:100%}.input-group>.custom-file,.input-group>.custom-select,.input-group>.form-control,.input-group>.form-control-plaintext{position:relative;flex:1 1 auto;width:1%;min-width:0;margin-bottom:0}.input-group>.custom-file+.custom-file,.input-group>.custom-file+.custom-select,.input-group>.custom-file+.form-control,.input-group>.custom-select+.custom-file,.input-group>.custom-select+.custom-select,.input-group>.custom-select+.form-control,.input-group>.form-control+.custom-file,.input-group>.form-control+.custom-select,.input-group>.form-control+.form-control,.input-group>.form-control-plaintext+.custom-file,.input-group>.form-control-plaintext+.custom-select,.input-group>.form-control-plaintext+.form-control{margin-left:-1px}.input-group>.custom-file .custom-file-input:focus~.custom-file-label,.input-group>.custom-select:focus,.input-group>.form-control:focus{z-index:3}.input-group>.custom-file .custom-file-input:focus{z-index:4}.input-group>.custom-select:not(:last-child),.input-group>.form-control:not(:last-child){border-top-right-radius:0;border-bottom-right-radius:0}.input-group>.custom-select:not(:first-child),.input-group>.form-control:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.input-group>.custom-file{display:flex;align-items:center}.input-group>.custom-file:not(:last-child) .custom-file-label,.input-group>.custom-file:not(:last-child) .custom-file-label::after{border-top-right-radius:0;border-bottom-right-radius:0}.input-group>.custom-file:not(:first-child) .custom-file-label{border-top-left-radius:0;border-bottom-left-radius:0}.input-group-append,.input-group-prepend{display:flex}.input-group-append .btn,.input-group-prepend .btn{position:relative;z-index:2}.input-group-append .btn:focus,.input-group-prepend .btn:focus{z-index:3}.input-group-append .btn+.btn,.input-group-append .btn+.input-group-text,.input-group-append .input-group-text+.btn,.input-group-append .input-group-text+.input-group-text,.input-group-prepend .btn+.btn,.input-group-prepend .btn+.input-group-text,.input-group-prepend .input-group-text+.btn,.input-group-prepend .input-group-text+.input-group-text{margin-left:-1px}.input-group-prepend{margin-right:-1px}.input-group-append{margin-left:-1px}.input-group-text{display:flex;align-items:center;padding:.375rem .75rem;margin-bottom:0;font-size:.9375rem;font-weight:400;line-height:1.5;color:#adb5bd;text-align:center;white-space:nowrap;background-color:#444;border:1px solid #222;border-radius:.25rem}.input-group-text input[type=checkbox],.input-group-text input[type=radio]{margin-top:0}.input-group-lg>.custom-select,.input-group-lg>.form-control:not(textarea){height:calc(1.5em + 1rem + 2px)}.input-group-lg>.custom-select,.input-group-lg>.form-control,.input-group-lg>.input-group-append>.btn,.input-group-lg>.input-group-append>.input-group-text,.input-group-lg>.input-group-prepend>.btn,.input-group-lg>.input-group-prepend>.input-group-text{padding:.5rem 1rem;font-size:1.17188rem;line-height:1.5;border-radius:.3rem}.input-group-sm>.custom-select,.input-group-sm>.form-control:not(textarea){height:calc(1.5em + .5rem + 2px)}.input-group-sm>.custom-select,.input-group-sm>.form-control,.input-group-sm>.input-group-append>.btn,.input-group-sm>.input-group-append>.input-group-text,.input-group-sm>.input-group-prepend>.btn,.input-group-sm>.input-group-prepend>.input-group-text{padding:.25rem .5rem;font-size:.82031rem;line-height:1.5;border-radius:.2rem}.input-group-lg>.custom-select,.input-group-sm>.custom-select{padding-right:1.75rem}.input-group>.input-group-append:last-child>.btn:not(:last-child):not(.dropdown-toggle),.input-group>.input-group-append:last-child>.input-group-text:not(:last-child),.input-group>.input-group-append:not(:last-child)>.btn,.input-group>.input-group-append:not(:last-child)>.input-group-text,.input-group>.input-group-prepend>.btn,.input-group>.input-group-prepend>.input-group-text{border-top-right-radius:0;border-bottom-right-radius:0}.input-group>.input-group-append>.btn,.input-group>.input-group-append>.input-group-text,.input-group>.input-group-prepend:first-child>.btn:not(:first-child),.input-group>.input-group-prepend:first-child>.input-group-text:not(:first-child),.input-group>.input-group-prepend:not(:first-child)>.btn,.input-group>.input-group-prepend:not(:first-child)>.input-group-text{border-top-left-radius:0;border-bottom-left-radius:0}.custom-control{position:relative;display:block;min-height:1.40625rem;padding-left:1.5rem}.custom-control-inline{display:inline-flex;margin-right:1rem}.custom-control-input{position:absolute;left:0;z-index:-1;width:1rem;height:1.20312rem;opacity:0}.custom-control-input:checked~.custom-control-label::before{color:#fff;border-color:#375a7f;background-color:#375a7f}.custom-control-input:focus~.custom-control-label::before{box-shadow:0 0 0 .2rem rgba(55,90,127,.25)}.custom-control-input:focus:not(:checked)~.custom-control-label::before{border-color:#739ac2}.custom-control-input:not(:disabled):active~.custom-control-label::before{color:#fff;background-color:#97b3d2;border-color:#97b3d2}.custom-control-input:disabled~.custom-control-label,.custom-control-input[disabled]~.custom-control-label{color:#888}.custom-control-input:disabled~.custom-control-label::before,.custom-control-input[disabled]~.custom-control-label::before{background-color:#2b2b2b}.custom-control-label{position:relative;margin-bottom:0;vertical-align:top}.custom-control-label::before{position:absolute;top:.20312rem;left:-1.5rem;display:block;width:1rem;height:1rem;pointer-events:none;content:"";background-color:#444;border:#adb5bd solid 1px}.custom-control-label::after{position:absolute;top:.20312rem;left:-1.5rem;display:block;width:1rem;height:1rem;content:"";background:no-repeat 50%/50% 50%}.custom-checkbox .custom-control-label::before{border-radius:.25rem}.custom-checkbox .custom-control-input:checked~.custom-control-label::after{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='8' height='8' viewBox='0 0 8 8'%3e%3cpath fill='%23fff' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26l2.974 2.99L8 2.193z'/%3e%3c/svg%3e")}.custom-checkbox .custom-control-input:indeterminate~.custom-control-label::before{border-color:#375a7f;background-color:#375a7f}.custom-checkbox .custom-control-input:indeterminate~.custom-control-label::after{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='4' height='4' viewBox='0 0 4 4'%3e%3cpath stroke='%23fff' d='M0 2h4'/%3e%3c/svg%3e")}.custom-checkbox .custom-control-input:disabled:checked~.custom-control-label::before{background-color:rgba(55,90,127,.5)}.custom-checkbox .custom-control-input:disabled:indeterminate~.custom-control-label::before{background-color:rgba(55,90,127,.5)}.custom-radio .custom-control-label::before{border-radius:50%}.custom-radio .custom-control-input:checked~.custom-control-label::after{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%23fff'/%3e%3c/svg%3e")}.custom-radio .custom-control-input:disabled:checked~.custom-control-label::before{background-color:rgba(55,90,127,.5)}.custom-switch{padding-left:2.25rem}.custom-switch .custom-control-label::before{left:-2.25rem;width:1.75rem;pointer-events:all;border-radius:.5rem}.custom-switch .custom-control-label::after{top:calc(.20312rem + 2px);left:calc(-2.25rem + 2px);width:calc(1rem - 4px);height:calc(1rem - 4px);background-color:#adb5bd;border-radius:.5rem;transition:transform .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.custom-switch .custom-control-label::after{transition:none}}.custom-switch .custom-control-input:checked~.custom-control-label::after{background-color:#444;transform:translateX(.75rem)}.custom-switch .custom-control-input:disabled:checked~.custom-control-label::before{background-color:rgba(55,90,127,.5)}.custom-select{display:inline-block;width:100%;height:calc(1.5em + .75rem + 2px);padding:.375rem 1.75rem .375rem .75rem;font-size:.9375rem;font-weight:400;line-height:1.5;color:#fff;vertical-align:middle;background:#444 url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='4' height='5' viewBox='0 0 4 5'%3e%3cpath fill='%23303030' d='M2 0L0 2h4zm0 5L0 3h4z'/%3e%3c/svg%3e") no-repeat right .75rem center/8px 10px;border:1px solid #222;border-radius:.25rem;appearance:none}.custom-select:focus{border-color:#739ac2;outline:0;box-shadow:0 0 0 .2rem rgba(55,90,127,.25)}.custom-select:focus::-ms-value{color:#fff;background-color:#444}.custom-select[multiple],.custom-select[size]:not([size="1"]){height:auto;padding-right:.75rem;background-image:none}.custom-select:disabled{color:#888;background-color:#ebebeb}.custom-select::-ms-expand{display:none}.custom-select:-moz-focusring{color:transparent;text-shadow:0 0 0 #fff}.custom-select-sm{height:calc(1.5em + .5rem + 2px);padding-top:.25rem;padding-bottom:.25rem;padding-left:.5rem;font-size:.82031rem}.custom-select-lg{height:calc(1.5em + 1rem + 2px);padding-top:.5rem;padding-bottom:.5rem;padding-left:1rem;font-size:1.17188rem}.custom-file{position:relative;display:inline-block;width:100%;height:calc(1.5em + .75rem + 2px);margin-bottom:0}.custom-file-input{position:relative;z-index:2;width:100%;height:calc(1.5em + .75rem + 2px);margin:0;opacity:0}.custom-file-input:focus~.custom-file-label{border-color:#739ac2;box-shadow:0 0 0 .2rem rgba(55,90,127,.25)}.custom-file-input:disabled~.custom-file-label,.custom-file-input[disabled]~.custom-file-label{background-color:#2b2b2b}.custom-file-input:lang(en)~.custom-file-label::after{content:"Browse"}.custom-file-input~.custom-file-label[data-browse]::after{content:attr(data-browse)}.custom-file-label{position:absolute;top:0;right:0;left:0;z-index:1;height:calc(1.5em + .75rem + 2px);padding:.375rem .75rem;font-weight:400;line-height:1.5;color:#adb5bd;background-color:#444;border:1px solid #222;border-radius:.25rem}.custom-file-label::after{position:absolute;top:0;right:0;bottom:0;z-index:3;display:block;height:calc(1.5em + .75rem);padding:.375rem .75rem;line-height:1.5;color:#adb5bd;content:"Browse";background-color:#444;border-left:inherit;border-radius:0 .25rem .25rem 0}.custom-range{width:100%;height:1.4rem;padding:0;background-color:transparent;appearance:none}.custom-range:focus{outline:0}.custom-range:focus::-webkit-slider-thumb{box-shadow:0 0 0 1px #222,0 0 0 .2rem rgba(55,90,127,.25)}.custom-range:focus::-moz-range-thumb{box-shadow:0 0 0 1px #222,0 0 0 .2rem rgba(55,90,127,.25)}.custom-range:focus::-ms-thumb{box-shadow:0 0 0 1px #222,0 0 0 .2rem rgba(55,90,127,.25)}.custom-range::-moz-focus-outer{border:0}.custom-range::-webkit-slider-thumb{width:1rem;height:1rem;margin-top:-.25rem;background-color:#375a7f;border:0;border-radius:1rem;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;appearance:none}@media (prefers-reduced-motion:reduce){.custom-range::-webkit-slider-thumb{transition:none}}.custom-range::-webkit-slider-thumb:active{background-color:#97b3d2}.custom-range::-webkit-slider-runnable-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:#dee2e6;border-color:transparent;border-radius:1rem}.custom-range::-moz-range-thumb{width:1rem;height:1rem;background-color:#375a7f;border:0;border-radius:1rem;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;appearance:none}@media (prefers-reduced-motion:reduce){.custom-range::-moz-range-thumb{transition:none}}.custom-range::-moz-range-thumb:active{background-color:#97b3d2}.custom-range::-moz-range-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:#dee2e6;border-color:transparent;border-radius:1rem}.custom-range::-ms-thumb{width:1rem;height:1rem;margin-top:0;margin-right:.2rem;margin-left:.2rem;background-color:#375a7f;border:0;border-radius:1rem;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;appearance:none}@media (prefers-reduced-motion:reduce){.custom-range::-ms-thumb{transition:none}}.custom-range::-ms-thumb:active{background-color:#97b3d2}.custom-range::-ms-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:transparent;border-color:transparent;border-width:.5rem}.custom-range::-ms-fill-lower{background-color:#dee2e6;border-radius:1rem}.custom-range::-ms-fill-upper{margin-right:15px;background-color:#dee2e6;border-radius:1rem}.custom-range:disabled::-webkit-slider-thumb{background-color:#adb5bd}.custom-range:disabled::-webkit-slider-runnable-track{cursor:default}.custom-range:disabled::-moz-range-thumb{background-color:#adb5bd}.custom-range:disabled::-moz-range-track{cursor:default}.custom-range:disabled::-ms-thumb{background-color:#adb5bd}.custom-control-label::before,.custom-file-label,.custom-select{transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.custom-control-label::before,.custom-file-label,.custom-select{transition:none}}.nav{display:flex;flex-wrap:wrap;padding-left:0;margin-bottom:0;list-style:none}.nav-link{display:block;padding:.5rem 2rem}.nav-link:focus,.nav-link:hover{text-decoration:none}.nav-link.disabled{color:#adb5bd;pointer-events:none;cursor:default}.nav-tabs{border-bottom:1px solid #444}.nav-tabs .nav-item{margin-bottom:-1px}.nav-tabs .nav-link{border:1px solid transparent;border-top-left-radius:.25rem;border-top-right-radius:.25rem}.nav-tabs .nav-link:focus,.nav-tabs .nav-link:hover{border-color:#444 #444 transparent}.nav-tabs .nav-link.disabled{color:#adb5bd;background-color:transparent;border-color:transparent}.nav-tabs .nav-item.show .nav-link,.nav-tabs .nav-link.active{color:#fff;background-color:#222;border-color:#444 #444 transparent}.nav-tabs .dropdown-menu{margin-top:-1px;border-top-left-radius:0;border-top-right-radius:0}.nav-pills .nav-link{border-radius:.25rem}.nav-pills .nav-link.active,.nav-pills .show>.nav-link{color:#fff;background-color:#375a7f}.nav-fill .nav-item{flex:1 1 auto;text-align:center}.nav-justified .nav-item{flex-basis:0;flex-grow:1;text-align:center}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.navbar{position:relative;display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;padding:1rem 1rem}.navbar .container,.navbar .container-fluid,.navbar .container-lg,.navbar .container-md,.navbar .container-sm,.navbar .container-xl{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between}.navbar-brand{display:inline-block;padding-top:.32422rem;padding-bottom:.32422rem;margin-right:1rem;font-size:1.17188rem;line-height:inherit;white-space:nowrap}.navbar-brand:focus,.navbar-brand:hover{text-decoration:none}.navbar-nav{display:flex;flex-direction:column;padding-left:0;margin-bottom:0;list-style:none}.navbar-nav .nav-link{padding-right:0;padding-left:0}.navbar-nav .dropdown-menu{position:static;float:none}.navbar-text{display:inline-block;padding-top:.5rem;padding-bottom:.5rem}.navbar-collapse{flex-basis:100%;flex-grow:1;align-items:center}.navbar-toggler{padding:.25rem .75rem;font-size:1.17188rem;line-height:1;background-color:transparent;border:1px solid transparent;border-radius:.25rem}.navbar-toggler:focus,.navbar-toggler:hover{text-decoration:none}.navbar-toggler-icon{display:inline-block;width:1.5em;height:1.5em;vertical-align:middle;content:"";background:no-repeat center center;background-size:100% 100%}@media (max-width:575.98px){.navbar-expand-sm>.container,.navbar-expand-sm>.container-fluid,.navbar-expand-sm>.container-lg,.navbar-expand-sm>.container-md,.navbar-expand-sm>.container-sm,.navbar-expand-sm>.container-xl{padding-right:0;padding-left:0}}@media (min-width:576px){.navbar-expand-sm{flex-flow:row nowrap;justify-content:flex-start}.navbar-expand-sm .navbar-nav{flex-direction:row}.navbar-expand-sm .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-sm .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-sm>.container,.navbar-expand-sm>.container-fluid,.navbar-expand-sm>.container-lg,.navbar-expand-sm>.container-md,.navbar-expand-sm>.container-sm,.navbar-expand-sm>.container-xl{flex-wrap:nowrap}.navbar-expand-sm .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-sm .navbar-toggler{display:none}}@media (max-width:767.98px){.navbar-expand-md>.container,.navbar-expand-md>.container-fluid,.navbar-expand-md>.container-lg,.navbar-expand-md>.container-md,.navbar-expand-md>.container-sm,.navbar-expand-md>.container-xl{padding-right:0;padding-left:0}}@media (min-width:768px){.navbar-expand-md{flex-flow:row nowrap;justify-content:flex-start}.navbar-expand-md .navbar-nav{flex-direction:row}.navbar-expand-md .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-md .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-md>.container,.navbar-expand-md>.container-fluid,.navbar-expand-md>.container-lg,.navbar-expand-md>.container-md,.navbar-expand-md>.container-sm,.navbar-expand-md>.container-xl{flex-wrap:nowrap}.navbar-expand-md .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-md .navbar-toggler{display:none}}@media (max-width:991.98px){.navbar-expand-lg>.container,.navbar-expand-lg>.container-fluid,.navbar-expand-lg>.container-lg,.navbar-expand-lg>.container-md,.navbar-expand-lg>.container-sm,.navbar-expand-lg>.container-xl{padding-right:0;padding-left:0}}@media (min-width:992px){.navbar-expand-lg{flex-flow:row nowrap;justify-content:flex-start}.navbar-expand-lg .navbar-nav{flex-direction:row}.navbar-expand-lg .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-lg .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-lg>.container,.navbar-expand-lg>.container-fluid,.navbar-expand-lg>.container-lg,.navbar-expand-lg>.container-md,.navbar-expand-lg>.container-sm,.navbar-expand-lg>.container-xl{flex-wrap:nowrap}.navbar-expand-lg .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-lg .navbar-toggler{display:none}}@media (max-width:1199.98px){.navbar-expand-xl>.container,.navbar-expand-xl>.container-fluid,.navbar-expand-xl>.container-lg,.navbar-expand-xl>.container-md,.navbar-expand-xl>.container-sm,.navbar-expand-xl>.container-xl{padding-right:0;padding-left:0}}@media (min-width:1200px){.navbar-expand-xl{flex-flow:row nowrap;justify-content:flex-start}.navbar-expand-xl .navbar-nav{flex-direction:row}.navbar-expand-xl .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-xl .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-xl>.container,.navbar-expand-xl>.container-fluid,.navbar-expand-xl>.container-lg,.navbar-expand-xl>.container-md,.navbar-expand-xl>.container-sm,.navbar-expand-xl>.container-xl{flex-wrap:nowrap}.navbar-expand-xl .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-xl .navbar-toggler{display:none}}.navbar-expand{flex-flow:row nowrap;justify-content:flex-start}.navbar-expand>.container,.navbar-expand>.container-fluid,.navbar-expand>.container-lg,.navbar-expand>.container-md,.navbar-expand>.container-sm,.navbar-expand>.container-xl{padding-right:0;padding-left:0}.navbar-expand .navbar-nav{flex-direction:row}.navbar-expand .navbar-nav .dropdown-menu{position:absolute}.navbar-expand .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand>.container,.navbar-expand>.container-fluid,.navbar-expand>.container-lg,.navbar-expand>.container-md,.navbar-expand>.container-sm,.navbar-expand>.container-xl{flex-wrap:nowrap}.navbar-expand .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand .navbar-toggler{display:none}.navbar-light .navbar-brand{color:#fff}.navbar-light .navbar-brand:focus,.navbar-light .navbar-brand:hover{color:#fff}.navbar-light .navbar-nav .nav-link{color:rgba(255,255,255,.6)}.navbar-light .navbar-nav .nav-link:focus,.navbar-light .navbar-nav .nav-link:hover{color:#fff}.navbar-light .navbar-nav .nav-link.disabled{color:rgba(0,0,0,.3)}.navbar-light .navbar-nav .active>.nav-link,.navbar-light .navbar-nav .nav-link.active,.navbar-light .navbar-nav .nav-link.show,.navbar-light .navbar-nav .show>.nav-link{color:#fff}.navbar-light .navbar-toggler{color:rgba(255,255,255,.6);border-color:rgba(34,34,34,.1)}.navbar-light .navbar-toggler-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='30' height='30' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.6%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e")}.navbar-light .navbar-text{color:rgba(255,255,255,.6)}.navbar-light .navbar-text a{color:#fff}.navbar-light .navbar-text a:focus,.navbar-light .navbar-text a:hover{color:#fff}.navbar-dark .navbar-brand{color:#fff}.navbar-dark .navbar-brand:focus,.navbar-dark .navbar-brand:hover{color:#fff}.navbar-dark .navbar-nav .nav-link{color:rgba(255,255,255,.6)}.navbar-dark .navbar-nav .nav-link:focus,.navbar-dark .navbar-nav .nav-link:hover{color:#fff}.navbar-dark .navbar-nav .nav-link.disabled{color:rgba(255,255,255,.25)}.navbar-dark .navbar-nav .active>.nav-link,.navbar-dark .navbar-nav .nav-link.active,.navbar-dark .navbar-nav .nav-link.show,.navbar-dark .navbar-nav .show>.nav-link{color:#fff}.navbar-dark .navbar-toggler{color:rgba(255,255,255,.6);border-color:rgba(255,255,255,.1)}.navbar-dark .navbar-toggler-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='30' height='30' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.6%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e")}.navbar-dark .navbar-text{color:rgba(255,255,255,.6)}.navbar-dark .navbar-text a{color:#fff}.navbar-dark .navbar-text a:focus,.navbar-dark .navbar-text a:hover{color:#fff}.card{position:relative;display:flex;flex-direction:column;min-width:0;word-wrap:break-word;background-color:#303030;background-clip:border-box;border:1px solid rgba(0,0,0,.125);border-radius:.25rem}.card>hr{margin-right:0;margin-left:0}.card>.list-group{border-top:inherit;border-bottom:inherit}.card>.list-group:first-child{border-top-width:0;border-top-left-radius:calc(.25rem - 1px);border-top-right-radius:calc(.25rem - 1px)}.card>.list-group:last-child{border-bottom-width:0;border-bottom-right-radius:calc(.25rem - 1px);border-bottom-left-radius:calc(.25rem - 1px)}.card-body{flex:1 1 auto;min-height:1px;padding:1.25rem}.card-title{margin-bottom:.75rem}.card-subtitle{margin-top:-.375rem;margin-bottom:0}.card-text:last-child{margin-bottom:0}.card-link:hover{text-decoration:none}.card-link+.card-link{margin-left:1.25rem}.card-header{padding:.75rem 1.25rem;margin-bottom:0;background-color:#444;border-bottom:1px solid rgba(0,0,0,.125)}.card-header:first-child{border-radius:calc(.25rem - 1px) calc(.25rem - 1px) 0 0}.card-header+.list-group .list-group-item:first-child{border-top:0}.card-footer{padding:.75rem 1.25rem;background-color:#444;border-top:1px solid rgba(0,0,0,.125)}.card-footer:last-child{border-radius:0 0 calc(.25rem - 1px) calc(.25rem - 1px)}.card-header-tabs{margin-right:-.625rem;margin-bottom:-.75rem;margin-left:-.625rem;border-bottom:0}.card-header-pills{margin-right:-.625rem;margin-left:-.625rem}.card-img-overlay{position:absolute;top:0;right:0;bottom:0;left:0;padding:1.25rem}.card-img,.card-img-bottom,.card-img-top{flex-shrink:0;width:100%}.card-img,.card-img-top{border-top-left-radius:calc(.25rem - 1px);border-top-right-radius:calc(.25rem - 1px)}.card-img,.card-img-bottom{border-bottom-right-radius:calc(.25rem - 1px);border-bottom-left-radius:calc(.25rem - 1px)}.card-deck .card{margin-bottom:15px}@media (min-width:576px){.card-deck{display:flex;flex-flow:row wrap;margin-right:-15px;margin-left:-15px}.card-deck .card{flex:1 0 0%;margin-right:15px;margin-bottom:0;margin-left:15px}}.card-group>.card{margin-bottom:15px}@media (min-width:576px){.card-group{display:flex;flex-flow:row wrap}.card-group>.card{flex:1 0 0%;margin-bottom:0}.card-group>.card+.card{margin-left:0;border-left:0}.card-group>.card:not(:last-child){border-top-right-radius:0;border-bottom-right-radius:0}.card-group>.card:not(:last-child) .card-header,.card-group>.card:not(:last-child) .card-img-top{border-top-right-radius:0}.card-group>.card:not(:last-child) .card-footer,.card-group>.card:not(:last-child) .card-img-bottom{border-bottom-right-radius:0}.card-group>.card:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.card-group>.card:not(:first-child) .card-header,.card-group>.card:not(:first-child) .card-img-top{border-top-left-radius:0}.card-group>.card:not(:first-child) .card-footer,.card-group>.card:not(:first-child) .card-img-bottom{border-bottom-left-radius:0}}.card-columns .card{margin-bottom:.75rem}@media (min-width:576px){.card-columns{column-count:3;column-gap:1.25rem;orphans:1;widows:1}.card-columns .card{display:inline-block;width:100%}}.accordion>.card{overflow:hidden}.accordion>.card:not(:last-of-type){border-bottom:0;border-bottom-right-radius:0;border-bottom-left-radius:0}.accordion>.card:not(:first-of-type){border-top-left-radius:0;border-top-right-radius:0}.accordion>.card>.card-header{border-radius:0;margin-bottom:-1px}.breadcrumb{display:flex;flex-wrap:wrap;padding:.75rem 1rem;margin-bottom:1rem;list-style:none;background-color:#444;border-radius:.25rem}.breadcrumb-item{display:flex}.breadcrumb-item+.breadcrumb-item{padding-left:.5rem}.breadcrumb-item+.breadcrumb-item::before{display:inline-block;padding-right:.5rem;color:#888;content:"/"}.breadcrumb-item+.breadcrumb-item:hover::before{text-decoration:underline}.breadcrumb-item+.breadcrumb-item:hover::before{text-decoration:none}.breadcrumb-item.active{color:#888}.pagination{display:flex;padding-left:0;list-style:none;border-radius:.25rem}.page-link{position:relative;display:block;padding:.5rem .75rem;margin-left:0;line-height:1.25;color:#fff;background-color:#00bc8c;border:0 solid transparent}.page-link:hover{z-index:2;color:#fff;text-decoration:none;background-color:#00efb2;border-color:transparent}.page-link:focus{z-index:3;outline:0;box-shadow:0 0 0 .2rem rgba(55,90,127,.25)}.page-item:first-child .page-link{margin-left:0;border-top-left-radius:.25rem;border-bottom-left-radius:.25rem}.page-item:last-child .page-link{border-top-right-radius:.25rem;border-bottom-right-radius:.25rem}.page-item.active .page-link{z-index:3;color:#fff;background-color:#00efb2;border-color:transparent}.page-item.disabled .page-link{color:#fff;pointer-events:none;cursor:auto;background-color:#007053;border-color:transparent}.pagination-lg .page-link{padding:.75rem 1.5rem;font-size:1.17188rem;line-height:1.5}.pagination-lg .page-item:first-child .page-link{border-top-left-radius:.3rem;border-bottom-left-radius:.3rem}.pagination-lg .page-item:last-child .page-link{border-top-right-radius:.3rem;border-bottom-right-radius:.3rem}.pagination-sm .page-link{padding:.25rem .5rem;font-size:.82031rem;line-height:1.5}.pagination-sm .page-item:first-child .page-link{border-top-left-radius:.2rem;border-bottom-left-radius:.2rem}.pagination-sm .page-item:last-child .page-link{border-top-right-radius:.2rem;border-bottom-right-radius:.2rem}.badge{display:inline-block;padding:.25em .4em;font-size:75%;font-weight:700;line-height:1;text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:.25rem;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.badge{transition:none}}a.badge:focus,a.badge:hover{text-decoration:none}.badge:empty{display:none}.btn .badge{position:relative;top:-1px}.badge-pill{padding-right:.6em;padding-left:.6em;border-radius:10rem}.badge-primary{color:#fff;background-color:#375a7f}a.badge-primary:focus,a.badge-primary:hover{color:#fff;background-color:#28415b}a.badge-primary.focus,a.badge-primary:focus{outline:0;box-shadow:0 0 0 .2rem rgba(55,90,127,.5)}.badge-secondary{color:#fff;background-color:#444}a.badge-secondary:focus,a.badge-secondary:hover{color:#fff;background-color:#2b2b2b}a.badge-secondary.focus,a.badge-secondary:focus{outline:0;box-shadow:0 0 0 .2rem rgba(68,68,68,.5)}.badge-success{color:#fff;background-color:#00bc8c}a.badge-success:focus,a.badge-success:hover{color:#fff;background-color:#008966}a.badge-success.focus,a.badge-success:focus{outline:0;box-shadow:0 0 0 .2rem rgba(0,188,140,.5)}.badge-info{color:#fff;background-color:#3498db}a.badge-info:focus,a.badge-info:hover{color:#fff;background-color:#217dbb}a.badge-info.focus,a.badge-info:focus{outline:0;box-shadow:0 0 0 .2rem rgba(52,152,219,.5)}.badge-warning{color:#fff;background-color:#f39c12}a.badge-warning:focus,a.badge-warning:hover{color:#fff;background-color:#c87f0a}a.badge-warning.focus,a.badge-warning:focus{outline:0;box-shadow:0 0 0 .2rem rgba(243,156,18,.5)}.badge-danger{color:#fff;background-color:#e74c3c}a.badge-danger:focus,a.badge-danger:hover{color:#fff;background-color:#d62c1a}a.badge-danger.focus,a.badge-danger:focus{outline:0;box-shadow:0 0 0 .2rem rgba(231,76,60,.5)}.badge-light{color:#fff;background-color:#303030}a.badge-light:focus,a.badge-light:hover{color:#fff;background-color:#171717}a.badge-light.focus,a.badge-light:focus{outline:0;box-shadow:0 0 0 .2rem rgba(48,48,48,.5)}.badge-dark{color:#222;background-color:#dee2e6}a.badge-dark:focus,a.badge-dark:hover{color:#222;background-color:#c1c9d0}a.badge-dark.focus,a.badge-dark:focus{outline:0;box-shadow:0 0 0 .2rem rgba(222,226,230,.5)}.jumbotron{padding:2rem 1rem;margin-bottom:2rem;background-color:#303030;border-radius:.3rem}@media (min-width:576px){.jumbotron{padding:4rem 2rem}}.jumbotron-fluid{padding-right:0;padding-left:0;border-radius:0}.alert{position:relative;padding:.75rem 1.25rem;margin-bottom:1rem;border:1px solid transparent;border-radius:.25rem}.alert-heading{color:inherit}.alert-link{font-weight:700}.alert-dismissible{padding-right:3.90625rem}.alert-dismissible .close{position:absolute;top:0;right:0;padding:.75rem 1.25rem;color:inherit}.alert-primary{color:#1d2f42;background-color:#d7dee5;border-color:#c7d1db}.alert-primary hr{border-top-color:#b7c4d1}.alert-primary .alert-link{color:#0d161f}.alert-secondary{color:#232323;background-color:#dadada;border-color:#cbcbcb}.alert-secondary hr{border-top-color:#bebebe}.alert-secondary .alert-link{color:#0a0a0a}.alert-success{color:#006249;background-color:#ccf2e8;border-color:#b8ecdf}.alert-success hr{border-top-color:#a4e7d6}.alert-success .alert-link{color:#002f23}.alert-info{color:#1b4f72;background-color:#d6eaf8;border-color:#c6e2f5}.alert-info hr{border-top-color:#b0d7f1}.alert-info .alert-link{color:#113249}.alert-warning{color:#7e5109;background-color:#fdebd0;border-color:#fce3bd}.alert-warning hr{border-top-color:#fbd9a5}.alert-warning .alert-link{color:#4e3206}.alert-danger{color:#78281f;background-color:#fadbd8;border-color:#f8cdc8}.alert-danger hr{border-top-color:#f5b8b1}.alert-danger .alert-link{color:#4f1a15}.alert-light{color:#191919;background-color:#d6d6d6;border-color:#c5c5c5}.alert-light hr{border-top-color:#b8b8b8}.alert-light .alert-link{color:#000}.alert-dark{color:#737678;background-color:#f8f9fa;border-color:#f6f7f8}.alert-dark hr{border-top-color:#e8eaed}.alert-dark .alert-link{color:#5a5c5e}@keyframes progress-bar-stripes{from{background-position:1rem 0}to{background-position:0 0}}.progress{display:flex;height:1rem;overflow:hidden;line-height:0;font-size:.70312rem;background-color:#444;border-radius:.25rem}.progress-bar{display:flex;flex-direction:column;justify-content:center;overflow:hidden;color:#fff;text-align:center;white-space:nowrap;background-color:#375a7f;transition:width .6s ease}@media (prefers-reduced-motion:reduce){.progress-bar{transition:none}}.progress-bar-striped{background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-size:1rem 1rem}.progress-bar-animated{animation:progress-bar-stripes 1s linear infinite}@media (prefers-reduced-motion:reduce){.progress-bar-animated{animation:none}}.media{display:flex;align-items:flex-start}.media-body{flex:1}.list-group{display:flex;flex-direction:column;padding-left:0;margin-bottom:0;border-radius:.25rem}.list-group-item-action{width:100%;color:#444;text-align:inherit}.list-group-item-action:focus,.list-group-item-action:hover{z-index:1;color:#444;text-decoration:none;background-color:#444}.list-group-item-action:active{color:#dee2e6;background-color:#ebebeb}.list-group-item{position:relative;display:block;padding:.75rem 1.25rem;background-color:#303030;border:1px solid #444}.list-group-item:first-child{border-top-left-radius:inherit;border-top-right-radius:inherit}.list-group-item:last-child{border-bottom-right-radius:inherit;border-bottom-left-radius:inherit}.list-group-item.disabled,.list-group-item:disabled{color:#888;pointer-events:none;background-color:#303030}.list-group-item.active{z-index:2;color:#fff;background-color:#375a7f;border-color:#375a7f}.list-group-item+.list-group-item{border-top-width:0}.list-group-item+.list-group-item.active{margin-top:-1px;border-top-width:1px}.list-group-horizontal{flex-direction:row}.list-group-horizontal>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal>.list-group-item.active{margin-top:0}.list-group-horizontal>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}@media (min-width:576px){.list-group-horizontal-sm{flex-direction:row}.list-group-horizontal-sm>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-sm>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-sm>.list-group-item.active{margin-top:0}.list-group-horizontal-sm>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-sm>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}@media (min-width:768px){.list-group-horizontal-md{flex-direction:row}.list-group-horizontal-md>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-md>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-md>.list-group-item.active{margin-top:0}.list-group-horizontal-md>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-md>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}@media (min-width:992px){.list-group-horizontal-lg{flex-direction:row}.list-group-horizontal-lg>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-lg>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-lg>.list-group-item.active{margin-top:0}.list-group-horizontal-lg>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-lg>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}@media (min-width:1200px){.list-group-horizontal-xl{flex-direction:row}.list-group-horizontal-xl>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-xl>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-xl>.list-group-item.active{margin-top:0}.list-group-horizontal-xl>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-xl>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}.list-group-flush{border-radius:0}.list-group-flush>.list-group-item{border-width:0 0 1px}.list-group-flush>.list-group-item:last-child{border-bottom-width:0}.list-group-item-primary{color:#1d2f42;background-color:#c7d1db}.list-group-item-primary.list-group-item-action:focus,.list-group-item-primary.list-group-item-action:hover{color:#1d2f42;background-color:#b7c4d1}.list-group-item-primary.list-group-item-action.active{color:#fff;background-color:#1d2f42;border-color:#1d2f42}.list-group-item-secondary{color:#232323;background-color:#cbcbcb}.list-group-item-secondary.list-group-item-action:focus,.list-group-item-secondary.list-group-item-action:hover{color:#232323;background-color:#bebebe}.list-group-item-secondary.list-group-item-action.active{color:#fff;background-color:#232323;border-color:#232323}.list-group-item-success{color:#006249;background-color:#b8ecdf}.list-group-item-success.list-group-item-action:focus,.list-group-item-success.list-group-item-action:hover{color:#006249;background-color:#a4e7d6}.list-group-item-success.list-group-item-action.active{color:#fff;background-color:#006249;border-color:#006249}.list-group-item-info{color:#1b4f72;background-color:#c6e2f5}.list-group-item-info.list-group-item-action:focus,.list-group-item-info.list-group-item-action:hover{color:#1b4f72;background-color:#b0d7f1}.list-group-item-info.list-group-item-action.active{color:#fff;background-color:#1b4f72;border-color:#1b4f72}.list-group-item-warning{color:#7e5109;background-color:#fce3bd}.list-group-item-warning.list-group-item-action:focus,.list-group-item-warning.list-group-item-action:hover{color:#7e5109;background-color:#fbd9a5}.list-group-item-warning.list-group-item-action.active{color:#fff;background-color:#7e5109;border-color:#7e5109}.list-group-item-danger{color:#78281f;background-color:#f8cdc8}.list-group-item-danger.list-group-item-action:focus,.list-group-item-danger.list-group-item-action:hover{color:#78281f;background-color:#f5b8b1}.list-group-item-danger.list-group-item-action.active{color:#fff;background-color:#78281f;border-color:#78281f}.list-group-item-light{color:#191919;background-color:#c5c5c5}.list-group-item-light.list-group-item-action:focus,.list-group-item-light.list-group-item-action:hover{color:#191919;background-color:#b8b8b8}.list-group-item-light.list-group-item-action.active{color:#fff;background-color:#191919;border-color:#191919}.list-group-item-dark{color:#737678;background-color:#f6f7f8}.list-group-item-dark.list-group-item-action:focus,.list-group-item-dark.list-group-item-action:hover{color:#737678;background-color:#e8eaed}.list-group-item-dark.list-group-item-action.active{color:#fff;background-color:#737678;border-color:#737678}.close{float:right;font-size:1.40625rem;font-weight:700;line-height:1;color:#fff;text-shadow:none;opacity:.5}.close:hover{color:#fff;text-decoration:none}.close:not(:disabled):not(.disabled):focus,.close:not(:disabled):not(.disabled):hover{opacity:.75}button.close{padding:0;background-color:transparent;border:0}a.close.disabled{pointer-events:none}.toast{max-width:350px;overflow:hidden;font-size:.875rem;background-color:#444;background-clip:padding-box;border:1px solid rgba(0,0,0,.1);box-shadow:0 .25rem .75rem rgba(0,0,0,.1);backdrop-filter:blur(10px);opacity:0;border-radius:.25rem}.toast:not(:last-child){margin-bottom:.75rem}.toast.showing{opacity:1}.toast.show{display:block;opacity:1}.toast.hide{display:none}.toast-header{display:flex;align-items:center;padding:.25rem .75rem;color:#888;background-color:#303030;background-clip:padding-box;border-bottom:1px solid rgba(0,0,0,.05)}.toast-body{padding:.75rem}.modal-open{overflow:hidden}.modal-open .modal{overflow-x:hidden;overflow-y:auto}.modal{position:fixed;top:0;left:0;z-index:1050;display:none;width:100%;height:100%;overflow:hidden;outline:0}.modal-dialog{position:relative;width:auto;margin:.5rem;pointer-events:none}.modal.fade .modal-dialog{transition:transform .3s ease-out;transform:translate(0,-50px)}@media (prefers-reduced-motion:reduce){.modal.fade .modal-dialog{transition:none}}.modal.show .modal-dialog{transform:none}.modal.modal-static .modal-dialog{transform:scale(1.02)}.modal-dialog-scrollable{display:flex;max-height:calc(100% - 1rem)}.modal-dialog-scrollable .modal-content{max-height:calc(100vh - 1rem);overflow:hidden}.modal-dialog-scrollable .modal-footer,.modal-dialog-scrollable .modal-header{flex-shrink:0}.modal-dialog-scrollable .modal-body{overflow-y:auto}.modal-dialog-centered{display:flex;align-items:center;min-height:calc(100% - 1rem)}.modal-dialog-centered::before{display:block;height:calc(100vh - 1rem);height:min-content;content:""}.modal-dialog-centered.modal-dialog-scrollable{flex-direction:column;justify-content:center;height:100%}.modal-dialog-centered.modal-dialog-scrollable .modal-content{max-height:none}.modal-dialog-centered.modal-dialog-scrollable::before{content:none}.modal-content{position:relative;display:flex;flex-direction:column;width:100%;pointer-events:auto;background-color:#303030;background-clip:padding-box;border:1px solid #444;border-radius:.3rem;outline:0}.modal-backdrop{position:fixed;top:0;left:0;z-index:1040;width:100vw;height:100vh;background-color:#000}.modal-backdrop.fade{opacity:0}.modal-backdrop.show{opacity:.5}.modal-header{display:flex;align-items:flex-start;justify-content:space-between;padding:1rem 1rem;border-bottom:1px solid #444;border-top-left-radius:calc(.3rem - 1px);border-top-right-radius:calc(.3rem - 1px)}.modal-header .close{padding:1rem 1rem;margin:-1rem -1rem -1rem auto}.modal-title{margin-bottom:0;line-height:1.5}.modal-body{position:relative;flex:1 1 auto;padding:1rem}.modal-footer{display:flex;flex-wrap:wrap;align-items:center;justify-content:flex-end;padding:.75rem;border-top:1px solid #444;border-bottom-right-radius:calc(.3rem - 1px);border-bottom-left-radius:calc(.3rem - 1px)}.modal-footer>*{margin:.25rem}.modal-scrollbar-measure{position:absolute;top:-9999px;width:50px;height:50px;overflow:scroll}@media (min-width:576px){.modal-dialog{max-width:500px;margin:1.75rem auto}.modal-dialog-scrollable{max-height:calc(100% - 3.5rem)}.modal-dialog-scrollable .modal-content{max-height:calc(100vh - 3.5rem)}.modal-dialog-centered{min-height:calc(100% - 3.5rem)}.modal-dialog-centered::before{height:calc(100vh - 3.5rem);height:min-content}.modal-sm{max-width:300px}}@media (min-width:992px){.modal-lg,.modal-xl{max-width:800px}}@media (min-width:1200px){.modal-xl{max-width:1140px}}.tooltip{position:absolute;z-index:1070;display:block;margin:0;font-family:Lato,-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol";font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;white-space:normal;line-break:auto;font-size:.82031rem;word-wrap:break-word;opacity:0}.tooltip.show{opacity:.9}.tooltip .arrow{position:absolute;display:block;width:.8rem;height:.4rem}.tooltip .arrow::before{position:absolute;content:"";border-color:transparent;border-style:solid}.bs-tooltip-auto[x-placement^=top],.bs-tooltip-top{padding:.4rem 0}.bs-tooltip-auto[x-placement^=top] .arrow,.bs-tooltip-top .arrow{bottom:0}.bs-tooltip-auto[x-placement^=top] .arrow::before,.bs-tooltip-top .arrow::before{top:0;border-width:.4rem .4rem 0;border-top-color:#000}.bs-tooltip-auto[x-placement^=right],.bs-tooltip-right{padding:0 .4rem}.bs-tooltip-auto[x-placement^=right] .arrow,.bs-tooltip-right .arrow{left:0;width:.4rem;height:.8rem}.bs-tooltip-auto[x-placement^=right] .arrow::before,.bs-tooltip-right .arrow::before{right:0;border-width:.4rem .4rem .4rem 0;border-right-color:#000}.bs-tooltip-auto[x-placement^=bottom],.bs-tooltip-bottom{padding:.4rem 0}.bs-tooltip-auto[x-placement^=bottom] .arrow,.bs-tooltip-bottom .arrow{top:0}.bs-tooltip-auto[x-placement^=bottom] .arrow::before,.bs-tooltip-bottom .arrow::before{bottom:0;border-width:0 .4rem .4rem;border-bottom-color:#000}.bs-tooltip-auto[x-placement^=left],.bs-tooltip-left{padding:0 .4rem}.bs-tooltip-auto[x-placement^=left] .arrow,.bs-tooltip-left .arrow{right:0;width:.4rem;height:.8rem}.bs-tooltip-auto[x-placement^=left] .arrow::before,.bs-tooltip-left .arrow::before{left:0;border-width:.4rem 0 .4rem .4rem;border-left-color:#000}.tooltip-inner{max-width:200px;padding:.25rem .5rem;color:#fff;text-align:center;background-color:#000;border-radius:.25rem}.popover{position:absolute;top:0;left:0;z-index:1060;display:block;max-width:276px;font-family:Lato,-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol";font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;white-space:normal;line-break:auto;font-size:.82031rem;word-wrap:break-word;background-color:#303030;background-clip:padding-box;border:1px solid rgba(0,0,0,.2);border-radius:.3rem}.popover .arrow{position:absolute;display:block;width:1rem;height:.5rem;margin:0 .3rem}.popover .arrow::after,.popover .arrow::before{position:absolute;display:block;content:"";border-color:transparent;border-style:solid}.bs-popover-auto[x-placement^=top],.bs-popover-top{margin-bottom:.5rem}.bs-popover-auto[x-placement^=top]>.arrow,.bs-popover-top>.arrow{bottom:calc(-.5rem - 1px)}.bs-popover-auto[x-placement^=top]>.arrow::before,.bs-popover-top>.arrow::before{bottom:0;border-width:.5rem .5rem 0;border-top-color:rgba(0,0,0,.25)}.bs-popover-auto[x-placement^=top]>.arrow::after,.bs-popover-top>.arrow::after{bottom:1px;border-width:.5rem .5rem 0;border-top-color:#303030}.bs-popover-auto[x-placement^=right],.bs-popover-right{margin-left:.5rem}.bs-popover-auto[x-placement^=right]>.arrow,.bs-popover-right>.arrow{left:calc(-.5rem - 1px);width:.5rem;height:1rem;margin:.3rem 0}.bs-popover-auto[x-placement^=right]>.arrow::before,.bs-popover-right>.arrow::before{left:0;border-width:.5rem .5rem .5rem 0;border-right-color:rgba(0,0,0,.25)}.bs-popover-auto[x-placement^=right]>.arrow::after,.bs-popover-right>.arrow::after{left:1px;border-width:.5rem .5rem .5rem 0;border-right-color:#303030}.bs-popover-auto[x-placement^=bottom],.bs-popover-bottom{margin-top:.5rem}.bs-popover-auto[x-placement^=bottom]>.arrow,.bs-popover-bottom>.arrow{top:calc(-.5rem - 1px)}.bs-popover-auto[x-placement^=bottom]>.arrow::before,.bs-popover-bottom>.arrow::before{top:0;border-width:0 .5rem .5rem .5rem;border-bottom-color:rgba(0,0,0,.25)}.bs-popover-auto[x-placement^=bottom]>.arrow::after,.bs-popover-bottom>.arrow::after{top:1px;border-width:0 .5rem .5rem .5rem;border-bottom-color:#303030}.bs-popover-auto[x-placement^=bottom] .popover-header::before,.bs-popover-bottom .popover-header::before{position:absolute;top:0;left:50%;display:block;width:1rem;margin-left:-.5rem;content:"";border-bottom:1px solid #444}.bs-popover-auto[x-placement^=left],.bs-popover-left{margin-right:.5rem}.bs-popover-auto[x-placement^=left]>.arrow,.bs-popover-left>.arrow{right:calc(-.5rem - 1px);width:.5rem;height:1rem;margin:.3rem 0}.bs-popover-auto[x-placement^=left]>.arrow::before,.bs-popover-left>.arrow::before{right:0;border-width:.5rem 0 .5rem .5rem;border-left-color:rgba(0,0,0,.25)}.bs-popover-auto[x-placement^=left]>.arrow::after,.bs-popover-left>.arrow::after{right:1px;border-width:.5rem 0 .5rem .5rem;border-left-color:#303030}.popover-header{padding:.5rem .75rem;margin-bottom:0;font-size:.9375rem;background-color:#444;border-bottom:1px solid #373737;border-top-left-radius:calc(.3rem - 1px);border-top-right-radius:calc(.3rem - 1px)}.popover-header:empty{display:none}.popover-body{padding:.5rem .75rem;color:#dee2e6}.carousel{position:relative}.carousel.pointer-event{touch-action:pan-y}.carousel-inner{position:relative;width:100%;overflow:hidden}.carousel-inner::after{display:block;clear:both;content:""}.carousel-item{position:relative;display:none;float:left;width:100%;margin-right:-100%;backface-visibility:hidden;transition:transform .6s ease-in-out}@media (prefers-reduced-motion:reduce){.carousel-item{transition:none}}.carousel-item-next,.carousel-item-prev,.carousel-item.active{display:block}.active.carousel-item-right,.carousel-item-next:not(.carousel-item-left){transform:translateX(100%)}.active.carousel-item-left,.carousel-item-prev:not(.carousel-item-right){transform:translateX(-100%)}.carousel-fade .carousel-item{opacity:0;transition-property:opacity;transform:none}.carousel-fade .carousel-item-next.carousel-item-left,.carousel-fade .carousel-item-prev.carousel-item-right,.carousel-fade .carousel-item.active{z-index:1;opacity:1}.carousel-fade .active.carousel-item-left,.carousel-fade .active.carousel-item-right{z-index:0;opacity:0;transition:opacity 0s .6s}@media (prefers-reduced-motion:reduce){.carousel-fade .active.carousel-item-left,.carousel-fade .active.carousel-item-right{transition:none}}.carousel-control-next,.carousel-control-prev{position:absolute;top:0;bottom:0;z-index:1;display:flex;align-items:center;justify-content:center;width:15%;color:#fff;text-align:center;opacity:.5;transition:opacity .15s ease}@media (prefers-reduced-motion:reduce){.carousel-control-next,.carousel-control-prev{transition:none}}.carousel-control-next:focus,.carousel-control-next:hover,.carousel-control-prev:focus,.carousel-control-prev:hover{color:#fff;text-decoration:none;outline:0;opacity:.9}.carousel-control-prev{left:0}.carousel-control-next{right:0}.carousel-control-next-icon,.carousel-control-prev-icon{display:inline-block;width:20px;height:20px;background:no-repeat 50%/100% 100%}.carousel-control-prev-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='%23fff' width='8' height='8' viewBox='0 0 8 8'%3e%3cpath d='M5.25 0l-4 4 4 4 1.5-1.5L4.25 4l2.5-2.5L5.25 0z'/%3e%3c/svg%3e")}.carousel-control-next-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='%23fff' width='8' height='8' viewBox='0 0 8 8'%3e%3cpath d='M2.75 0l-1.5 1.5L3.75 4l-2.5 2.5L2.75 8l4-4-4-4z'/%3e%3c/svg%3e")}.carousel-indicators{position:absolute;right:0;bottom:0;left:0;z-index:15;display:flex;justify-content:center;padding-left:0;margin-right:15%;margin-left:15%;list-style:none}.carousel-indicators li{box-sizing:content-box;flex:0 1 auto;width:30px;height:3px;margin-right:3px;margin-left:3px;text-indent:-999px;cursor:pointer;background-color:#fff;background-clip:padding-box;border-top:10px solid transparent;border-bottom:10px solid transparent;opacity:.5;transition:opacity .6s ease}@media (prefers-reduced-motion:reduce){.carousel-indicators li{transition:none}}.carousel-indicators .active{opacity:1}.carousel-caption{position:absolute;right:15%;bottom:20px;left:15%;z-index:10;padding-top:20px;padding-bottom:20px;color:#fff;text-align:center}@keyframes spinner-border{to{transform:rotate(360deg)}}.spinner-border{display:inline-block;width:2rem;height:2rem;vertical-align:text-bottom;border:.25em solid currentColor;border-right-color:transparent;border-radius:50%;animation:spinner-border .75s linear infinite}.spinner-border-sm{width:1rem;height:1rem;border-width:.2em}@keyframes spinner-grow{0%{transform:scale(0)}50%{opacity:1;transform:none}}.spinner-grow{display:inline-block;width:2rem;height:2rem;vertical-align:text-bottom;background-color:currentColor;border-radius:50%;opacity:0;animation:spinner-grow .75s linear infinite}.spinner-grow-sm{width:1rem;height:1rem}.align-baseline{vertical-align:baseline!important}.align-top{vertical-align:top!important}.align-middle{vertical-align:middle!important}.align-bottom{vertical-align:bottom!important}.align-text-bottom{vertical-align:text-bottom!important}.align-text-top{vertical-align:text-top!important}.bg-primary{background-color:#375a7f!important}a.bg-primary:focus,a.bg-primary:hover,button.bg-primary:focus,button.bg-primary:hover{background-color:#28415b!important}.bg-secondary{background-color:#444!important}a.bg-secondary:focus,a.bg-secondary:hover,button.bg-secondary:focus,button.bg-secondary:hover{background-color:#2b2b2b!important}.bg-success{background-color:#00bc8c!important}a.bg-success:focus,a.bg-success:hover,button.bg-success:focus,button.bg-success:hover{background-color:#008966!important}.bg-info{background-color:#3498db!important}a.bg-info:focus,a.bg-info:hover,button.bg-info:focus,button.bg-info:hover{background-color:#217dbb!important}.bg-warning{background-color:#f39c12!important}a.bg-warning:focus,a.bg-warning:hover,button.bg-warning:focus,button.bg-warning:hover{background-color:#c87f0a!important}.bg-danger{background-color:#e74c3c!important}a.bg-danger:focus,a.bg-danger:hover,button.bg-danger:focus,button.bg-danger:hover{background-color:#d62c1a!important}.bg-light{background-color:#303030!important}a.bg-light:focus,a.bg-light:hover,button.bg-light:focus,button.bg-light:hover{background-color:#171717!important}.bg-dark{background-color:#dee2e6!important}a.bg-dark:focus,a.bg-dark:hover,button.bg-dark:focus,button.bg-dark:hover{background-color:#c1c9d0!important}.bg-white{background-color:#fff!important}.bg-transparent{background-color:transparent!important}.border{border:1px solid #dee2e6!important}.border-top{border-top:1px solid #dee2e6!important}.border-right{border-right:1px solid #dee2e6!important}.border-bottom{border-bottom:1px solid #dee2e6!important}.border-left{border-left:1px solid #dee2e6!important}.border-0{border:0!important}.border-top-0{border-top:0!important}.border-right-0{border-right:0!important}.border-bottom-0{border-bottom:0!important}.border-left-0{border-left:0!important}.border-primary{border-color:#375a7f!important}.border-secondary{border-color:#444!important}.border-success{border-color:#00bc8c!important}.border-info{border-color:#3498db!important}.border-warning{border-color:#f39c12!important}.border-danger{border-color:#e74c3c!important}.border-light{border-color:#303030!important}.border-dark{border-color:#dee2e6!important}.border-white{border-color:#fff!important}.rounded-sm{border-radius:.2rem!important}.rounded{border-radius:.25rem!important}.rounded-top{border-top-left-radius:.25rem!important;border-top-right-radius:.25rem!important}.rounded-right{border-top-right-radius:.25rem!important;border-bottom-right-radius:.25rem!important}.rounded-bottom{border-bottom-right-radius:.25rem!important;border-bottom-left-radius:.25rem!important}.rounded-left{border-top-left-radius:.25rem!important;border-bottom-left-radius:.25rem!important}.rounded-lg{border-radius:.3rem!important}.rounded-circle{border-radius:50%!important}.rounded-pill{border-radius:50rem!important}.rounded-0{border-radius:0!important}.clearfix::after{display:block;clear:both;content:""}.d-none{display:none!important}.d-inline{display:inline!important}.d-inline-block{display:inline-block!important}.d-block{display:block!important}.d-table{display:table!important}.d-table-row{display:table-row!important}.d-table-cell{display:table-cell!important}.d-flex{display:flex!important}.d-inline-flex{display:inline-flex!important}@media (min-width:576px){.d-sm-none{display:none!important}.d-sm-inline{display:inline!important}.d-sm-inline-block{display:inline-block!important}.d-sm-block{display:block!important}.d-sm-table{display:table!important}.d-sm-table-row{display:table-row!important}.d-sm-table-cell{display:table-cell!important}.d-sm-flex{display:flex!important}.d-sm-inline-flex{display:inline-flex!important}}@media (min-width:768px){.d-md-none{display:none!important}.d-md-inline{display:inline!important}.d-md-inline-block{display:inline-block!important}.d-md-block{display:block!important}.d-md-table{display:table!important}.d-md-table-row{display:table-row!important}.d-md-table-cell{display:table-cell!important}.d-md-flex{display:flex!important}.d-md-inline-flex{display:inline-flex!important}}@media (min-width:992px){.d-lg-none{display:none!important}.d-lg-inline{display:inline!important}.d-lg-inline-block{display:inline-block!important}.d-lg-block{display:block!important}.d-lg-table{display:table!important}.d-lg-table-row{display:table-row!important}.d-lg-table-cell{display:table-cell!important}.d-lg-flex{display:flex!important}.d-lg-inline-flex{display:inline-flex!important}}@media (min-width:1200px){.d-xl-none{display:none!important}.d-xl-inline{display:inline!important}.d-xl-inline-block{display:inline-block!important}.d-xl-block{display:block!important}.d-xl-table{display:table!important}.d-xl-table-row{display:table-row!important}.d-xl-table-cell{display:table-cell!important}.d-xl-flex{display:flex!important}.d-xl-inline-flex{display:inline-flex!important}}@media print{.d-print-none{display:none!important}.d-print-inline{display:inline!important}.d-print-inline-block{display:inline-block!important}.d-print-block{display:block!important}.d-print-table{display:table!important}.d-print-table-row{display:table-row!important}.d-print-table-cell{display:table-cell!important}.d-print-flex{display:flex!important}.d-print-inline-flex{display:inline-flex!important}}.embed-responsive{position:relative;display:block;width:100%;padding:0;overflow:hidden}.embed-responsive::before{display:block;content:""}.embed-responsive .embed-responsive-item,.embed-responsive embed,.embed-responsive iframe,.embed-responsive object,.embed-responsive video{position:absolute;top:0;bottom:0;left:0;width:100%;height:100%;border:0}.embed-responsive-21by9::before{padding-top:42.85714%}.embed-responsive-16by9::before{padding-top:56.25%}.embed-responsive-4by3::before{padding-top:75%}.embed-responsive-1by1::before{padding-top:100%}.flex-row{flex-direction:row!important}.flex-column{flex-direction:column!important}.flex-row-reverse{flex-direction:row-reverse!important}.flex-column-reverse{flex-direction:column-reverse!important}.flex-wrap{flex-wrap:wrap!important}.flex-nowrap{flex-wrap:nowrap!important}.flex-wrap-reverse{flex-wrap:wrap-reverse!important}.flex-fill{flex:1 1 auto!important}.flex-grow-0{flex-grow:0!important}.flex-grow-1{flex-grow:1!important}.flex-shrink-0{flex-shrink:0!important}.flex-shrink-1{flex-shrink:1!important}.justify-content-start{justify-content:flex-start!important}.justify-content-end{justify-content:flex-end!important}.justify-content-center{justify-content:center!important}.justify-content-between{justify-content:space-between!important}.justify-content-around{justify-content:space-around!important}.align-items-start{align-items:flex-start!important}.align-items-end{align-items:flex-end!important}.align-items-center{align-items:center!important}.align-items-baseline{align-items:baseline!important}.align-items-stretch{align-items:stretch!important}.align-content-start{align-content:flex-start!important}.align-content-end{align-content:flex-end!important}.align-content-center{align-content:center!important}.align-content-between{align-content:space-between!important}.align-content-around{align-content:space-around!important}.align-content-stretch{align-content:stretch!important}.align-self-auto{align-self:auto!important}.align-self-start{align-self:flex-start!important}.align-self-end{align-self:flex-end!important}.align-self-center{align-self:center!important}.align-self-baseline{align-self:baseline!important}.align-self-stretch{align-self:stretch!important}@media (min-width:576px){.flex-sm-row{flex-direction:row!important}.flex-sm-column{flex-direction:column!important}.flex-sm-row-reverse{flex-direction:row-reverse!important}.flex-sm-column-reverse{flex-direction:column-reverse!important}.flex-sm-wrap{flex-wrap:wrap!important}.flex-sm-nowrap{flex-wrap:nowrap!important}.flex-sm-wrap-reverse{flex-wrap:wrap-reverse!important}.flex-sm-fill{flex:1 1 auto!important}.flex-sm-grow-0{flex-grow:0!important}.flex-sm-grow-1{flex-grow:1!important}.flex-sm-shrink-0{flex-shrink:0!important}.flex-sm-shrink-1{flex-shrink:1!important}.justify-content-sm-start{justify-content:flex-start!important}.justify-content-sm-end{justify-content:flex-end!important}.justify-content-sm-center{justify-content:center!important}.justify-content-sm-between{justify-content:space-between!important}.justify-content-sm-around{justify-content:space-around!important}.align-items-sm-start{align-items:flex-start!important}.align-items-sm-end{align-items:flex-end!important}.align-items-sm-center{align-items:center!important}.align-items-sm-baseline{align-items:baseline!important}.align-items-sm-stretch{align-items:stretch!important}.align-content-sm-start{align-content:flex-start!important}.align-content-sm-end{align-content:flex-end!important}.align-content-sm-center{align-content:center!important}.align-content-sm-between{align-content:space-between!important}.align-content-sm-around{align-content:space-around!important}.align-content-sm-stretch{align-content:stretch!important}.align-self-sm-auto{align-self:auto!important}.align-self-sm-start{align-self:flex-start!important}.align-self-sm-end{align-self:flex-end!important}.align-self-sm-center{align-self:center!important}.align-self-sm-baseline{align-self:baseline!important}.align-self-sm-stretch{align-self:stretch!important}}@media (min-width:768px){.flex-md-row{flex-direction:row!important}.flex-md-column{flex-direction:column!important}.flex-md-row-reverse{flex-direction:row-reverse!important}.flex-md-column-reverse{flex-direction:column-reverse!important}.flex-md-wrap{flex-wrap:wrap!important}.flex-md-nowrap{flex-wrap:nowrap!important}.flex-md-wrap-reverse{flex-wrap:wrap-reverse!important}.flex-md-fill{flex:1 1 auto!important}.flex-md-grow-0{flex-grow:0!important}.flex-md-grow-1{flex-grow:1!important}.flex-md-shrink-0{flex-shrink:0!important}.flex-md-shrink-1{flex-shrink:1!important}.justify-content-md-start{justify-content:flex-start!important}.justify-content-md-end{justify-content:flex-end!important}.justify-content-md-center{justify-content:center!important}.justify-content-md-between{justify-content:space-between!important}.justify-content-md-around{justify-content:space-around!important}.align-items-md-start{align-items:flex-start!important}.align-items-md-end{align-items:flex-end!important}.align-items-md-center{align-items:center!important}.align-items-md-baseline{align-items:baseline!important}.align-items-md-stretch{align-items:stretch!important}.align-content-md-start{align-content:flex-start!important}.align-content-md-end{align-content:flex-end!important}.align-content-md-center{align-content:center!important}.align-content-md-between{align-content:space-between!important}.align-content-md-around{align-content:space-around!important}.align-content-md-stretch{align-content:stretch!important}.align-self-md-auto{align-self:auto!important}.align-self-md-start{align-self:flex-start!important}.align-self-md-end{align-self:flex-end!important}.align-self-md-center{align-self:center!important}.align-self-md-baseline{align-self:baseline!important}.align-self-md-stretch{align-self:stretch!important}}@media (min-width:992px){.flex-lg-row{flex-direction:row!important}.flex-lg-column{flex-direction:column!important}.flex-lg-row-reverse{flex-direction:row-reverse!important}.flex-lg-column-reverse{flex-direction:column-reverse!important}.flex-lg-wrap{flex-wrap:wrap!important}.flex-lg-nowrap{flex-wrap:nowrap!important}.flex-lg-wrap-reverse{flex-wrap:wrap-reverse!important}.flex-lg-fill{flex:1 1 auto!important}.flex-lg-grow-0{flex-grow:0!important}.flex-lg-grow-1{flex-grow:1!important}.flex-lg-shrink-0{flex-shrink:0!important}.flex-lg-shrink-1{flex-shrink:1!important}.justify-content-lg-start{justify-content:flex-start!important}.justify-content-lg-end{justify-content:flex-end!important}.justify-content-lg-center{justify-content:center!important}.justify-content-lg-between{justify-content:space-between!important}.justify-content-lg-around{justify-content:space-around!important}.align-items-lg-start{align-items:flex-start!important}.align-items-lg-end{align-items:flex-end!important}.align-items-lg-center{align-items:center!important}.align-items-lg-baseline{align-items:baseline!important}.align-items-lg-stretch{align-items:stretch!important}.align-content-lg-start{align-content:flex-start!important}.align-content-lg-end{align-content:flex-end!important}.align-content-lg-center{align-content:center!important}.align-content-lg-between{align-content:space-between!important}.align-content-lg-around{align-content:space-around!important}.align-content-lg-stretch{align-content:stretch!important}.align-self-lg-auto{align-self:auto!important}.align-self-lg-start{align-self:flex-start!important}.align-self-lg-end{align-self:flex-end!important}.align-self-lg-center{align-self:center!important}.align-self-lg-baseline{align-self:baseline!important}.align-self-lg-stretch{align-self:stretch!important}}@media (min-width:1200px){.flex-xl-row{flex-direction:row!important}.flex-xl-column{flex-direction:column!important}.flex-xl-row-reverse{flex-direction:row-reverse!important}.flex-xl-column-reverse{flex-direction:column-reverse!important}.flex-xl-wrap{flex-wrap:wrap!important}.flex-xl-nowrap{flex-wrap:nowrap!important}.flex-xl-wrap-reverse{flex-wrap:wrap-reverse!important}.flex-xl-fill{flex:1 1 auto!important}.flex-xl-grow-0{flex-grow:0!important}.flex-xl-grow-1{flex-grow:1!important}.flex-xl-shrink-0{flex-shrink:0!important}.flex-xl-shrink-1{flex-shrink:1!important}.justify-content-xl-start{justify-content:flex-start!important}.justify-content-xl-end{justify-content:flex-end!important}.justify-content-xl-center{justify-content:center!important}.justify-content-xl-between{justify-content:space-between!important}.justify-content-xl-around{justify-content:space-around!important}.align-items-xl-start{align-items:flex-start!important}.align-items-xl-end{align-items:flex-end!important}.align-items-xl-center{align-items:center!important}.align-items-xl-baseline{align-items:baseline!important}.align-items-xl-stretch{align-items:stretch!important}.align-content-xl-start{align-content:flex-start!important}.align-content-xl-end{align-content:flex-end!important}.align-content-xl-center{align-content:center!important}.align-content-xl-between{align-content:space-between!important}.align-content-xl-around{align-content:space-around!important}.align-content-xl-stretch{align-content:stretch!important}.align-self-xl-auto{align-self:auto!important}.align-self-xl-start{align-self:flex-start!important}.align-self-xl-end{align-self:flex-end!important}.align-self-xl-center{align-self:center!important}.align-self-xl-baseline{align-self:baseline!important}.align-self-xl-stretch{align-self:stretch!important}}.float-left{float:left!important}.float-right{float:right!important}.float-none{float:none!important}@media (min-width:576px){.float-sm-left{float:left!important}.float-sm-right{float:right!important}.float-sm-none{float:none!important}}@media (min-width:768px){.float-md-left{float:left!important}.float-md-right{float:right!important}.float-md-none{float:none!important}}@media (min-width:992px){.float-lg-left{float:left!important}.float-lg-right{float:right!important}.float-lg-none{float:none!important}}@media (min-width:1200px){.float-xl-left{float:left!important}.float-xl-right{float:right!important}.float-xl-none{float:none!important}}.user-select-all{user-select:all!important}.user-select-auto{user-select:auto!important}.user-select-none{user-select:none!important}.overflow-auto{overflow:auto!important}.overflow-hidden{overflow:hidden!important}.position-static{position:static!important}.position-relative{position:relative!important}.position-absolute{position:absolute!important}.position-fixed{position:fixed!important}.position-sticky{position:sticky!important}.fixed-top{position:fixed;top:0;right:0;left:0;z-index:1030}.fixed-bottom{position:fixed;right:0;bottom:0;left:0;z-index:1030}@supports (position:sticky){.sticky-top{position:sticky;top:0;z-index:1020}}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;overflow:visible;clip:auto;white-space:normal}.shadow-sm{box-shadow:0 .125rem .25rem rgba(0,0,0,.075)!important}.shadow{box-shadow:0 .5rem 1rem rgba(0,0,0,.15)!important}.shadow-lg{box-shadow:0 1rem 3rem rgba(0,0,0,.175)!important}.shadow-none{box-shadow:none!important}.w-25{width:25%!important}.w-50{width:50%!important}.w-75{width:75%!important}.w-100{width:100%!important}.w-auto{width:auto!important}.h-25{height:25%!important}.h-50{height:50%!important}.h-75{height:75%!important}.h-100{height:100%!important}.h-auto{height:auto!important}.mw-100{max-width:100%!important}.mh-100{max-height:100%!important}.min-vw-100{min-width:100vw!important}.min-vh-100{min-height:100vh!important}.vw-100{width:100vw!important}.vh-100{height:100vh!important}.m-0{margin:0!important}.mt-0,.my-0{margin-top:0!important}.mr-0,.mx-0{margin-right:0!important}.mb-0,.my-0{margin-bottom:0!important}.ml-0,.mx-0{margin-left:0!important}.m-1{margin:.25rem!important}.mt-1,.my-1{margin-top:.25rem!important}.mr-1,.mx-1{margin-right:.25rem!important}.mb-1,.my-1{margin-bottom:.25rem!important}.ml-1,.mx-1{margin-left:.25rem!important}.m-2{margin:.5rem!important}.mt-2,.my-2{margin-top:.5rem!important}.mr-2,.mx-2{margin-right:.5rem!important}.mb-2,.my-2{margin-bottom:.5rem!important}.ml-2,.mx-2{margin-left:.5rem!important}.m-3{margin:1rem!important}.mt-3,.my-3{margin-top:1rem!important}.mr-3,.mx-3{margin-right:1rem!important}.mb-3,.my-3{margin-bottom:1rem!important}.ml-3,.mx-3{margin-left:1rem!important}.m-4{margin:1.5rem!important}.mt-4,.my-4{margin-top:1.5rem!important}.mr-4,.mx-4{margin-right:1.5rem!important}.mb-4,.my-4{margin-bottom:1.5rem!important}.ml-4,.mx-4{margin-left:1.5rem!important}.m-5{margin:3rem!important}.mt-5,.my-5{margin-top:3rem!important}.mr-5,.mx-5{margin-right:3rem!important}.mb-5,.my-5{margin-bottom:3rem!important}.ml-5,.mx-5{margin-left:3rem!important}.p-0{padding:0!important}.pt-0,.py-0{padding-top:0!important}.pr-0,.px-0{padding-right:0!important}.pb-0,.py-0{padding-bottom:0!important}.pl-0,.px-0{padding-left:0!important}.p-1{padding:.25rem!important}.pt-1,.py-1{padding-top:.25rem!important}.pr-1,.px-1{padding-right:.25rem!important}.pb-1,.py-1{padding-bottom:.25rem!important}.pl-1,.px-1{padding-left:.25rem!important}.p-2{padding:.5rem!important}.pt-2,.py-2{padding-top:.5rem!important}.pr-2,.px-2{padding-right:.5rem!important}.pb-2,.py-2{padding-bottom:.5rem!important}.pl-2,.px-2{padding-left:.5rem!important}.p-3{padding:1rem!important}.pt-3,.py-3{padding-top:1rem!important}.pr-3,.px-3{padding-right:1rem!important}.pb-3,.py-3{padding-bottom:1rem!important}.pl-3,.px-3{padding-left:1rem!important}.p-4{padding:1.5rem!important}.pt-4,.py-4{padding-top:1.5rem!important}.pr-4,.px-4{padding-right:1.5rem!important}.pb-4,.py-4{padding-bottom:1.5rem!important}.pl-4,.px-4{padding-left:1.5rem!important}.p-5{padding:3rem!important}.pt-5,.py-5{padding-top:3rem!important}.pr-5,.px-5{padding-right:3rem!important}.pb-5,.py-5{padding-bottom:3rem!important}.pl-5,.px-5{padding-left:3rem!important}.m-n1{margin:-.25rem!important}.mt-n1,.my-n1{margin-top:-.25rem!important}.mr-n1,.mx-n1{margin-right:-.25rem!important}.mb-n1,.my-n1{margin-bottom:-.25rem!important}.ml-n1,.mx-n1{margin-left:-.25rem!important}.m-n2{margin:-.5rem!important}.mt-n2,.my-n2{margin-top:-.5rem!important}.mr-n2,.mx-n2{margin-right:-.5rem!important}.mb-n2,.my-n2{margin-bottom:-.5rem!important}.ml-n2,.mx-n2{margin-left:-.5rem!important}.m-n3{margin:-1rem!important}.mt-n3,.my-n3{margin-top:-1rem!important}.mr-n3,.mx-n3{margin-right:-1rem!important}.mb-n3,.my-n3{margin-bottom:-1rem!important}.ml-n3,.mx-n3{margin-left:-1rem!important}.m-n4{margin:-1.5rem!important}.mt-n4,.my-n4{margin-top:-1.5rem!important}.mr-n4,.mx-n4{margin-right:-1.5rem!important}.mb-n4,.my-n4{margin-bottom:-1.5rem!important}.ml-n4,.mx-n4{margin-left:-1.5rem!important}.m-n5{margin:-3rem!important}.mt-n5,.my-n5{margin-top:-3rem!important}.mr-n5,.mx-n5{margin-right:-3rem!important}.mb-n5,.my-n5{margin-bottom:-3rem!important}.ml-n5,.mx-n5{margin-left:-3rem!important}.m-auto{margin:auto!important}.mt-auto,.my-auto{margin-top:auto!important}.mr-auto,.mx-auto{margin-right:auto!important}.mb-auto,.my-auto{margin-bottom:auto!important}.ml-auto,.mx-auto{margin-left:auto!important}@media (min-width:576px){.m-sm-0{margin:0!important}.mt-sm-0,.my-sm-0{margin-top:0!important}.mr-sm-0,.mx-sm-0{margin-right:0!important}.mb-sm-0,.my-sm-0{margin-bottom:0!important}.ml-sm-0,.mx-sm-0{margin-left:0!important}.m-sm-1{margin:.25rem!important}.mt-sm-1,.my-sm-1{margin-top:.25rem!important}.mr-sm-1,.mx-sm-1{margin-right:.25rem!important}.mb-sm-1,.my-sm-1{margin-bottom:.25rem!important}.ml-sm-1,.mx-sm-1{margin-left:.25rem!important}.m-sm-2{margin:.5rem!important}.mt-sm-2,.my-sm-2{margin-top:.5rem!important}.mr-sm-2,.mx-sm-2{margin-right:.5rem!important}.mb-sm-2,.my-sm-2{margin-bottom:.5rem!important}.ml-sm-2,.mx-sm-2{margin-left:.5rem!important}.m-sm-3{margin:1rem!important}.mt-sm-3,.my-sm-3{margin-top:1rem!important}.mr-sm-3,.mx-sm-3{margin-right:1rem!important}.mb-sm-3,.my-sm-3{margin-bottom:1rem!important}.ml-sm-3,.mx-sm-3{margin-left:1rem!important}.m-sm-4{margin:1.5rem!important}.mt-sm-4,.my-sm-4{margin-top:1.5rem!important}.mr-sm-4,.mx-sm-4{margin-right:1.5rem!important}.mb-sm-4,.my-sm-4{margin-bottom:1.5rem!important}.ml-sm-4,.mx-sm-4{margin-left:1.5rem!important}.m-sm-5{margin:3rem!important}.mt-sm-5,.my-sm-5{margin-top:3rem!important}.mr-sm-5,.mx-sm-5{margin-right:3rem!important}.mb-sm-5,.my-sm-5{margin-bottom:3rem!important}.ml-sm-5,.mx-sm-5{margin-left:3rem!important}.p-sm-0{padding:0!important}.pt-sm-0,.py-sm-0{padding-top:0!important}.pr-sm-0,.px-sm-0{padding-right:0!important}.pb-sm-0,.py-sm-0{padding-bottom:0!important}.pl-sm-0,.px-sm-0{padding-left:0!important}.p-sm-1{padding:.25rem!important}.pt-sm-1,.py-sm-1{padding-top:.25rem!important}.pr-sm-1,.px-sm-1{padding-right:.25rem!important}.pb-sm-1,.py-sm-1{padding-bottom:.25rem!important}.pl-sm-1,.px-sm-1{padding-left:.25rem!important}.p-sm-2{padding:.5rem!important}.pt-sm-2,.py-sm-2{padding-top:.5rem!important}.pr-sm-2,.px-sm-2{padding-right:.5rem!important}.pb-sm-2,.py-sm-2{padding-bottom:.5rem!important}.pl-sm-2,.px-sm-2{padding-left:.5rem!important}.p-sm-3{padding:1rem!important}.pt-sm-3,.py-sm-3{padding-top:1rem!important}.pr-sm-3,.px-sm-3{padding-right:1rem!important}.pb-sm-3,.py-sm-3{padding-bottom:1rem!important}.pl-sm-3,.px-sm-3{padding-left:1rem!important}.p-sm-4{padding:1.5rem!important}.pt-sm-4,.py-sm-4{padding-top:1.5rem!important}.pr-sm-4,.px-sm-4{padding-right:1.5rem!important}.pb-sm-4,.py-sm-4{padding-bottom:1.5rem!important}.pl-sm-4,.px-sm-4{padding-left:1.5rem!important}.p-sm-5{padding:3rem!important}.pt-sm-5,.py-sm-5{padding-top:3rem!important}.pr-sm-5,.px-sm-5{padding-right:3rem!important}.pb-sm-5,.py-sm-5{padding-bottom:3rem!important}.pl-sm-5,.px-sm-5{padding-left:3rem!important}.m-sm-n1{margin:-.25rem!important}.mt-sm-n1,.my-sm-n1{margin-top:-.25rem!important}.mr-sm-n1,.mx-sm-n1{margin-right:-.25rem!important}.mb-sm-n1,.my-sm-n1{margin-bottom:-.25rem!important}.ml-sm-n1,.mx-sm-n1{margin-left:-.25rem!important}.m-sm-n2{margin:-.5rem!important}.mt-sm-n2,.my-sm-n2{margin-top:-.5rem!important}.mr-sm-n2,.mx-sm-n2{margin-right:-.5rem!important}.mb-sm-n2,.my-sm-n2{margin-bottom:-.5rem!important}.ml-sm-n2,.mx-sm-n2{margin-left:-.5rem!important}.m-sm-n3{margin:-1rem!important}.mt-sm-n3,.my-sm-n3{margin-top:-1rem!important}.mr-sm-n3,.mx-sm-n3{margin-right:-1rem!important}.mb-sm-n3,.my-sm-n3{margin-bottom:-1rem!important}.ml-sm-n3,.mx-sm-n3{margin-left:-1rem!important}.m-sm-n4{margin:-1.5rem!important}.mt-sm-n4,.my-sm-n4{margin-top:-1.5rem!important}.mr-sm-n4,.mx-sm-n4{margin-right:-1.5rem!important}.mb-sm-n4,.my-sm-n4{margin-bottom:-1.5rem!important}.ml-sm-n4,.mx-sm-n4{margin-left:-1.5rem!important}.m-sm-n5{margin:-3rem!important}.mt-sm-n5,.my-sm-n5{margin-top:-3rem!important}.mr-sm-n5,.mx-sm-n5{margin-right:-3rem!important}.mb-sm-n5,.my-sm-n5{margin-bottom:-3rem!important}.ml-sm-n5,.mx-sm-n5{margin-left:-3rem!important}.m-sm-auto{margin:auto!important}.mt-sm-auto,.my-sm-auto{margin-top:auto!important}.mr-sm-auto,.mx-sm-auto{margin-right:auto!important}.mb-sm-auto,.my-sm-auto{margin-bottom:auto!important}.ml-sm-auto,.mx-sm-auto{margin-left:auto!important}}@media (min-width:768px){.m-md-0{margin:0!important}.mt-md-0,.my-md-0{margin-top:0!important}.mr-md-0,.mx-md-0{margin-right:0!important}.mb-md-0,.my-md-0{margin-bottom:0!important}.ml-md-0,.mx-md-0{margin-left:0!important}.m-md-1{margin:.25rem!important}.mt-md-1,.my-md-1{margin-top:.25rem!important}.mr-md-1,.mx-md-1{margin-right:.25rem!important}.mb-md-1,.my-md-1{margin-bottom:.25rem!important}.ml-md-1,.mx-md-1{margin-left:.25rem!important}.m-md-2{margin:.5rem!important}.mt-md-2,.my-md-2{margin-top:.5rem!important}.mr-md-2,.mx-md-2{margin-right:.5rem!important}.mb-md-2,.my-md-2{margin-bottom:.5rem!important}.ml-md-2,.mx-md-2{margin-left:.5rem!important}.m-md-3{margin:1rem!important}.mt-md-3,.my-md-3{margin-top:1rem!important}.mr-md-3,.mx-md-3{margin-right:1rem!important}.mb-md-3,.my-md-3{margin-bottom:1rem!important}.ml-md-3,.mx-md-3{margin-left:1rem!important}.m-md-4{margin:1.5rem!important}.mt-md-4,.my-md-4{margin-top:1.5rem!important}.mr-md-4,.mx-md-4{margin-right:1.5rem!important}.mb-md-4,.my-md-4{margin-bottom:1.5rem!important}.ml-md-4,.mx-md-4{margin-left:1.5rem!important}.m-md-5{margin:3rem!important}.mt-md-5,.my-md-5{margin-top:3rem!important}.mr-md-5,.mx-md-5{margin-right:3rem!important}.mb-md-5,.my-md-5{margin-bottom:3rem!important}.ml-md-5,.mx-md-5{margin-left:3rem!important}.p-md-0{padding:0!important}.pt-md-0,.py-md-0{padding-top:0!important}.pr-md-0,.px-md-0{padding-right:0!important}.pb-md-0,.py-md-0{padding-bottom:0!important}.pl-md-0,.px-md-0{padding-left:0!important}.p-md-1{padding:.25rem!important}.pt-md-1,.py-md-1{padding-top:.25rem!important}.pr-md-1,.px-md-1{padding-right:.25rem!important}.pb-md-1,.py-md-1{padding-bottom:.25rem!important}.pl-md-1,.px-md-1{padding-left:.25rem!important}.p-md-2{padding:.5rem!important}.pt-md-2,.py-md-2{padding-top:.5rem!important}.pr-md-2,.px-md-2{padding-right:.5rem!important}.pb-md-2,.py-md-2{padding-bottom:.5rem!important}.pl-md-2,.px-md-2{padding-left:.5rem!important}.p-md-3{padding:1rem!important}.pt-md-3,.py-md-3{padding-top:1rem!important}.pr-md-3,.px-md-3{padding-right:1rem!important}.pb-md-3,.py-md-3{padding-bottom:1rem!important}.pl-md-3,.px-md-3{padding-left:1rem!important}.p-md-4{padding:1.5rem!important}.pt-md-4,.py-md-4{padding-top:1.5rem!important}.pr-md-4,.px-md-4{padding-right:1.5rem!important}.pb-md-4,.py-md-4{padding-bottom:1.5rem!important}.pl-md-4,.px-md-4{padding-left:1.5rem!important}.p-md-5{padding:3rem!important}.pt-md-5,.py-md-5{padding-top:3rem!important}.pr-md-5,.px-md-5{padding-right:3rem!important}.pb-md-5,.py-md-5{padding-bottom:3rem!important}.pl-md-5,.px-md-5{padding-left:3rem!important}.m-md-n1{margin:-.25rem!important}.mt-md-n1,.my-md-n1{margin-top:-.25rem!important}.mr-md-n1,.mx-md-n1{margin-right:-.25rem!important}.mb-md-n1,.my-md-n1{margin-bottom:-.25rem!important}.ml-md-n1,.mx-md-n1{margin-left:-.25rem!important}.m-md-n2{margin:-.5rem!important}.mt-md-n2,.my-md-n2{margin-top:-.5rem!important}.mr-md-n2,.mx-md-n2{margin-right:-.5rem!important}.mb-md-n2,.my-md-n2{margin-bottom:-.5rem!important}.ml-md-n2,.mx-md-n2{margin-left:-.5rem!important}.m-md-n3{margin:-1rem!important}.mt-md-n3,.my-md-n3{margin-top:-1rem!important}.mr-md-n3,.mx-md-n3{margin-right:-1rem!important}.mb-md-n3,.my-md-n3{margin-bottom:-1rem!important}.ml-md-n3,.mx-md-n3{margin-left:-1rem!important}.m-md-n4{margin:-1.5rem!important}.mt-md-n4,.my-md-n4{margin-top:-1.5rem!important}.mr-md-n4,.mx-md-n4{margin-right:-1.5rem!important}.mb-md-n4,.my-md-n4{margin-bottom:-1.5rem!important}.ml-md-n4,.mx-md-n4{margin-left:-1.5rem!important}.m-md-n5{margin:-3rem!important}.mt-md-n5,.my-md-n5{margin-top:-3rem!important}.mr-md-n5,.mx-md-n5{margin-right:-3rem!important}.mb-md-n5,.my-md-n5{margin-bottom:-3rem!important}.ml-md-n5,.mx-md-n5{margin-left:-3rem!important}.m-md-auto{margin:auto!important}.mt-md-auto,.my-md-auto{margin-top:auto!important}.mr-md-auto,.mx-md-auto{margin-right:auto!important}.mb-md-auto,.my-md-auto{margin-bottom:auto!important}.ml-md-auto,.mx-md-auto{margin-left:auto!important}}@media (min-width:992px){.m-lg-0{margin:0!important}.mt-lg-0,.my-lg-0{margin-top:0!important}.mr-lg-0,.mx-lg-0{margin-right:0!important}.mb-lg-0,.my-lg-0{margin-bottom:0!important}.ml-lg-0,.mx-lg-0{margin-left:0!important}.m-lg-1{margin:.25rem!important}.mt-lg-1,.my-lg-1{margin-top:.25rem!important}.mr-lg-1,.mx-lg-1{margin-right:.25rem!important}.mb-lg-1,.my-lg-1{margin-bottom:.25rem!important}.ml-lg-1,.mx-lg-1{margin-left:.25rem!important}.m-lg-2{margin:.5rem!important}.mt-lg-2,.my-lg-2{margin-top:.5rem!important}.mr-lg-2,.mx-lg-2{margin-right:.5rem!important}.mb-lg-2,.my-lg-2{margin-bottom:.5rem!important}.ml-lg-2,.mx-lg-2{margin-left:.5rem!important}.m-lg-3{margin:1rem!important}.mt-lg-3,.my-lg-3{margin-top:1rem!important}.mr-lg-3,.mx-lg-3{margin-right:1rem!important}.mb-lg-3,.my-lg-3{margin-bottom:1rem!important}.ml-lg-3,.mx-lg-3{margin-left:1rem!important}.m-lg-4{margin:1.5rem!important}.mt-lg-4,.my-lg-4{margin-top:1.5rem!important}.mr-lg-4,.mx-lg-4{margin-right:1.5rem!important}.mb-lg-4,.my-lg-4{margin-bottom:1.5rem!important}.ml-lg-4,.mx-lg-4{margin-left:1.5rem!important}.m-lg-5{margin:3rem!important}.mt-lg-5,.my-lg-5{margin-top:3rem!important}.mr-lg-5,.mx-lg-5{margin-right:3rem!important}.mb-lg-5,.my-lg-5{margin-bottom:3rem!important}.ml-lg-5,.mx-lg-5{margin-left:3rem!important}.p-lg-0{padding:0!important}.pt-lg-0,.py-lg-0{padding-top:0!important}.pr-lg-0,.px-lg-0{padding-right:0!important}.pb-lg-0,.py-lg-0{padding-bottom:0!important}.pl-lg-0,.px-lg-0{padding-left:0!important}.p-lg-1{padding:.25rem!important}.pt-lg-1,.py-lg-1{padding-top:.25rem!important}.pr-lg-1,.px-lg-1{padding-right:.25rem!important}.pb-lg-1,.py-lg-1{padding-bottom:.25rem!important}.pl-lg-1,.px-lg-1{padding-left:.25rem!important}.p-lg-2{padding:.5rem!important}.pt-lg-2,.py-lg-2{padding-top:.5rem!important}.pr-lg-2,.px-lg-2{padding-right:.5rem!important}.pb-lg-2,.py-lg-2{padding-bottom:.5rem!important}.pl-lg-2,.px-lg-2{padding-left:.5rem!important}.p-lg-3{padding:1rem!important}.pt-lg-3,.py-lg-3{padding-top:1rem!important}.pr-lg-3,.px-lg-3{padding-right:1rem!important}.pb-lg-3,.py-lg-3{padding-bottom:1rem!important}.pl-lg-3,.px-lg-3{padding-left:1rem!important}.p-lg-4{padding:1.5rem!important}.pt-lg-4,.py-lg-4{padding-top:1.5rem!important}.pr-lg-4,.px-lg-4{padding-right:1.5rem!important}.pb-lg-4,.py-lg-4{padding-bottom:1.5rem!important}.pl-lg-4,.px-lg-4{padding-left:1.5rem!important}.p-lg-5{padding:3rem!important}.pt-lg-5,.py-lg-5{padding-top:3rem!important}.pr-lg-5,.px-lg-5{padding-right:3rem!important}.pb-lg-5,.py-lg-5{padding-bottom:3rem!important}.pl-lg-5,.px-lg-5{padding-left:3rem!important}.m-lg-n1{margin:-.25rem!important}.mt-lg-n1,.my-lg-n1{margin-top:-.25rem!important}.mr-lg-n1,.mx-lg-n1{margin-right:-.25rem!important}.mb-lg-n1,.my-lg-n1{margin-bottom:-.25rem!important}.ml-lg-n1,.mx-lg-n1{margin-left:-.25rem!important}.m-lg-n2{margin:-.5rem!important}.mt-lg-n2,.my-lg-n2{margin-top:-.5rem!important}.mr-lg-n2,.mx-lg-n2{margin-right:-.5rem!important}.mb-lg-n2,.my-lg-n2{margin-bottom:-.5rem!important}.ml-lg-n2,.mx-lg-n2{margin-left:-.5rem!important}.m-lg-n3{margin:-1rem!important}.mt-lg-n3,.my-lg-n3{margin-top:-1rem!important}.mr-lg-n3,.mx-lg-n3{margin-right:-1rem!important}.mb-lg-n3,.my-lg-n3{margin-bottom:-1rem!important}.ml-lg-n3,.mx-lg-n3{margin-left:-1rem!important}.m-lg-n4{margin:-1.5rem!important}.mt-lg-n4,.my-lg-n4{margin-top:-1.5rem!important}.mr-lg-n4,.mx-lg-n4{margin-right:-1.5rem!important}.mb-lg-n4,.my-lg-n4{margin-bottom:-1.5rem!important}.ml-lg-n4,.mx-lg-n4{margin-left:-1.5rem!important}.m-lg-n5{margin:-3rem!important}.mt-lg-n5,.my-lg-n5{margin-top:-3rem!important}.mr-lg-n5,.mx-lg-n5{margin-right:-3rem!important}.mb-lg-n5,.my-lg-n5{margin-bottom:-3rem!important}.ml-lg-n5,.mx-lg-n5{margin-left:-3rem!important}.m-lg-auto{margin:auto!important}.mt-lg-auto,.my-lg-auto{margin-top:auto!important}.mr-lg-auto,.mx-lg-auto{margin-right:auto!important}.mb-lg-auto,.my-lg-auto{margin-bottom:auto!important}.ml-lg-auto,.mx-lg-auto{margin-left:auto!important}}@media (min-width:1200px){.m-xl-0{margin:0!important}.mt-xl-0,.my-xl-0{margin-top:0!important}.mr-xl-0,.mx-xl-0{margin-right:0!important}.mb-xl-0,.my-xl-0{margin-bottom:0!important}.ml-xl-0,.mx-xl-0{margin-left:0!important}.m-xl-1{margin:.25rem!important}.mt-xl-1,.my-xl-1{margin-top:.25rem!important}.mr-xl-1,.mx-xl-1{margin-right:.25rem!important}.mb-xl-1,.my-xl-1{margin-bottom:.25rem!important}.ml-xl-1,.mx-xl-1{margin-left:.25rem!important}.m-xl-2{margin:.5rem!important}.mt-xl-2,.my-xl-2{margin-top:.5rem!important}.mr-xl-2,.mx-xl-2{margin-right:.5rem!important}.mb-xl-2,.my-xl-2{margin-bottom:.5rem!important}.ml-xl-2,.mx-xl-2{margin-left:.5rem!important}.m-xl-3{margin:1rem!important}.mt-xl-3,.my-xl-3{margin-top:1rem!important}.mr-xl-3,.mx-xl-3{margin-right:1rem!important}.mb-xl-3,.my-xl-3{margin-bottom:1rem!important}.ml-xl-3,.mx-xl-3{margin-left:1rem!important}.m-xl-4{margin:1.5rem!important}.mt-xl-4,.my-xl-4{margin-top:1.5rem!important}.mr-xl-4,.mx-xl-4{margin-right:1.5rem!important}.mb-xl-4,.my-xl-4{margin-bottom:1.5rem!important}.ml-xl-4,.mx-xl-4{margin-left:1.5rem!important}.m-xl-5{margin:3rem!important}.mt-xl-5,.my-xl-5{margin-top:3rem!important}.mr-xl-5,.mx-xl-5{margin-right:3rem!important}.mb-xl-5,.my-xl-5{margin-bottom:3rem!important}.ml-xl-5,.mx-xl-5{margin-left:3rem!important}.p-xl-0{padding:0!important}.pt-xl-0,.py-xl-0{padding-top:0!important}.pr-xl-0,.px-xl-0{padding-right:0!important}.pb-xl-0,.py-xl-0{padding-bottom:0!important}.pl-xl-0,.px-xl-0{padding-left:0!important}.p-xl-1{padding:.25rem!important}.pt-xl-1,.py-xl-1{padding-top:.25rem!important}.pr-xl-1,.px-xl-1{padding-right:.25rem!important}.pb-xl-1,.py-xl-1{padding-bottom:.25rem!important}.pl-xl-1,.px-xl-1{padding-left:.25rem!important}.p-xl-2{padding:.5rem!important}.pt-xl-2,.py-xl-2{padding-top:.5rem!important}.pr-xl-2,.px-xl-2{padding-right:.5rem!important}.pb-xl-2,.py-xl-2{padding-bottom:.5rem!important}.pl-xl-2,.px-xl-2{padding-left:.5rem!important}.p-xl-3{padding:1rem!important}.pt-xl-3,.py-xl-3{padding-top:1rem!important}.pr-xl-3,.px-xl-3{padding-right:1rem!important}.pb-xl-3,.py-xl-3{padding-bottom:1rem!important}.pl-xl-3,.px-xl-3{padding-left:1rem!important}.p-xl-4{padding:1.5rem!important}.pt-xl-4,.py-xl-4{padding-top:1.5rem!important}.pr-xl-4,.px-xl-4{padding-right:1.5rem!important}.pb-xl-4,.py-xl-4{padding-bottom:1.5rem!important}.pl-xl-4,.px-xl-4{padding-left:1.5rem!important}.p-xl-5{padding:3rem!important}.pt-xl-5,.py-xl-5{padding-top:3rem!important}.pr-xl-5,.px-xl-5{padding-right:3rem!important}.pb-xl-5,.py-xl-5{padding-bottom:3rem!important}.pl-xl-5,.px-xl-5{padding-left:3rem!important}.m-xl-n1{margin:-.25rem!important}.mt-xl-n1,.my-xl-n1{margin-top:-.25rem!important}.mr-xl-n1,.mx-xl-n1{margin-right:-.25rem!important}.mb-xl-n1,.my-xl-n1{margin-bottom:-.25rem!important}.ml-xl-n1,.mx-xl-n1{margin-left:-.25rem!important}.m-xl-n2{margin:-.5rem!important}.mt-xl-n2,.my-xl-n2{margin-top:-.5rem!important}.mr-xl-n2,.mx-xl-n2{margin-right:-.5rem!important}.mb-xl-n2,.my-xl-n2{margin-bottom:-.5rem!important}.ml-xl-n2,.mx-xl-n2{margin-left:-.5rem!important}.m-xl-n3{margin:-1rem!important}.mt-xl-n3,.my-xl-n3{margin-top:-1rem!important}.mr-xl-n3,.mx-xl-n3{margin-right:-1rem!important}.mb-xl-n3,.my-xl-n3{margin-bottom:-1rem!important}.ml-xl-n3,.mx-xl-n3{margin-left:-1rem!important}.m-xl-n4{margin:-1.5rem!important}.mt-xl-n4,.my-xl-n4{margin-top:-1.5rem!important}.mr-xl-n4,.mx-xl-n4{margin-right:-1.5rem!important}.mb-xl-n4,.my-xl-n4{margin-bottom:-1.5rem!important}.ml-xl-n4,.mx-xl-n4{margin-left:-1.5rem!important}.m-xl-n5{margin:-3rem!important}.mt-xl-n5,.my-xl-n5{margin-top:-3rem!important}.mr-xl-n5,.mx-xl-n5{margin-right:-3rem!important}.mb-xl-n5,.my-xl-n5{margin-bottom:-3rem!important}.ml-xl-n5,.mx-xl-n5{margin-left:-3rem!important}.m-xl-auto{margin:auto!important}.mt-xl-auto,.my-xl-auto{margin-top:auto!important}.mr-xl-auto,.mx-xl-auto{margin-right:auto!important}.mb-xl-auto,.my-xl-auto{margin-bottom:auto!important}.ml-xl-auto,.mx-xl-auto{margin-left:auto!important}}.stretched-link::after{position:absolute;top:0;right:0;bottom:0;left:0;z-index:1;pointer-events:auto;content:"";background-color:rgba(0,0,0,0)}.text-monospace{font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace!important}.text-justify{text-align:justify!important}.text-wrap{white-space:normal!important}.text-nowrap{white-space:nowrap!important}.text-truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.text-left{text-align:left!important}.text-right{text-align:right!important}.text-center{text-align:center!important}@media (min-width:576px){.text-sm-left{text-align:left!important}.text-sm-right{text-align:right!important}.text-sm-center{text-align:center!important}}@media (min-width:768px){.text-md-left{text-align:left!important}.text-md-right{text-align:right!important}.text-md-center{text-align:center!important}}@media (min-width:992px){.text-lg-left{text-align:left!important}.text-lg-right{text-align:right!important}.text-lg-center{text-align:center!important}}@media (min-width:1200px){.text-xl-left{text-align:left!important}.text-xl-right{text-align:right!important}.text-xl-center{text-align:center!important}}.text-lowercase{text-transform:lowercase!important}.text-uppercase{text-transform:uppercase!important}.text-capitalize{text-transform:capitalize!important}.font-weight-light{font-weight:300!important}.font-weight-lighter{font-weight:lighter!important}.font-weight-normal{font-weight:400!important}.font-weight-bold{font-weight:700!important}.font-weight-bolder{font-weight:bolder!important}.font-italic{font-style:italic!important}.text-white{color:#fff!important}.text-primary{color:#375a7f!important}a.text-primary:focus,a.text-primary:hover{color:#20344a!important}.text-secondary{color:#444!important}a.text-secondary:focus,a.text-secondary:hover{color:#1e1e1e!important}.text-success{color:#00bc8c!important}a.text-success:focus,a.text-success:hover{color:#007053!important}.text-info{color:#3498db!important}a.text-info:focus,a.text-info:hover{color:#1d6fa5!important}.text-warning{color:#f39c12!important}a.text-warning:focus,a.text-warning:hover{color:#b06f09!important}.text-danger{color:#e74c3c!important}a.text-danger:focus,a.text-danger:hover{color:#bf2718!important}.text-light{color:#303030!important}a.text-light:focus,a.text-light:hover{color:#0a0a0a!important}.text-dark{color:#dee2e6!important}a.text-dark:focus,a.text-dark:hover{color:#b2bcc5!important}.text-body{color:#dee2e6!important}.text-muted{color:#888!important}.text-black-50{color:rgba(0,0,0,.5)!important}.text-white-50{color:rgba(255,255,255,.5)!important}.text-hide{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.text-decoration-none{text-decoration:none!important}.text-break{word-wrap:break-word!important}.text-reset{color:inherit!important}.visible{visibility:visible!important}.invisible{visibility:hidden!important}@media print{*,::after,::before{text-shadow:none!important;box-shadow:none!important}a:not(.btn){text-decoration:underline}abbr[title]::after{content:" (" attr(title) ")"}pre{white-space:pre-wrap!important}blockquote,pre{border:1px solid #adb5bd;page-break-inside:avoid}thead{display:table-header-group}img,tr{page-break-inside:avoid}h2,h3,p{orphans:3;widows:3}h2,h3{page-break-after:avoid}@page{size:a3}body{min-width:992px!important}.container{min-width:992px!important}.navbar{display:none}.badge{border:1px solid #000}.table{border-collapse:collapse!important}.table td,.table th{background-color:#fff!important}.table-bordered td,.table-bordered th{border:1px solid #dee2e6!important}.table-dark{color:inherit}.table-dark tbody+tbody,.table-dark td,.table-dark th,.table-dark thead th{border-color:#444}.table .thead-dark th{color:inherit;border-color:#444}}
index 18a2244ba6f126a323e5540ae9664a2bc0c0fd9c..5c6615f43f1c94729ff7b29338aa4225504e64e5 100644 (file)
@@ -1 +1 @@
-:root{--blue:#007bff;--indigo:#6610f2;--purple:#6f42c1;--pink:#e83e8c;--red:#d8486a;--orange:#fac57c;--yellow:#ffc107;--green:#bad2be;--teal:#20c997;--cyan:#02bdc2;--white:#ffffff;--gray:#6c757d;--gray-dark:#343a40;--primary:#fac57c;--secondary:#bad2be;--success:#38553d;--info:#49ce5f;--warning:#ffc107;--danger:#ed8d09;--light:#f8f9fa;--dark:#343a40;--breakpoint-xs:0;--breakpoint-sm:576px;--breakpoint-md:768px;--breakpoint-lg:992px;--breakpoint-xl:1200px;--font-family-sans-serif:-apple-system,BlinkMacSystemFont,"Droid Sans","Segoe UI","Helvetica",Arial,sans-serif;--font-family-monospace:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace}*,::after,::before{box-sizing:border-box}html{font-family:sans-serif;line-height:1.15;-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:rgba(34,34,34,0)}article,aside,figcaption,figure,footer,header,hgroup,main,nav,section{display:block}body{margin:0;font-family:-apple-system,BlinkMacSystemFont,"Droid Sans","Segoe UI",Helvetica,Arial,sans-serif;font-size:1rem;font-weight:400;line-height:1.5;color:#495057;text-align:left;background-color:#f2f0f0}[tabindex="-1"]:focus{outline:0!important}hr{box-sizing:content-box;height:0;overflow:visible}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem}p{margin-top:0;margin-bottom:1rem}abbr[data-original-title],abbr[title]{text-decoration:underline;text-decoration:underline dotted;cursor:help;border-bottom:0;text-decoration-skip-ink:none}address{margin-bottom:1rem;font-style:normal;line-height:inherit}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}b,strong{font-weight:bolder}small{font-size:80%}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:#ed8d09;text-decoration:none;background-color:transparent}a:hover{color:#a46206;text-decoration:underline}a:not([href]):not([tabindex]){color:inherit;text-decoration:none}a:not([href]):not([tabindex]):focus,a:not([href]):not([tabindex]):hover{color:inherit;text-decoration:none}a:not([href]):not([tabindex]):focus{outline:0}code,kbd,pre,samp{font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;font-size:1em}pre{margin-top:0;margin-bottom:1rem;overflow:auto}figure{margin:0 0 1rem}img{vertical-align:middle;border-style:none}svg{overflow:hidden;vertical-align:middle}table{border-collapse:collapse}caption{padding-top:.75rem;padding-bottom:.75rem;color:#6c757d;text-align:left;caption-side:bottom}th{text-align:inherit}label{display:inline-block;margin-bottom:.5rem}button{border-radius:0}button:focus{outline:1px dotted;outline:5px auto -webkit-focus-ring-color}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,input{overflow:visible}button,select{text-transform:none}select{word-wrap:normal}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]:not(:disabled),[type=reset]:not(:disabled),[type=submit]:not(:disabled),button:not(:disabled){cursor:pointer}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{padding:0;border-style:none}input[type=checkbox],input[type=radio]{box-sizing:border-box;padding:0}input[type=date],input[type=datetime-local],input[type=month],input[type=time]{-webkit-appearance:listbox}textarea{overflow:auto;resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{display:block;width:100%;max-width:100%;padding:0;margin-bottom:.5rem;font-size:1.5rem;line-height:inherit;color:inherit;white-space:normal}progress{vertical-align:baseline}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:none}[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}output{display:inline-block}summary{display:list-item;cursor:pointer}template{display:none}[hidden]{display:none!important}.h1,.h2,.h3,.h4,.h5,.h6,h1,h2,h3,h4,h5,h6{margin-bottom:.5rem;font-weight:500;line-height:1.2;color:#495057}.h1,h1{font-size:2.5rem}.h2,h2{font-size:2rem}.h3,h3{font-size:1.75rem}.h4,h4{font-size:1.5rem}.h5,h5{font-size:1.25rem}.h6,h6{font-size:1rem}.lead{font-size:1.25rem;font-weight:300}.display-1{font-size:6rem;font-weight:300;line-height:1.2}.display-2{font-size:5.5rem;font-weight:300;line-height:1.2}.display-3{font-size:4.5rem;font-weight:300;line-height:1.2}.display-4{font-size:3.5rem;font-weight:300;line-height:1.2}hr{margin-top:1rem;margin-bottom:1rem;border:0;border-top:1px solid rgba(34,34,34,.1)}.small,small{font-size:80%;font-weight:400}.mark,mark{padding:.2em;background-color:#fffcef}.list-unstyled{padding-left:0;list-style:none}.list-inline{padding-left:0;list-style:none}.list-inline-item{display:inline-block}.list-inline-item:not(:last-child){margin-right:.5rem}.initialism{font-size:90%;text-transform:uppercase}.blockquote{margin-bottom:1rem;font-size:1.25rem}.blockquote-footer{display:block;font-size:80%;color:#6c757d}.blockquote-footer::before{content:"\2014\00A0"}.img-fluid{max-width:100%;height:auto}.img-thumbnail{padding:.25rem;background-color:#f2f0f0;border:1px solid #dee2e6;border-radius:1.5rem;max-width:100%;height:auto}.figure{display:inline-block}.figure-img{margin-bottom:.5rem;line-height:1}.figure-caption{font-size:90%;color:#6c757d}code{font-size:87.5%;color:#e83e8c;word-break:break-word}a>code{color:inherit}kbd{padding:.2rem .4rem;font-size:87.5%;color:#fff;background-color:#212529;border-radius:1rem}kbd kbd{padding:0;font-size:100%;font-weight:700}pre{display:block;font-size:87.5%;color:#212529}pre code{font-size:inherit;color:inherit;word-break:normal}.pre-scrollable{max-height:340px;overflow-y:scroll}.container{width:100%;padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}@media (min-width:576px){.container{max-width:540px}}@media (min-width:768px){.container{max-width:720px}}@media (min-width:992px){.container{max-width:960px}}@media (min-width:1200px){.container{max-width:1140px}}.container-fluid{width:100%;padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}.row{display:flex;flex-wrap:wrap;margin-right:-15px;margin-left:-15px}.no-gutters{margin-right:0;margin-left:0}.no-gutters>.col,.no-gutters>[class*=col-]{padding-right:0;padding-left:0}.col,.col-1,.col-10,.col-11,.col-12,.col-2,.col-3,.col-4,.col-5,.col-6,.col-7,.col-8,.col-9,.col-auto,.col-lg,.col-lg-1,.col-lg-10,.col-lg-11,.col-lg-12,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9,.col-lg-auto,.col-md,.col-md-1,.col-md-10,.col-md-11,.col-md-12,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9,.col-md-auto,.col-sm,.col-sm-1,.col-sm-10,.col-sm-11,.col-sm-12,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9,.col-sm-auto,.col-xl,.col-xl-1,.col-xl-10,.col-xl-11,.col-xl-12,.col-xl-2,.col-xl-3,.col-xl-4,.col-xl-5,.col-xl-6,.col-xl-7,.col-xl-8,.col-xl-9,.col-xl-auto{position:relative;width:100%;padding-right:15px;padding-left:15px}.col{flex-basis:0;flex-grow:1;max-width:100%}.col-auto{flex:0 0 auto;width:auto;max-width:100%}.col-1{flex:0 0 8.33333%;max-width:8.33333%}.col-2{flex:0 0 16.66667%;max-width:16.66667%}.col-3{flex:0 0 25%;max-width:25%}.col-4{flex:0 0 33.33333%;max-width:33.33333%}.col-5{flex:0 0 41.66667%;max-width:41.66667%}.col-6{flex:0 0 50%;max-width:50%}.col-7{flex:0 0 58.33333%;max-width:58.33333%}.col-8{flex:0 0 66.66667%;max-width:66.66667%}.col-9{flex:0 0 75%;max-width:75%}.col-10{flex:0 0 83.33333%;max-width:83.33333%}.col-11{flex:0 0 91.66667%;max-width:91.66667%}.col-12{flex:0 0 100%;max-width:100%}.order-first{order:-1}.order-last{order:13}.order-0{order:0}.order-1{order:1}.order-2{order:2}.order-3{order:3}.order-4{order:4}.order-5{order:5}.order-6{order:6}.order-7{order:7}.order-8{order:8}.order-9{order:9}.order-10{order:10}.order-11{order:11}.order-12{order:12}.offset-1{margin-left:8.33333%}.offset-2{margin-left:16.66667%}.offset-3{margin-left:25%}.offset-4{margin-left:33.33333%}.offset-5{margin-left:41.66667%}.offset-6{margin-left:50%}.offset-7{margin-left:58.33333%}.offset-8{margin-left:66.66667%}.offset-9{margin-left:75%}.offset-10{margin-left:83.33333%}.offset-11{margin-left:91.66667%}@media (min-width:576px){.col-sm{flex-basis:0;flex-grow:1;max-width:100%}.col-sm-auto{flex:0 0 auto;width:auto;max-width:100%}.col-sm-1{flex:0 0 8.33333%;max-width:8.33333%}.col-sm-2{flex:0 0 16.66667%;max-width:16.66667%}.col-sm-3{flex:0 0 25%;max-width:25%}.col-sm-4{flex:0 0 33.33333%;max-width:33.33333%}.col-sm-5{flex:0 0 41.66667%;max-width:41.66667%}.col-sm-6{flex:0 0 50%;max-width:50%}.col-sm-7{flex:0 0 58.33333%;max-width:58.33333%}.col-sm-8{flex:0 0 66.66667%;max-width:66.66667%}.col-sm-9{flex:0 0 75%;max-width:75%}.col-sm-10{flex:0 0 83.33333%;max-width:83.33333%}.col-sm-11{flex:0 0 91.66667%;max-width:91.66667%}.col-sm-12{flex:0 0 100%;max-width:100%}.order-sm-first{order:-1}.order-sm-last{order:13}.order-sm-0{order:0}.order-sm-1{order:1}.order-sm-2{order:2}.order-sm-3{order:3}.order-sm-4{order:4}.order-sm-5{order:5}.order-sm-6{order:6}.order-sm-7{order:7}.order-sm-8{order:8}.order-sm-9{order:9}.order-sm-10{order:10}.order-sm-11{order:11}.order-sm-12{order:12}.offset-sm-0{margin-left:0}.offset-sm-1{margin-left:8.33333%}.offset-sm-2{margin-left:16.66667%}.offset-sm-3{margin-left:25%}.offset-sm-4{margin-left:33.33333%}.offset-sm-5{margin-left:41.66667%}.offset-sm-6{margin-left:50%}.offset-sm-7{margin-left:58.33333%}.offset-sm-8{margin-left:66.66667%}.offset-sm-9{margin-left:75%}.offset-sm-10{margin-left:83.33333%}.offset-sm-11{margin-left:91.66667%}}@media (min-width:768px){.col-md{flex-basis:0;flex-grow:1;max-width:100%}.col-md-auto{flex:0 0 auto;width:auto;max-width:100%}.col-md-1{flex:0 0 8.33333%;max-width:8.33333%}.col-md-2{flex:0 0 16.66667%;max-width:16.66667%}.col-md-3{flex:0 0 25%;max-width:25%}.col-md-4{flex:0 0 33.33333%;max-width:33.33333%}.col-md-5{flex:0 0 41.66667%;max-width:41.66667%}.col-md-6{flex:0 0 50%;max-width:50%}.col-md-7{flex:0 0 58.33333%;max-width:58.33333%}.col-md-8{flex:0 0 66.66667%;max-width:66.66667%}.col-md-9{flex:0 0 75%;max-width:75%}.col-md-10{flex:0 0 83.33333%;max-width:83.33333%}.col-md-11{flex:0 0 91.66667%;max-width:91.66667%}.col-md-12{flex:0 0 100%;max-width:100%}.order-md-first{order:-1}.order-md-last{order:13}.order-md-0{order:0}.order-md-1{order:1}.order-md-2{order:2}.order-md-3{order:3}.order-md-4{order:4}.order-md-5{order:5}.order-md-6{order:6}.order-md-7{order:7}.order-md-8{order:8}.order-md-9{order:9}.order-md-10{order:10}.order-md-11{order:11}.order-md-12{order:12}.offset-md-0{margin-left:0}.offset-md-1{margin-left:8.33333%}.offset-md-2{margin-left:16.66667%}.offset-md-3{margin-left:25%}.offset-md-4{margin-left:33.33333%}.offset-md-5{margin-left:41.66667%}.offset-md-6{margin-left:50%}.offset-md-7{margin-left:58.33333%}.offset-md-8{margin-left:66.66667%}.offset-md-9{margin-left:75%}.offset-md-10{margin-left:83.33333%}.offset-md-11{margin-left:91.66667%}}@media (min-width:992px){.col-lg{flex-basis:0;flex-grow:1;max-width:100%}.col-lg-auto{flex:0 0 auto;width:auto;max-width:100%}.col-lg-1{flex:0 0 8.33333%;max-width:8.33333%}.col-lg-2{flex:0 0 16.66667%;max-width:16.66667%}.col-lg-3{flex:0 0 25%;max-width:25%}.col-lg-4{flex:0 0 33.33333%;max-width:33.33333%}.col-lg-5{flex:0 0 41.66667%;max-width:41.66667%}.col-lg-6{flex:0 0 50%;max-width:50%}.col-lg-7{flex:0 0 58.33333%;max-width:58.33333%}.col-lg-8{flex:0 0 66.66667%;max-width:66.66667%}.col-lg-9{flex:0 0 75%;max-width:75%}.col-lg-10{flex:0 0 83.33333%;max-width:83.33333%}.col-lg-11{flex:0 0 91.66667%;max-width:91.66667%}.col-lg-12{flex:0 0 100%;max-width:100%}.order-lg-first{order:-1}.order-lg-last{order:13}.order-lg-0{order:0}.order-lg-1{order:1}.order-lg-2{order:2}.order-lg-3{order:3}.order-lg-4{order:4}.order-lg-5{order:5}.order-lg-6{order:6}.order-lg-7{order:7}.order-lg-8{order:8}.order-lg-9{order:9}.order-lg-10{order:10}.order-lg-11{order:11}.order-lg-12{order:12}.offset-lg-0{margin-left:0}.offset-lg-1{margin-left:8.33333%}.offset-lg-2{margin-left:16.66667%}.offset-lg-3{margin-left:25%}.offset-lg-4{margin-left:33.33333%}.offset-lg-5{margin-left:41.66667%}.offset-lg-6{margin-left:50%}.offset-lg-7{margin-left:58.33333%}.offset-lg-8{margin-left:66.66667%}.offset-lg-9{margin-left:75%}.offset-lg-10{margin-left:83.33333%}.offset-lg-11{margin-left:91.66667%}}@media (min-width:1200px){.col-xl{flex-basis:0;flex-grow:1;max-width:100%}.col-xl-auto{flex:0 0 auto;width:auto;max-width:100%}.col-xl-1{flex:0 0 8.33333%;max-width:8.33333%}.col-xl-2{flex:0 0 16.66667%;max-width:16.66667%}.col-xl-3{flex:0 0 25%;max-width:25%}.col-xl-4{flex:0 0 33.33333%;max-width:33.33333%}.col-xl-5{flex:0 0 41.66667%;max-width:41.66667%}.col-xl-6{flex:0 0 50%;max-width:50%}.col-xl-7{flex:0 0 58.33333%;max-width:58.33333%}.col-xl-8{flex:0 0 66.66667%;max-width:66.66667%}.col-xl-9{flex:0 0 75%;max-width:75%}.col-xl-10{flex:0 0 83.33333%;max-width:83.33333%}.col-xl-11{flex:0 0 91.66667%;max-width:91.66667%}.col-xl-12{flex:0 0 100%;max-width:100%}.order-xl-first{order:-1}.order-xl-last{order:13}.order-xl-0{order:0}.order-xl-1{order:1}.order-xl-2{order:2}.order-xl-3{order:3}.order-xl-4{order:4}.order-xl-5{order:5}.order-xl-6{order:6}.order-xl-7{order:7}.order-xl-8{order:8}.order-xl-9{order:9}.order-xl-10{order:10}.order-xl-11{order:11}.order-xl-12{order:12}.offset-xl-0{margin-left:0}.offset-xl-1{margin-left:8.33333%}.offset-xl-2{margin-left:16.66667%}.offset-xl-3{margin-left:25%}.offset-xl-4{margin-left:33.33333%}.offset-xl-5{margin-left:41.66667%}.offset-xl-6{margin-left:50%}.offset-xl-7{margin-left:58.33333%}.offset-xl-8{margin-left:66.66667%}.offset-xl-9{margin-left:75%}.offset-xl-10{margin-left:83.33333%}.offset-xl-11{margin-left:91.66667%}}.table{width:100%;margin-bottom:1rem;color:#495057}.table td,.table th{padding:.75rem;vertical-align:top;border-top:1px solid #495057}.table thead th{vertical-align:bottom;border-bottom:2px solid #495057}.table tbody+tbody{border-top:2px solid #495057}.table-sm td,.table-sm th{padding:.3rem}.table-bordered{border:1px solid #495057}.table-bordered td,.table-bordered th{border:1px solid #495057}.table-bordered thead td,.table-bordered thead th{border-bottom-width:2px}.table-borderless tbody+tbody,.table-borderless td,.table-borderless th,.table-borderless thead th{border:0}.table-striped tbody tr:nth-of-type(odd){background-color:rgba(34,34,34,.05)}.table-hover tbody tr:hover{color:#495057;background-color:rgba(34,34,34,.075)}.table-primary,.table-primary>td,.table-primary>th{background-color:#feefda}.table-primary tbody+tbody,.table-primary td,.table-primary th,.table-primary thead th{border-color:#fce1bb}.table-hover .table-primary:hover{background-color:#fde4c1}.table-hover .table-primary:hover>td,.table-hover .table-primary:hover>th{background-color:#fde4c1}.table-secondary,.table-secondary>td,.table-secondary>th{background-color:#ecf2ed}.table-secondary tbody+tbody,.table-secondary td,.table-secondary th,.table-secondary thead th{border-color:#dbe8dd}.table-hover .table-secondary:hover{background-color:#dde8df}.table-hover .table-secondary:hover>td,.table-hover .table-secondary:hover>th{background-color:#dde8df}.table-success,.table-success>td,.table-success>th{background-color:#c7cfc9}.table-success tbody+tbody,.table-success td,.table-success th,.table-success thead th{border-color:#97a79a}.table-hover .table-success:hover{background-color:#b9c3bc}.table-hover .table-success:hover>td,.table-hover .table-success:hover>th{background-color:#b9c3bc}.table-info,.table-info>td,.table-info>th{background-color:#ccf1d2}.table-info tbody+tbody,.table-info td,.table-info th,.table-info thead th{border-color:#a0e6ac}.table-hover .table-info:hover{background-color:#b8ecc0}.table-hover .table-info:hover>td,.table-hover .table-info:hover>th{background-color:#b8ecc0}.table-warning,.table-warning>td,.table-warning>th{background-color:#ffeeba}.table-warning tbody+tbody,.table-warning td,.table-warning th,.table-warning thead th{border-color:#ffdf7e}.table-hover .table-warning:hover{background-color:#ffe8a1}.table-hover .table-warning:hover>td,.table-hover .table-warning:hover>th{background-color:#ffe8a1}.table-danger,.table-danger>td,.table-danger>th{background-color:#fadfba}.table-danger tbody+tbody,.table-danger td,.table-danger th,.table-danger thead th{border-color:#f6c47f}.table-hover .table-danger:hover{background-color:#f8d4a2}.table-hover .table-danger:hover>td,.table-hover .table-danger:hover>th{background-color:#f8d4a2}.table-light,.table-light>td,.table-light>th{background-color:#fdfdfe}.table-light tbody+tbody,.table-light td,.table-light th,.table-light thead th{border-color:#fbfcfc}.table-hover .table-light:hover{background-color:#ececf6}.table-hover .table-light:hover>td,.table-hover .table-light:hover>th{background-color:#ececf6}.table-dark,.table-dark>td,.table-dark>th{background-color:#c6c8ca}.table-dark tbody+tbody,.table-dark td,.table-dark th,.table-dark thead th{border-color:#95999c}.table-hover .table-dark:hover{background-color:#b9bbbe}.table-hover .table-dark:hover>td,.table-hover .table-dark:hover>th{background-color:#b9bbbe}.table-active,.table-active>td,.table-active>th{background-color:rgba(34,34,34,.075)}.table-hover .table-active:hover{background-color:rgba(21,21,21,.075)}.table-hover .table-active:hover>td,.table-hover .table-active:hover>th{background-color:rgba(21,21,21,.075)}.table .thead-dark th{color:#fff;background-color:#343a40;border-color:#454d55}.table .thead-light th{color:#495057;background-color:#e9ecef;border-color:#495057}.table-dark{color:#fff;background-color:#343a40}.table-dark td,.table-dark th,.table-dark thead th{border-color:#454d55}.table-dark.table-bordered{border:0}.table-dark.table-striped tbody tr:nth-of-type(odd){background-color:rgba(255,255,255,.05)}.table-dark.table-hover tbody tr:hover{color:#fff;background-color:rgba(255,255,255,.075)}@media (max-width:575.98px){.table-responsive-sm{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch}.table-responsive-sm>.table-bordered{border:0}}@media (max-width:767.98px){.table-responsive-md{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch}.table-responsive-md>.table-bordered{border:0}}@media (max-width:991.98px){.table-responsive-lg{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch}.table-responsive-lg>.table-bordered{border:0}}@media (max-width:1199.98px){.table-responsive-xl{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch}.table-responsive-xl>.table-bordered{border:0}}.table-responsive{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch}.table-responsive>.table-bordered{border:0}.form-control{display:block;width:100%;height:calc(1.5em + .75rem + 2px);padding:.375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5;color:#495057;background-color:#fff;background-clip:padding-box;border:1px solid #ced4da;border-radius:1.5rem;transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-control{transition:none}}.form-control::-ms-expand{background-color:transparent;border:0}.form-control:focus{color:#495057;background-color:#fff;border-color:#fffbf7;outline:0;box-shadow:0 0 0 .2rem rgba(250,197,124,.75)}.form-control::placeholder{color:#6c757d;opacity:1}.form-control:disabled,.form-control[readonly]{background-color:#e9ecef;opacity:1}select.form-control:focus::-ms-value{color:#495057;background-color:#fff}.form-control-file,.form-control-range{display:block;width:100%}.col-form-label{padding-top:calc(.375rem + 1px);padding-bottom:calc(.375rem + 1px);margin-bottom:0;font-size:inherit;line-height:1.5}.col-form-label-lg{padding-top:calc(.5rem + 1px);padding-bottom:calc(.5rem + 1px);font-size:1.25rem;line-height:1.5}.col-form-label-sm{padding-top:calc(.25rem + 1px);padding-bottom:calc(.25rem + 1px);font-size:.875rem;line-height:1.5}.form-control-plaintext{display:block;width:100%;padding-top:.375rem;padding-bottom:.375rem;margin-bottom:0;line-height:1.5;color:#495057;background-color:transparent;border:solid transparent;border-width:1px 0}.form-control-plaintext.form-control-lg,.form-control-plaintext.form-control-sm{padding-right:0;padding-left:0}.form-control-sm{height:calc(1.5em + .5rem + 2px);padding:.25rem .5rem;font-size:.875rem;line-height:1.5;border-radius:1rem}.form-control-lg{height:calc(1.5em + 1rem + 2px);padding:.5rem 1rem;font-size:1.25rem;line-height:1.5;border-radius:1.5rem}select.form-control[multiple],select.form-control[size]{height:auto}textarea.form-control{height:auto}.form-group{margin-bottom:1rem}.form-text{display:block;margin-top:.25rem}.form-row{display:flex;flex-wrap:wrap;margin-right:-5px;margin-left:-5px}.form-row>.col,.form-row>[class*=col-]{padding-right:5px;padding-left:5px}.form-check{position:relative;display:block;padding-left:1.25rem}.form-check-input{position:absolute;margin-top:.3rem;margin-left:-1.25rem}.form-check-input:disabled~.form-check-label{color:#6c757d}.form-check-label{margin-bottom:0}.form-check-inline{display:inline-flex;align-items:center;padding-left:0;margin-right:.75rem}.form-check-inline .form-check-input{position:static;margin-top:0;margin-right:.3125rem;margin-left:0}.valid-feedback{display:none;width:100%;margin-top:.25rem;font-size:80%;color:#49ce5f}.valid-tooltip{position:absolute;top:100%;z-index:5;display:none;max-width:100%;padding:.25rem .5rem;margin-top:.1rem;font-size:.875rem;line-height:1.5;color:#212529;background-color:rgba(73,206,95,.9);border-radius:1.5rem}.form-control.is-valid,.was-validated .form-control:valid{border-color:#49ce5f;padding-right:calc(1.5em + .75rem);background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%2349ce5f' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:center right calc(.375em + .1875rem);background-size:calc(.75em + .375rem) calc(.75em + .375rem)}.form-control.is-valid:focus,.was-validated .form-control:valid:focus{border-color:#49ce5f;box-shadow:0 0 0 .2rem rgba(73,206,95,.25)}.form-control.is-valid~.valid-feedback,.form-control.is-valid~.valid-tooltip,.was-validated .form-control:valid~.valid-feedback,.was-validated .form-control:valid~.valid-tooltip{display:block}.was-validated textarea.form-control:valid,textarea.form-control.is-valid{padding-right:calc(1.5em + .75rem);background-position:top calc(.375em + .1875rem) right calc(.375em + .1875rem)}.custom-select.is-valid,.was-validated .custom-select:valid{border-color:#49ce5f;padding-right:calc((1em + .75rem) * 3 / 4 + 1.75rem);background:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 4 5'%3e%3cpath fill='%23343a40' d='M2 0L0 2h4zm0 5L0 3h4z'/%3e%3c/svg%3e") no-repeat right .75rem center/8px 10px,url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%2349ce5f' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e") #fff no-repeat center right 1.75rem/calc(.75em + .375rem) calc(.75em + .375rem)}.custom-select.is-valid:focus,.was-validated .custom-select:valid:focus{border-color:#49ce5f;box-shadow:0 0 0 .2rem rgba(73,206,95,.25)}.custom-select.is-valid~.valid-feedback,.custom-select.is-valid~.valid-tooltip,.was-validated .custom-select:valid~.valid-feedback,.was-validated .custom-select:valid~.valid-tooltip{display:block}.form-control-file.is-valid~.valid-feedback,.form-control-file.is-valid~.valid-tooltip,.was-validated .form-control-file:valid~.valid-feedback,.was-validated .form-control-file:valid~.valid-tooltip{display:block}.form-check-input.is-valid~.form-check-label,.was-validated .form-check-input:valid~.form-check-label{color:#49ce5f}.form-check-input.is-valid~.valid-feedback,.form-check-input.is-valid~.valid-tooltip,.was-validated .form-check-input:valid~.valid-feedback,.was-validated .form-check-input:valid~.valid-tooltip{display:block}.custom-control-input.is-valid~.custom-control-label,.was-validated .custom-control-input:valid~.custom-control-label{color:#49ce5f}.custom-control-input.is-valid~.custom-control-label::before,.was-validated .custom-control-input:valid~.custom-control-label::before{border-color:#49ce5f}.custom-control-input.is-valid~.valid-feedback,.custom-control-input.is-valid~.valid-tooltip,.was-validated .custom-control-input:valid~.valid-feedback,.was-validated .custom-control-input:valid~.valid-tooltip{display:block}.custom-control-input.is-valid:checked~.custom-control-label::before,.was-validated .custom-control-input:valid:checked~.custom-control-label::before{border-color:#71d982;background-color:#71d982}.custom-control-input.is-valid:focus~.custom-control-label::before,.was-validated .custom-control-input:valid:focus~.custom-control-label::before{box-shadow:0 0 0 .2rem rgba(73,206,95,.25)}.custom-control-input.is-valid:focus:not(:checked)~.custom-control-label::before,.was-validated .custom-control-input:valid:focus:not(:checked)~.custom-control-label::before{border-color:#49ce5f}.custom-file-input.is-valid~.custom-file-label,.was-validated .custom-file-input:valid~.custom-file-label{border-color:#49ce5f}.custom-file-input.is-valid~.valid-feedback,.custom-file-input.is-valid~.valid-tooltip,.was-validated .custom-file-input:valid~.valid-feedback,.was-validated .custom-file-input:valid~.valid-tooltip{display:block}.custom-file-input.is-valid:focus~.custom-file-label,.was-validated .custom-file-input:valid:focus~.custom-file-label{border-color:#49ce5f;box-shadow:0 0 0 .2rem rgba(73,206,95,.25)}.invalid-feedback{display:none;width:100%;margin-top:.25rem;font-size:80%;color:#ed8d09}.invalid-tooltip{position:absolute;top:100%;z-index:5;display:none;max-width:100%;padding:.25rem .5rem;margin-top:.1rem;font-size:.875rem;line-height:1.5;color:#212529;background-color:rgba(237,141,9,.9);border-radius:1.5rem}.form-control.is-invalid,.was-validated .form-control:invalid{border-color:#ed8d09;padding-right:calc(1.5em + .75rem);background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='%23ed8d09' viewBox='-2 -2 7 7'%3e%3cpath stroke='%23ed8d09' d='M0 0l3 3m0-3L0 3'/%3e%3ccircle r='.5'/%3e%3ccircle cx='3' r='.5'/%3e%3ccircle cy='3' r='.5'/%3e%3ccircle cx='3' cy='3' r='.5'/%3e%3c/svg%3E");background-repeat:no-repeat;background-position:center right calc(.375em + .1875rem);background-size:calc(.75em + .375rem) calc(.75em + .375rem)}.form-control.is-invalid:focus,.was-validated .form-control:invalid:focus{border-color:#ed8d09;box-shadow:0 0 0 .2rem rgba(237,141,9,.25)}.form-control.is-invalid~.invalid-feedback,.form-control.is-invalid~.invalid-tooltip,.was-validated .form-control:invalid~.invalid-feedback,.was-validated .form-control:invalid~.invalid-tooltip{display:block}.was-validated textarea.form-control:invalid,textarea.form-control.is-invalid{padding-right:calc(1.5em + .75rem);background-position:top calc(.375em + .1875rem) right calc(.375em + .1875rem)}.custom-select.is-invalid,.was-validated .custom-select:invalid{border-color:#ed8d09;padding-right:calc((1em + .75rem) * 3 / 4 + 1.75rem);background:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 4 5'%3e%3cpath fill='%23343a40' d='M2 0L0 2h4zm0 5L0 3h4z'/%3e%3c/svg%3e") no-repeat right .75rem center/8px 10px,url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='%23ed8d09' viewBox='-2 -2 7 7'%3e%3cpath stroke='%23ed8d09' d='M0 0l3 3m0-3L0 3'/%3e%3ccircle r='.5'/%3e%3ccircle cx='3' r='.5'/%3e%3ccircle cy='3' r='.5'/%3e%3ccircle cx='3' cy='3' r='.5'/%3e%3c/svg%3E") #fff no-repeat center right 1.75rem/calc(.75em + .375rem) calc(.75em + .375rem)}.custom-select.is-invalid:focus,.was-validated .custom-select:invalid:focus{border-color:#ed8d09;box-shadow:0 0 0 .2rem rgba(237,141,9,.25)}.custom-select.is-invalid~.invalid-feedback,.custom-select.is-invalid~.invalid-tooltip,.was-validated .custom-select:invalid~.invalid-feedback,.was-validated .custom-select:invalid~.invalid-tooltip{display:block}.form-control-file.is-invalid~.invalid-feedback,.form-control-file.is-invalid~.invalid-tooltip,.was-validated .form-control-file:invalid~.invalid-feedback,.was-validated .form-control-file:invalid~.invalid-tooltip{display:block}.form-check-input.is-invalid~.form-check-label,.was-validated .form-check-input:invalid~.form-check-label{color:#ed8d09}.form-check-input.is-invalid~.invalid-feedback,.form-check-input.is-invalid~.invalid-tooltip,.was-validated .form-check-input:invalid~.invalid-feedback,.was-validated .form-check-input:invalid~.invalid-tooltip{display:block}.custom-control-input.is-invalid~.custom-control-label,.was-validated .custom-control-input:invalid~.custom-control-label{color:#ed8d09}.custom-control-input.is-invalid~.custom-control-label::before,.was-validated .custom-control-input:invalid~.custom-control-label::before{border-color:#ed8d09}.custom-control-input.is-invalid~.invalid-feedback,.custom-control-input.is-invalid~.invalid-tooltip,.was-validated .custom-control-input:invalid~.invalid-feedback,.was-validated .custom-control-input:invalid~.invalid-tooltip{display:block}.custom-control-input.is-invalid:checked~.custom-control-label::before,.was-validated .custom-control-input:invalid:checked~.custom-control-label::before{border-color:#f7a432;background-color:#f7a432}.custom-control-input.is-invalid:focus~.custom-control-label::before,.was-validated .custom-control-input:invalid:focus~.custom-control-label::before{box-shadow:0 0 0 .2rem rgba(237,141,9,.25)}.custom-control-input.is-invalid:focus:not(:checked)~.custom-control-label::before,.was-validated .custom-control-input:invalid:focus:not(:checked)~.custom-control-label::before{border-color:#ed8d09}.custom-file-input.is-invalid~.custom-file-label,.was-validated .custom-file-input:invalid~.custom-file-label{border-color:#ed8d09}.custom-file-input.is-invalid~.invalid-feedback,.custom-file-input.is-invalid~.invalid-tooltip,.was-validated .custom-file-input:invalid~.invalid-feedback,.was-validated .custom-file-input:invalid~.invalid-tooltip{display:block}.custom-file-input.is-invalid:focus~.custom-file-label,.was-validated .custom-file-input:invalid:focus~.custom-file-label{border-color:#ed8d09;box-shadow:0 0 0 .2rem rgba(237,141,9,.25)}.form-inline{display:flex;flex-flow:row wrap;align-items:center}.form-inline .form-check{width:100%}@media (min-width:576px){.form-inline label{display:flex;align-items:center;justify-content:center;margin-bottom:0}.form-inline .form-group{display:flex;flex:0 0 auto;flex-flow:row wrap;align-items:center;margin-bottom:0}.form-inline .form-control{display:inline-block;width:auto;vertical-align:middle}.form-inline .form-control-plaintext{display:inline-block}.form-inline .custom-select,.form-inline .input-group{width:auto}.form-inline .form-check{display:flex;align-items:center;justify-content:center;width:auto;padding-left:0}.form-inline .form-check-input{position:relative;flex-shrink:0;margin-top:0;margin-right:.25rem;margin-left:0}.form-inline .custom-control{align-items:center;justify-content:center}.form-inline .custom-control-label{margin-bottom:0}}.btn{display:inline-block;font-weight:400;color:#495057;text-align:center;vertical-align:middle;user-select:none;background-color:transparent;border:1px solid transparent;padding:.375rem .75rem;font-size:1rem;line-height:1.5;border-radius:1.5rem;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.btn{transition:none}}.btn:hover{color:#495057;text-decoration:none}.btn.focus,.btn:focus{outline:0;box-shadow:0 0 0 .2rem rgba(250,197,124,.75)}.btn.disabled,.btn:disabled{opacity:.65}a.btn.disabled,fieldset:disabled a.btn{pointer-events:none}.btn-primary{color:#212529;background-color:#fac57c;border-color:#fac57c}.btn-primary:hover{color:#212529;background-color:#f9b557;border-color:#f8af4b}.btn-primary.focus,.btn-primary:focus{box-shadow:0 0 0 .2rem rgba(217,173,112,.5)}.btn-primary.disabled,.btn-primary:disabled{color:#212529;background-color:#fac57c;border-color:#fac57c}.btn-primary:not(:disabled):not(.disabled).active,.btn-primary:not(:disabled):not(.disabled):active,.show>.btn-primary.dropdown-toggle{color:#212529;background-color:#f8af4b;border-color:#f8aa3f}.btn-primary:not(:disabled):not(.disabled).active:focus,.btn-primary:not(:disabled):not(.disabled):active:focus,.show>.btn-primary.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(217,173,112,.5)}.btn-secondary{color:#212529;background-color:#bad2be;border-color:#bad2be}.btn-secondary:hover{color:#212529;background-color:#a3c3a8;border-color:#9bbea1}.btn-secondary.focus,.btn-secondary:focus{box-shadow:0 0 0 .2rem rgba(163,184,168,.5)}.btn-secondary.disabled,.btn-secondary:disabled{color:#212529;background-color:#bad2be;border-color:#bad2be}.btn-secondary:not(:disabled):not(.disabled).active,.btn-secondary:not(:disabled):not(.disabled):active,.show>.btn-secondary.dropdown-toggle{color:#212529;background-color:#9bbea1;border-color:#93b99a}.btn-secondary:not(:disabled):not(.disabled).active:focus,.btn-secondary:not(:disabled):not(.disabled):active:focus,.show>.btn-secondary.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(163,184,168,.5)}.btn-success{color:#fff;background-color:#38553d;border-color:#38553d}.btn-success:hover{color:#fff;background-color:#293e2c;border-color:#243627}.btn-success.focus,.btn-success:focus{box-shadow:0 0 0 .2rem rgba(86,111,90,.5)}.btn-success.disabled,.btn-success:disabled{color:#fff;background-color:#38553d;border-color:#38553d}.btn-success:not(:disabled):not(.disabled).active,.btn-success:not(:disabled):not(.disabled):active,.show>.btn-success.dropdown-toggle{color:#fff;background-color:#243627;border-color:#1e2f21}.btn-success:not(:disabled):not(.disabled).active:focus,.btn-success:not(:disabled):not(.disabled):active:focus,.show>.btn-success.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(86,111,90,.5)}.btn-info{color:#212529;background-color:#49ce5f;border-color:#49ce5f}.btn-info:hover{color:#fff;background-color:#33be4a;border-color:#30b446}.btn-info.focus,.btn-info:focus{box-shadow:0 0 0 .2rem rgba(67,181,87,.5)}.btn-info.disabled,.btn-info:disabled{color:#212529;background-color:#49ce5f;border-color:#49ce5f}.btn-info:not(:disabled):not(.disabled).active,.btn-info:not(:disabled):not(.disabled):active,.show>.btn-info.dropdown-toggle{color:#fff;background-color:#30b446;border-color:#2eaa42}.btn-info:not(:disabled):not(.disabled).active:focus,.btn-info:not(:disabled):not(.disabled):active:focus,.show>.btn-info.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(67,181,87,.5)}.btn-warning{color:#212529;background-color:#ffc107;border-color:#ffc107}.btn-warning:hover{color:#212529;background-color:#e0a800;border-color:#d39e00}.btn-warning.focus,.btn-warning:focus{box-shadow:0 0 0 .2rem rgba(222,170,12,.5)}.btn-warning.disabled,.btn-warning:disabled{color:#212529;background-color:#ffc107;border-color:#ffc107}.btn-warning:not(:disabled):not(.disabled).active,.btn-warning:not(:disabled):not(.disabled):active,.show>.btn-warning.dropdown-toggle{color:#212529;background-color:#d39e00;border-color:#c69500}.btn-warning:not(:disabled):not(.disabled).active:focus,.btn-warning:not(:disabled):not(.disabled):active:focus,.show>.btn-warning.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(222,170,12,.5)}.btn-danger{color:#212529;background-color:#ed8d09;border-color:#ed8d09}.btn-danger:hover{color:#fff;background-color:#c97708;border-color:#bc7007}.btn-danger.focus,.btn-danger:focus{box-shadow:0 0 0 .2rem rgba(207,126,14,.5)}.btn-danger.disabled,.btn-danger:disabled{color:#212529;background-color:#ed8d09;border-color:#ed8d09}.btn-danger:not(:disabled):not(.disabled).active,.btn-danger:not(:disabled):not(.disabled):active,.show>.btn-danger.dropdown-toggle{color:#fff;background-color:#bc7007;border-color:#b06907}.btn-danger:not(:disabled):not(.disabled).active:focus,.btn-danger:not(:disabled):not(.disabled):active:focus,.show>.btn-danger.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(207,126,14,.5)}.btn-light{color:#212529;background-color:#f8f9fa;border-color:#f8f9fa}.btn-light:hover{color:#212529;background-color:#e2e6ea;border-color:#dae0e5}.btn-light.focus,.btn-light:focus{box-shadow:0 0 0 .2rem rgba(216,217,219,.5)}.btn-light.disabled,.btn-light:disabled{color:#212529;background-color:#f8f9fa;border-color:#f8f9fa}.btn-light:not(:disabled):not(.disabled).active,.btn-light:not(:disabled):not(.disabled):active,.show>.btn-light.dropdown-toggle{color:#212529;background-color:#dae0e5;border-color:#d3d9df}.btn-light:not(:disabled):not(.disabled).active:focus,.btn-light:not(:disabled):not(.disabled):active:focus,.show>.btn-light.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(216,217,219,.5)}.btn-dark{color:#fff;background-color:#343a40;border-color:#343a40}.btn-dark:hover{color:#fff;background-color:#23272b;border-color:#1d2124}.btn-dark.focus,.btn-dark:focus{box-shadow:0 0 0 .2rem rgba(82,88,93,.5)}.btn-dark.disabled,.btn-dark:disabled{color:#fff;background-color:#343a40;border-color:#343a40}.btn-dark:not(:disabled):not(.disabled).active,.btn-dark:not(:disabled):not(.disabled):active,.show>.btn-dark.dropdown-toggle{color:#fff;background-color:#1d2124;border-color:#171a1d}.btn-dark:not(:disabled):not(.disabled).active:focus,.btn-dark:not(:disabled):not(.disabled):active:focus,.show>.btn-dark.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(82,88,93,.5)}.btn-outline-primary{color:#fac57c;border-color:#fac57c}.btn-outline-primary:hover{color:#212529;background-color:#fac57c;border-color:#fac57c}.btn-outline-primary.focus,.btn-outline-primary:focus{box-shadow:0 0 0 .2rem rgba(250,197,124,.5)}.btn-outline-primary.disabled,.btn-outline-primary:disabled{color:#fac57c;background-color:transparent}.btn-outline-primary:not(:disabled):not(.disabled).active,.btn-outline-primary:not(:disabled):not(.disabled):active,.show>.btn-outline-primary.dropdown-toggle{color:#212529;background-color:#fac57c;border-color:#fac57c}.btn-outline-primary:not(:disabled):not(.disabled).active:focus,.btn-outline-primary:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-primary.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(250,197,124,.5)}.btn-outline-secondary{color:#bad2be;border-color:#bad2be}.btn-outline-secondary:hover{color:#212529;background-color:#bad2be;border-color:#bad2be}.btn-outline-secondary.focus,.btn-outline-secondary:focus{box-shadow:0 0 0 .2rem rgba(186,210,190,.5)}.btn-outline-secondary.disabled,.btn-outline-secondary:disabled{color:#bad2be;background-color:transparent}.btn-outline-secondary:not(:disabled):not(.disabled).active,.btn-outline-secondary:not(:disabled):not(.disabled):active,.show>.btn-outline-secondary.dropdown-toggle{color:#212529;background-color:#bad2be;border-color:#bad2be}.btn-outline-secondary:not(:disabled):not(.disabled).active:focus,.btn-outline-secondary:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-secondary.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(186,210,190,.5)}.btn-outline-success{color:#38553d;border-color:#38553d}.btn-outline-success:hover{color:#fff;background-color:#38553d;border-color:#38553d}.btn-outline-success.focus,.btn-outline-success:focus{box-shadow:0 0 0 .2rem rgba(56,85,61,.5)}.btn-outline-success.disabled,.btn-outline-success:disabled{color:#38553d;background-color:transparent}.btn-outline-success:not(:disabled):not(.disabled).active,.btn-outline-success:not(:disabled):not(.disabled):active,.show>.btn-outline-success.dropdown-toggle{color:#fff;background-color:#38553d;border-color:#38553d}.btn-outline-success:not(:disabled):not(.disabled).active:focus,.btn-outline-success:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-success.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(56,85,61,.5)}.btn-outline-info{color:#49ce5f;border-color:#49ce5f}.btn-outline-info:hover{color:#212529;background-color:#49ce5f;border-color:#49ce5f}.btn-outline-info.focus,.btn-outline-info:focus{box-shadow:0 0 0 .2rem rgba(73,206,95,.5)}.btn-outline-info.disabled,.btn-outline-info:disabled{color:#49ce5f;background-color:transparent}.btn-outline-info:not(:disabled):not(.disabled).active,.btn-outline-info:not(:disabled):not(.disabled):active,.show>.btn-outline-info.dropdown-toggle{color:#212529;background-color:#49ce5f;border-color:#49ce5f}.btn-outline-info:not(:disabled):not(.disabled).active:focus,.btn-outline-info:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-info.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(73,206,95,.5)}.btn-outline-warning{color:#ffc107;border-color:#ffc107}.btn-outline-warning:hover{color:#212529;background-color:#ffc107;border-color:#ffc107}.btn-outline-warning.focus,.btn-outline-warning:focus{box-shadow:0 0 0 .2rem rgba(255,193,7,.5)}.btn-outline-warning.disabled,.btn-outline-warning:disabled{color:#ffc107;background-color:transparent}.btn-outline-warning:not(:disabled):not(.disabled).active,.btn-outline-warning:not(:disabled):not(.disabled):active,.show>.btn-outline-warning.dropdown-toggle{color:#212529;background-color:#ffc107;border-color:#ffc107}.btn-outline-warning:not(:disabled):not(.disabled).active:focus,.btn-outline-warning:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-warning.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(255,193,7,.5)}.btn-outline-danger{color:#ed8d09;border-color:#ed8d09}.btn-outline-danger:hover{color:#212529;background-color:#ed8d09;border-color:#ed8d09}.btn-outline-danger.focus,.btn-outline-danger:focus{box-shadow:0 0 0 .2rem rgba(237,141,9,.5)}.btn-outline-danger.disabled,.btn-outline-danger:disabled{color:#ed8d09;background-color:transparent}.btn-outline-danger:not(:disabled):not(.disabled).active,.btn-outline-danger:not(:disabled):not(.disabled):active,.show>.btn-outline-danger.dropdown-toggle{color:#212529;background-color:#ed8d09;border-color:#ed8d09}.btn-outline-danger:not(:disabled):not(.disabled).active:focus,.btn-outline-danger:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-danger.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(237,141,9,.5)}.btn-outline-light{color:#f8f9fa;border-color:#f8f9fa}.btn-outline-light:hover{color:#212529;background-color:#f8f9fa;border-color:#f8f9fa}.btn-outline-light.focus,.btn-outline-light:focus{box-shadow:0 0 0 .2rem rgba(248,249,250,.5)}.btn-outline-light.disabled,.btn-outline-light:disabled{color:#f8f9fa;background-color:transparent}.btn-outline-light:not(:disabled):not(.disabled).active,.btn-outline-light:not(:disabled):not(.disabled):active,.show>.btn-outline-light.dropdown-toggle{color:#212529;background-color:#f8f9fa;border-color:#f8f9fa}.btn-outline-light:not(:disabled):not(.disabled).active:focus,.btn-outline-light:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-light.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(248,249,250,.5)}.btn-outline-dark{color:#343a40;border-color:#343a40}.btn-outline-dark:hover{color:#fff;background-color:#343a40;border-color:#343a40}.btn-outline-dark.focus,.btn-outline-dark:focus{box-shadow:0 0 0 .2rem rgba(52,58,64,.5)}.btn-outline-dark.disabled,.btn-outline-dark:disabled{color:#343a40;background-color:transparent}.btn-outline-dark:not(:disabled):not(.disabled).active,.btn-outline-dark:not(:disabled):not(.disabled):active,.show>.btn-outline-dark.dropdown-toggle{color:#fff;background-color:#343a40;border-color:#343a40}.btn-outline-dark:not(:disabled):not(.disabled).active:focus,.btn-outline-dark:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-dark.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(52,58,64,.5)}.btn-link{font-weight:400;color:#ed8d09;text-decoration:none}.btn-link:hover{color:#a46206;text-decoration:underline}.btn-link.focus,.btn-link:focus{text-decoration:underline;box-shadow:none}.btn-link.disabled,.btn-link:disabled{color:#6c757d;pointer-events:none}.btn-group-lg>.btn,.btn-lg{padding:.5rem 1rem;font-size:1.25rem;line-height:1.5;border-radius:1.5rem}.btn-group-sm>.btn,.btn-sm{padding:.25rem .5rem;font-size:.875rem;line-height:1.5;border-radius:1rem}.btn-block{display:block;width:100%}.btn-block+.btn-block{margin-top:.5rem}input[type=button].btn-block,input[type=reset].btn-block,input[type=submit].btn-block{width:100%}.fade{transition:opacity .15s linear}@media (prefers-reduced-motion:reduce){.fade{transition:none}}.fade:not(.show){opacity:0}.collapse:not(.show){display:none}.collapsing{position:relative;height:0;overflow:hidden;transition:height .35s ease}@media (prefers-reduced-motion:reduce){.collapsing{transition:none}}.dropdown,.dropleft,.dropright,.dropup{position:relative}.dropdown-toggle{white-space:nowrap}.dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:.3em solid;border-right:.3em solid transparent;border-bottom:0;border-left:.3em solid transparent}.dropdown-toggle:empty::after{margin-left:0}.dropdown-menu{position:absolute;top:100%;left:0;z-index:1000;display:none;float:left;min-width:10rem;padding:.5rem 0;margin:.125rem 0 0;font-size:1rem;color:#495057;text-align:left;list-style:none;background-color:#fff;background-clip:padding-box;border:1px solid rgba(34,34,34,.15);border-radius:1.5rem}.dropdown-menu-left{right:auto;left:0}.dropdown-menu-right{right:0;left:auto}@media (min-width:576px){.dropdown-menu-sm-left{right:auto;left:0}.dropdown-menu-sm-right{right:0;left:auto}}@media (min-width:768px){.dropdown-menu-md-left{right:auto;left:0}.dropdown-menu-md-right{right:0;left:auto}}@media (min-width:992px){.dropdown-menu-lg-left{right:auto;left:0}.dropdown-menu-lg-right{right:0;left:auto}}@media (min-width:1200px){.dropdown-menu-xl-left{right:auto;left:0}.dropdown-menu-xl-right{right:0;left:auto}}.dropup .dropdown-menu{top:auto;bottom:100%;margin-top:0;margin-bottom:.125rem}.dropup .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:0;border-right:.3em solid transparent;border-bottom:.3em solid;border-left:.3em solid transparent}.dropup .dropdown-toggle:empty::after{margin-left:0}.dropright .dropdown-menu{top:0;right:auto;left:100%;margin-top:0;margin-left:.125rem}.dropright .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:.3em solid transparent;border-right:0;border-bottom:.3em solid transparent;border-left:.3em solid}.dropright .dropdown-toggle:empty::after{margin-left:0}.dropright .dropdown-toggle::after{vertical-align:0}.dropleft .dropdown-menu{top:0;right:100%;left:auto;margin-top:0;margin-right:.125rem}.dropleft .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:""}.dropleft .dropdown-toggle::after{display:none}.dropleft .dropdown-toggle::before{display:inline-block;margin-right:.255em;vertical-align:.255em;content:"";border-top:.3em solid transparent;border-right:.3em solid;border-bottom:.3em solid transparent}.dropleft .dropdown-toggle:empty::after{margin-left:0}.dropleft .dropdown-toggle::before{vertical-align:0}.dropdown-menu[x-placement^=bottom],.dropdown-menu[x-placement^=left],.dropdown-menu[x-placement^=right],.dropdown-menu[x-placement^=top]{right:auto;bottom:auto}.dropdown-divider{height:0;margin:.5rem 0;overflow:hidden;border-top:1px solid #e9ecef}.dropdown-item{display:block;width:100%;padding:.25rem 1.5rem;clear:both;font-weight:400;color:#212529;text-align:inherit;white-space:nowrap;background-color:transparent;border:0}.dropdown-item:focus,.dropdown-item:hover{color:#16181b;text-decoration:none;background-color:#f8f9fa}.dropdown-item.active,.dropdown-item:active{color:#fff;text-decoration:none;background-color:#fac57c}.dropdown-item.disabled,.dropdown-item:disabled{color:#6c757d;pointer-events:none;background-color:transparent}.dropdown-menu.show{display:block}.dropdown-header{display:block;padding:.5rem 1.5rem;margin-bottom:0;font-size:.875rem;color:#6c757d;white-space:nowrap}.dropdown-item-text{display:block;padding:.25rem 1.5rem;color:#212529}.btn-group,.btn-group-vertical{position:relative;display:inline-flex;vertical-align:middle}.btn-group-vertical>.btn,.btn-group>.btn{position:relative;flex:1 1 auto}.btn-group-vertical>.btn:hover,.btn-group>.btn:hover{z-index:1}.btn-group-vertical>.btn.active,.btn-group-vertical>.btn:active,.btn-group-vertical>.btn:focus,.btn-group>.btn.active,.btn-group>.btn:active,.btn-group>.btn:focus{z-index:1}.btn-toolbar{display:flex;flex-wrap:wrap;justify-content:flex-start}.btn-toolbar .input-group{width:auto}.btn-group>.btn-group:not(:first-child),.btn-group>.btn:not(:first-child){margin-left:-1px}.btn-group>.btn-group:not(:last-child)>.btn,.btn-group>.btn:not(:last-child):not(.dropdown-toggle){border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn-group:not(:first-child)>.btn,.btn-group>.btn:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.dropdown-toggle-split{padding-right:.5625rem;padding-left:.5625rem}.dropdown-toggle-split::after,.dropright .dropdown-toggle-split::after,.dropup .dropdown-toggle-split::after{margin-left:0}.dropleft .dropdown-toggle-split::before{margin-right:0}.btn-group-sm>.btn+.dropdown-toggle-split,.btn-sm+.dropdown-toggle-split{padding-right:.375rem;padding-left:.375rem}.btn-group-lg>.btn+.dropdown-toggle-split,.btn-lg+.dropdown-toggle-split{padding-right:.75rem;padding-left:.75rem}.btn-group-vertical{flex-direction:column;align-items:flex-start;justify-content:center}.btn-group-vertical>.btn,.btn-group-vertical>.btn-group{width:100%}.btn-group-vertical>.btn-group:not(:first-child),.btn-group-vertical>.btn:not(:first-child){margin-top:-1px}.btn-group-vertical>.btn-group:not(:last-child)>.btn,.btn-group-vertical>.btn:not(:last-child):not(.dropdown-toggle){border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn-group:not(:first-child)>.btn,.btn-group-vertical>.btn:not(:first-child){border-top-left-radius:0;border-top-right-radius:0}.btn-group-toggle>.btn,.btn-group-toggle>.btn-group>.btn{margin-bottom:0}.btn-group-toggle>.btn input[type=checkbox],.btn-group-toggle>.btn input[type=radio],.btn-group-toggle>.btn-group>.btn input[type=checkbox],.btn-group-toggle>.btn-group>.btn input[type=radio]{position:absolute;clip:rect(0,0,0,0);pointer-events:none}.input-group{position:relative;display:flex;flex-wrap:wrap;align-items:stretch;width:100%}.input-group>.custom-file,.input-group>.custom-select,.input-group>.form-control,.input-group>.form-control-plaintext{position:relative;flex:1 1 auto;width:1%;margin-bottom:0}.input-group>.custom-file+.custom-file,.input-group>.custom-file+.custom-select,.input-group>.custom-file+.form-control,.input-group>.custom-select+.custom-file,.input-group>.custom-select+.custom-select,.input-group>.custom-select+.form-control,.input-group>.form-control+.custom-file,.input-group>.form-control+.custom-select,.input-group>.form-control+.form-control,.input-group>.form-control-plaintext+.custom-file,.input-group>.form-control-plaintext+.custom-select,.input-group>.form-control-plaintext+.form-control{margin-left:-1px}.input-group>.custom-file .custom-file-input:focus~.custom-file-label,.input-group>.custom-select:focus,.input-group>.form-control:focus{z-index:3}.input-group>.custom-file .custom-file-input:focus{z-index:4}.input-group>.custom-select:not(:last-child),.input-group>.form-control:not(:last-child){border-top-right-radius:0;border-bottom-right-radius:0}.input-group>.custom-select:not(:first-child),.input-group>.form-control:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.input-group>.custom-file{display:flex;align-items:center}.input-group>.custom-file:not(:last-child) .custom-file-label,.input-group>.custom-file:not(:last-child) .custom-file-label::after{border-top-right-radius:0;border-bottom-right-radius:0}.input-group>.custom-file:not(:first-child) .custom-file-label{border-top-left-radius:0;border-bottom-left-radius:0}.input-group-append,.input-group-prepend{display:flex}.input-group-append .btn,.input-group-prepend .btn{position:relative;z-index:2}.input-group-append .btn:focus,.input-group-prepend .btn:focus{z-index:3}.input-group-append .btn+.btn,.input-group-append .btn+.input-group-text,.input-group-append .input-group-text+.btn,.input-group-append .input-group-text+.input-group-text,.input-group-prepend .btn+.btn,.input-group-prepend .btn+.input-group-text,.input-group-prepend .input-group-text+.btn,.input-group-prepend .input-group-text+.input-group-text{margin-left:-1px}.input-group-prepend{margin-right:-1px}.input-group-append{margin-left:-1px}.input-group-text{display:flex;align-items:center;padding:.375rem .75rem;margin-bottom:0;font-size:1rem;font-weight:400;line-height:1.5;color:#495057;text-align:center;white-space:nowrap;background-color:#e9ecef;border:1px solid #ced4da;border-radius:1.5rem}.input-group-text input[type=checkbox],.input-group-text input[type=radio]{margin-top:0}.input-group-lg>.custom-select,.input-group-lg>.form-control:not(textarea){height:calc(1.5em + 1rem + 2px)}.input-group-lg>.custom-select,.input-group-lg>.form-control,.input-group-lg>.input-group-append>.btn,.input-group-lg>.input-group-append>.input-group-text,.input-group-lg>.input-group-prepend>.btn,.input-group-lg>.input-group-prepend>.input-group-text{padding:.5rem 1rem;font-size:1.25rem;line-height:1.5;border-radius:1.5rem}.input-group-sm>.custom-select,.input-group-sm>.form-control:not(textarea){height:calc(1.5em + .5rem + 2px)}.input-group-sm>.custom-select,.input-group-sm>.form-control,.input-group-sm>.input-group-append>.btn,.input-group-sm>.input-group-append>.input-group-text,.input-group-sm>.input-group-prepend>.btn,.input-group-sm>.input-group-prepend>.input-group-text{padding:.25rem .5rem;font-size:.875rem;line-height:1.5;border-radius:1rem}.input-group-lg>.custom-select,.input-group-sm>.custom-select{padding-right:1.75rem}.input-group>.input-group-append:last-child>.btn:not(:last-child):not(.dropdown-toggle),.input-group>.input-group-append:last-child>.input-group-text:not(:last-child),.input-group>.input-group-append:not(:last-child)>.btn,.input-group>.input-group-append:not(:last-child)>.input-group-text,.input-group>.input-group-prepend>.btn,.input-group>.input-group-prepend>.input-group-text{border-top-right-radius:0;border-bottom-right-radius:0}.input-group>.input-group-append>.btn,.input-group>.input-group-append>.input-group-text,.input-group>.input-group-prepend:first-child>.btn:not(:first-child),.input-group>.input-group-prepend:first-child>.input-group-text:not(:first-child),.input-group>.input-group-prepend:not(:first-child)>.btn,.input-group>.input-group-prepend:not(:first-child)>.input-group-text{border-top-left-radius:0;border-bottom-left-radius:0}.custom-control{position:relative;display:block;min-height:1.5rem;padding-left:1.5rem}.custom-control-inline{display:inline-flex;margin-right:1rem}.custom-control-input{position:absolute;z-index:-1;opacity:0}.custom-control-input:checked~.custom-control-label::before{color:#fff;border-color:#fac57c;background-color:#fac57c}.custom-control-input:focus~.custom-control-label::before{box-shadow:0 0 0 .2rem rgba(250,197,124,.75)}.custom-control-input:focus:not(:checked)~.custom-control-label::before{border-color:#fffbf7}.custom-control-input:not(:disabled):active~.custom-control-label::before{color:#fff;background-color:#fff;border-color:#fff}.custom-control-input:disabled~.custom-control-label{color:#6c757d}.custom-control-input:disabled~.custom-control-label::before{background-color:#e9ecef}.custom-control-label{position:relative;margin-bottom:0;vertical-align:top}.custom-control-label::before{position:absolute;top:.25rem;left:-1.5rem;display:block;width:1rem;height:1rem;pointer-events:none;content:"";background-color:#fff;border:#adb5bd solid 1px}.custom-control-label::after{position:absolute;top:.25rem;left:-1.5rem;display:block;width:1rem;height:1rem;content:"";background:no-repeat 50%/50% 50%}.custom-checkbox .custom-control-label::before{border-radius:1.5rem}.custom-checkbox .custom-control-input:checked~.custom-control-label::after{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%23ffffff' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z'/%3e%3c/svg%3e")}.custom-checkbox .custom-control-input:indeterminate~.custom-control-label::before{border-color:#fac57c;background-color:#fac57c}.custom-checkbox .custom-control-input:indeterminate~.custom-control-label::after{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 4 4'%3e%3cpath stroke='%23ffffff' d='M0 2h4'/%3e%3c/svg%3e")}.custom-checkbox .custom-control-input:disabled:checked~.custom-control-label::before{background-color:rgba(250,197,124,.5)}.custom-checkbox .custom-control-input:disabled:indeterminate~.custom-control-label::before{background-color:rgba(250,197,124,.5)}.custom-radio .custom-control-label::before{border-radius:50%}.custom-radio .custom-control-input:checked~.custom-control-label::after{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%23ffffff'/%3e%3c/svg%3e")}.custom-radio .custom-control-input:disabled:checked~.custom-control-label::before{background-color:rgba(250,197,124,.5)}.custom-switch{padding-left:2.25rem}.custom-switch .custom-control-label::before{left:-2.25rem;width:1.75rem;pointer-events:all;border-radius:.5rem}.custom-switch .custom-control-label::after{top:calc(.25rem + 2px);left:calc(-2.25rem + 2px);width:calc(1rem - 4px);height:calc(1rem - 4px);background-color:#adb5bd;border-radius:.5rem;transition:transform .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.custom-switch .custom-control-label::after{transition:none}}.custom-switch .custom-control-input:checked~.custom-control-label::after{background-color:#fff;transform:translateX(.75rem)}.custom-switch .custom-control-input:disabled:checked~.custom-control-label::before{background-color:rgba(250,197,124,.5)}.custom-select{display:inline-block;width:100%;height:calc(1.5em + .75rem + 2px);padding:.375rem 1.75rem .375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5;color:#495057;vertical-align:middle;background:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 4 5'%3e%3cpath fill='%23343a40' d='M2 0L0 2h4zm0 5L0 3h4z'/%3e%3c/svg%3e") no-repeat right .75rem center/8px 10px;background-color:#fff;border:1px solid #ced4da;border-radius:1.5rem;appearance:none}.custom-select:focus{border-color:#fffbf7;outline:0;box-shadow:0 0 0 .2rem rgba(250,197,124,.75)}.custom-select:focus::-ms-value{color:#495057;background-color:#fff}.custom-select[multiple],.custom-select[size]:not([size="1"]){height:auto;padding-right:.75rem;background-image:none}.custom-select:disabled{color:#6c757d;background-color:#e9ecef}.custom-select::-ms-expand{display:none}.custom-select-sm{height:calc(1.5em + .5rem + 2px);padding-top:.25rem;padding-bottom:.25rem;padding-left:.5rem;font-size:.875rem}.custom-select-lg{height:calc(1.5em + 1rem + 2px);padding-top:.5rem;padding-bottom:.5rem;padding-left:1rem;font-size:1.25rem}.custom-file{position:relative;display:inline-block;width:100%;height:calc(1.5em + .75rem + 2px);margin-bottom:0}.custom-file-input{position:relative;z-index:2;width:100%;height:calc(1.5em + .75rem + 2px);margin:0;opacity:0}.custom-file-input:focus~.custom-file-label{border-color:#fffbf7;box-shadow:0 0 0 .2rem rgba(250,197,124,.75)}.custom-file-input:disabled~.custom-file-label{background-color:#e9ecef}.custom-file-input:lang(en)~.custom-file-label::after{content:"Browse"}.custom-file-input~.custom-file-label[data-browse]::after{content:attr(data-browse)}.custom-file-label{position:absolute;top:0;right:0;left:0;z-index:1;height:calc(1.5em + .75rem + 2px);padding:.375rem .75rem;font-weight:400;line-height:1.5;color:#495057;background-color:#fff;border:1px solid #ced4da;border-radius:1.5rem}.custom-file-label::after{position:absolute;top:0;right:0;bottom:0;z-index:3;display:block;height:calc(1.5em + .75rem);padding:.375rem .75rem;line-height:1.5;color:#495057;content:"Browse";background-color:#e9ecef;border-left:inherit;border-radius:0 1.5rem 1.5rem 0}.custom-range{width:100%;height:calc(1rem + .4rem);padding:0;background-color:transparent;appearance:none}.custom-range:focus{outline:0}.custom-range:focus::-webkit-slider-thumb{box-shadow:0 0 0 1px #f2f0f0,0 0 0 .2rem rgba(250,197,124,.75)}.custom-range:focus::-moz-range-thumb{box-shadow:0 0 0 1px #f2f0f0,0 0 0 .2rem rgba(250,197,124,.75)}.custom-range:focus::-ms-thumb{box-shadow:0 0 0 1px #f2f0f0,0 0 0 .2rem rgba(250,197,124,.75)}.custom-range::-moz-focus-outer{border:0}.custom-range::-webkit-slider-thumb{width:1rem;height:1rem;margin-top:-.25rem;background-color:#fac57c;border:0;border-radius:1rem;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;appearance:none}@media (prefers-reduced-motion:reduce){.custom-range::-webkit-slider-thumb{transition:none}}.custom-range::-webkit-slider-thumb:active{background-color:#fff}.custom-range::-webkit-slider-runnable-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:#dee2e6;border-color:transparent;border-radius:1rem}.custom-range::-moz-range-thumb{width:1rem;height:1rem;background-color:#fac57c;border:0;border-radius:1rem;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;appearance:none}@media (prefers-reduced-motion:reduce){.custom-range::-moz-range-thumb{transition:none}}.custom-range::-moz-range-thumb:active{background-color:#fff}.custom-range::-moz-range-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:#dee2e6;border-color:transparent;border-radius:1rem}.custom-range::-ms-thumb{width:1rem;height:1rem;margin-top:0;margin-right:.2rem;margin-left:.2rem;background-color:#fac57c;border:0;border-radius:1rem;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;appearance:none}@media (prefers-reduced-motion:reduce){.custom-range::-ms-thumb{transition:none}}.custom-range::-ms-thumb:active{background-color:#fff}.custom-range::-ms-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:transparent;border-color:transparent;border-width:.5rem}.custom-range::-ms-fill-lower{background-color:#dee2e6;border-radius:1rem}.custom-range::-ms-fill-upper{margin-right:15px;background-color:#dee2e6;border-radius:1rem}.custom-range:disabled::-webkit-slider-thumb{background-color:#adb5bd}.custom-range:disabled::-webkit-slider-runnable-track{cursor:default}.custom-range:disabled::-moz-range-thumb{background-color:#adb5bd}.custom-range:disabled::-moz-range-track{cursor:default}.custom-range:disabled::-ms-thumb{background-color:#adb5bd}.custom-control-label::before,.custom-file-label,.custom-select{transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.custom-control-label::before,.custom-file-label,.custom-select{transition:none}}.nav{display:flex;flex-wrap:wrap;padding-left:0;margin-bottom:0;list-style:none}.nav-link{display:block;padding:.5rem 1rem}.nav-link:focus,.nav-link:hover{text-decoration:none}.nav-link.disabled{color:#6c757d;pointer-events:none;cursor:default}.nav-tabs{border-bottom:1px solid #dee2e6}.nav-tabs .nav-item{margin-bottom:-1px}.nav-tabs .nav-link{border:1px solid transparent;border-top-left-radius:1.5rem;border-top-right-radius:1.5rem}.nav-tabs .nav-link:focus,.nav-tabs .nav-link:hover{border-color:#e9ecef #e9ecef #dee2e6}.nav-tabs .nav-link.disabled{color:#6c757d;background-color:transparent;border-color:transparent}.nav-tabs .nav-item.show .nav-link,.nav-tabs .nav-link.active{color:#495057;background-color:#f2f0f0;border-color:#dee2e6 #dee2e6 #f2f0f0}.nav-tabs .dropdown-menu{margin-top:-1px;border-top-left-radius:0;border-top-right-radius:0}.nav-pills .nav-link{border-radius:1.5rem}.nav-pills .nav-link.active,.nav-pills .show>.nav-link{color:#fff;background-color:#fac57c}.nav-fill .nav-item{flex:1 1 auto;text-align:center}.nav-justified .nav-item{flex-basis:0;flex-grow:1;text-align:center}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.navbar{position:relative;display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;padding:.5rem 1rem}.navbar>.container,.navbar>.container-fluid{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between}.navbar-brand{display:inline-block;padding-top:.3125rem;padding-bottom:.3125rem;margin-right:1rem;font-size:1.25rem;line-height:inherit;white-space:nowrap}.navbar-brand:focus,.navbar-brand:hover{text-decoration:none}.navbar-nav{display:flex;flex-direction:column;padding-left:0;margin-bottom:0;list-style:none}.navbar-nav .nav-link{padding-right:0;padding-left:0}.navbar-nav .dropdown-menu{position:static;float:none}.navbar-text{display:inline-block;padding-top:.5rem;padding-bottom:.5rem}.navbar-collapse{flex-basis:100%;flex-grow:1;align-items:center}.navbar-toggler{padding:.25rem .75rem;font-size:1.25rem;line-height:1;background-color:transparent;border:1px solid transparent;border-radius:1.5rem}.navbar-toggler:focus,.navbar-toggler:hover{text-decoration:none}.navbar-toggler-icon{display:inline-block;width:1.5em;height:1.5em;vertical-align:middle;content:"";background:no-repeat center center;background-size:100% 100%}@media (max-width:575.98px){.navbar-expand-sm>.container,.navbar-expand-sm>.container-fluid{padding-right:0;padding-left:0}}@media (min-width:576px){.navbar-expand-sm{flex-flow:row nowrap;justify-content:flex-start}.navbar-expand-sm .navbar-nav{flex-direction:row}.navbar-expand-sm .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-sm .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-sm>.container,.navbar-expand-sm>.container-fluid{flex-wrap:nowrap}.navbar-expand-sm .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-sm .navbar-toggler{display:none}}@media (max-width:767.98px){.navbar-expand-md>.container,.navbar-expand-md>.container-fluid{padding-right:0;padding-left:0}}@media (min-width:768px){.navbar-expand-md{flex-flow:row nowrap;justify-content:flex-start}.navbar-expand-md .navbar-nav{flex-direction:row}.navbar-expand-md .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-md .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-md>.container,.navbar-expand-md>.container-fluid{flex-wrap:nowrap}.navbar-expand-md .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-md .navbar-toggler{display:none}}@media (max-width:991.98px){.navbar-expand-lg>.container,.navbar-expand-lg>.container-fluid{padding-right:0;padding-left:0}}@media (min-width:992px){.navbar-expand-lg{flex-flow:row nowrap;justify-content:flex-start}.navbar-expand-lg .navbar-nav{flex-direction:row}.navbar-expand-lg .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-lg .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-lg>.container,.navbar-expand-lg>.container-fluid{flex-wrap:nowrap}.navbar-expand-lg .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-lg .navbar-toggler{display:none}}@media (max-width:1199.98px){.navbar-expand-xl>.container,.navbar-expand-xl>.container-fluid{padding-right:0;padding-left:0}}@media (min-width:1200px){.navbar-expand-xl{flex-flow:row nowrap;justify-content:flex-start}.navbar-expand-xl .navbar-nav{flex-direction:row}.navbar-expand-xl .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-xl .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-xl>.container,.navbar-expand-xl>.container-fluid{flex-wrap:nowrap}.navbar-expand-xl .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-xl .navbar-toggler{display:none}}.navbar-expand{flex-flow:row nowrap;justify-content:flex-start}.navbar-expand>.container,.navbar-expand>.container-fluid{padding-right:0;padding-left:0}.navbar-expand .navbar-nav{flex-direction:row}.navbar-expand .navbar-nav .dropdown-menu{position:absolute}.navbar-expand .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand>.container,.navbar-expand>.container-fluid{flex-wrap:nowrap}.navbar-expand .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand .navbar-toggler{display:none}.navbar-light .navbar-brand{color:#212529}.navbar-light .navbar-brand:focus,.navbar-light .navbar-brand:hover{color:#212529}.navbar-light .navbar-nav .nav-link{color:#6c757d}.navbar-light .navbar-nav .nav-link:focus,.navbar-light .navbar-nav .nav-link:hover{color:#212529}.navbar-light .navbar-nav .nav-link.disabled{color:rgba(34,34,34,.3)}.navbar-light .navbar-nav .active>.nav-link,.navbar-light .navbar-nav .nav-link.active,.navbar-light .navbar-nav .nav-link.show,.navbar-light .navbar-nav .show>.nav-link{color:#212529}.navbar-light .navbar-toggler{color:#6c757d;border-color:rgba(34,34,34,.1)}.navbar-light .navbar-toggler-icon{background-image:url("data:image/svg+xml,%3csvg viewBox='0 0 30 30' xmlns='http://www.w3.org/2000/svg'%3e%3cpath stroke='%236c757d' stroke-width='2' stroke-linecap='round' stroke-miterlimit='10' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e")}.navbar-light .navbar-text{color:#6c757d}.navbar-light .navbar-text a{color:#212529}.navbar-light .navbar-text a:focus,.navbar-light .navbar-text a:hover{color:#212529}.navbar-dark .navbar-brand{color:#fff}.navbar-dark .navbar-brand:focus,.navbar-dark .navbar-brand:hover{color:#fff}.navbar-dark .navbar-nav .nav-link{color:rgba(255,255,255,.5)}.navbar-dark .navbar-nav .nav-link:focus,.navbar-dark .navbar-nav .nav-link:hover{color:rgba(255,255,255,.75)}.navbar-dark .navbar-nav .nav-link.disabled{color:rgba(255,255,255,.25)}.navbar-dark .navbar-nav .active>.nav-link,.navbar-dark .navbar-nav .nav-link.active,.navbar-dark .navbar-nav .nav-link.show,.navbar-dark .navbar-nav .show>.nav-link{color:#fff}.navbar-dark .navbar-toggler{color:rgba(255,255,255,.5);border-color:rgba(34,34,34,.1)}.navbar-dark .navbar-toggler-icon{background-image:url("data:image/svg+xml,%3csvg viewBox='0 0 30 30' xmlns='http://www.w3.org/2000/svg'%3e%3cpath stroke='rgba(255, 255, 255, 0.5)' stroke-width='2' stroke-linecap='round' stroke-miterlimit='10' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e")}.navbar-dark .navbar-text{color:rgba(255,255,255,.5)}.navbar-dark .navbar-text a{color:#fff}.navbar-dark .navbar-text a:focus,.navbar-dark .navbar-text a:hover{color:#fff}.card{position:relative;display:flex;flex-direction:column;min-width:0;word-wrap:break-word;background-color:#f8f9fa;background-clip:border-box;border:1px solid rgba(34,34,34,.125);border-radius:1.5rem}.card>hr{margin-right:0;margin-left:0}.card>.list-group:first-child .list-group-item:first-child{border-top-left-radius:1.5rem;border-top-right-radius:1.5rem}.card>.list-group:last-child .list-group-item:last-child{border-bottom-right-radius:1.5rem;border-bottom-left-radius:1.5rem}.card-body{flex:1 1 auto;padding:1.25rem;color:#495057}.card-title{margin-bottom:.75rem}.card-subtitle{margin-top:-.375rem;margin-bottom:0}.card-text:last-child{margin-bottom:0}.card-link:hover{text-decoration:none}.card-link+.card-link{margin-left:1.25rem}.card-header{padding:.75rem 1.25rem;margin-bottom:0;color:#495057;background-color:rgba(34,34,34,.03);border-bottom:1px solid rgba(34,34,34,.125)}.card-header:first-child{border-radius:calc(1.5rem - 1px) calc(1.5rem - 1px) 0 0}.card-header+.list-group .list-group-item:first-child{border-top:0}.card-footer{padding:.75rem 1.25rem;background-color:rgba(34,34,34,.03);border-top:1px solid rgba(34,34,34,.125)}.card-footer:last-child{border-radius:0 0 calc(1.5rem - 1px) calc(1.5rem - 1px)}.card-header-tabs{margin-right:-.625rem;margin-bottom:-.75rem;margin-left:-.625rem;border-bottom:0}.card-header-pills{margin-right:-.625rem;margin-left:-.625rem}.card-img-overlay{position:absolute;top:0;right:0;bottom:0;left:0;padding:1.25rem}.card-img{width:100%;border-radius:calc(1.5rem - 1px)}.card-img-top{width:100%;border-top-left-radius:calc(1.5rem - 1px);border-top-right-radius:calc(1.5rem - 1px)}.card-img-bottom{width:100%;border-bottom-right-radius:calc(1.5rem - 1px);border-bottom-left-radius:calc(1.5rem - 1px)}.card-deck{display:flex;flex-direction:column}.card-deck .card{margin-bottom:15px}@media (min-width:576px){.card-deck{flex-flow:row wrap;margin-right:-15px;margin-left:-15px}.card-deck .card{display:flex;flex:1 0 0%;flex-direction:column;margin-right:15px;margin-bottom:0;margin-left:15px}}.card-group{display:flex;flex-direction:column}.card-group>.card{margin-bottom:15px}@media (min-width:576px){.card-group{flex-flow:row wrap}.card-group>.card{flex:1 0 0%;margin-bottom:0}.card-group>.card+.card{margin-left:0;border-left:0}.card-group>.card:not(:last-child){border-top-right-radius:0;border-bottom-right-radius:0}.card-group>.card:not(:last-child) .card-header,.card-group>.card:not(:last-child) .card-img-top{border-top-right-radius:0}.card-group>.card:not(:last-child) .card-footer,.card-group>.card:not(:last-child) .card-img-bottom{border-bottom-right-radius:0}.card-group>.card:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.card-group>.card:not(:first-child) .card-header,.card-group>.card:not(:first-child) .card-img-top{border-top-left-radius:0}.card-group>.card:not(:first-child) .card-footer,.card-group>.card:not(:first-child) .card-img-bottom{border-bottom-left-radius:0}}.card-columns .card{margin-bottom:.75rem}@media (min-width:576px){.card-columns{column-count:3;column-gap:1.25rem;orphans:1;widows:1}.card-columns .card{display:inline-block;width:100%}}.accordion>.card{overflow:hidden}.accordion>.card:not(:first-of-type) .card-header:first-child{border-radius:0}.accordion>.card:not(:first-of-type):not(:last-of-type){border-bottom:0;border-radius:0}.accordion>.card:first-of-type{border-bottom:0;border-bottom-right-radius:0;border-bottom-left-radius:0}.accordion>.card:last-of-type{border-top-left-radius:0;border-top-right-radius:0}.accordion>.card .card-header{margin-bottom:-1px}.breadcrumb{display:flex;flex-wrap:wrap;padding:.75rem 1rem;margin-bottom:1rem;list-style:none;background-color:#e9ecef;border-radius:1.5rem}.breadcrumb-item+.breadcrumb-item{padding-left:.5rem}.breadcrumb-item+.breadcrumb-item::before{display:inline-block;padding-right:.5rem;color:#6c757d;content:"/"}.breadcrumb-item+.breadcrumb-item:hover::before{text-decoration:underline}.breadcrumb-item+.breadcrumb-item:hover::before{text-decoration:none}.breadcrumb-item.active{color:#6c757d}.pagination{display:flex;padding-left:0;list-style:none;border-radius:1.5rem}.page-link{position:relative;display:block;padding:.5rem .75rem;margin-left:-1px;line-height:1.25;color:#ed8d09;background-color:#fff;border:1px solid #dee2e6}.page-link:hover{z-index:2;color:#a46206;text-decoration:none;background-color:#e9ecef;border-color:#dee2e6}.page-link:focus{z-index:2;outline:0;box-shadow:0 0 0 .2rem rgba(250,197,124,.75)}.page-item:first-child .page-link{margin-left:0;border-top-left-radius:1.5rem;border-bottom-left-radius:1.5rem}.page-item:last-child .page-link{border-top-right-radius:1.5rem;border-bottom-right-radius:1.5rem}.page-item.active .page-link{z-index:1;color:#fff;background-color:#fac57c;border-color:#fac57c}.page-item.disabled .page-link{color:#6c757d;pointer-events:none;cursor:auto;background-color:#fff;border-color:#dee2e6}.pagination-lg .page-link{padding:.75rem 1.5rem;font-size:1.25rem;line-height:1.5}.pagination-lg .page-item:first-child .page-link{border-top-left-radius:1.5rem;border-bottom-left-radius:1.5rem}.pagination-lg .page-item:last-child .page-link{border-top-right-radius:1.5rem;border-bottom-right-radius:1.5rem}.pagination-sm .page-link{padding:.25rem .5rem;font-size:.875rem;line-height:1.5}.pagination-sm .page-item:first-child .page-link{border-top-left-radius:1rem;border-bottom-left-radius:1rem}.pagination-sm .page-item:last-child .page-link{border-top-right-radius:1rem;border-bottom-right-radius:1rem}.badge{display:inline-block;padding:.25em .4em;font-size:75%;font-weight:700;line-height:1;text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:1.5rem;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.badge{transition:none}}a.badge:focus,a.badge:hover{text-decoration:none}.badge:empty{display:none}.btn .badge{position:relative;top:-1px}.badge-pill{padding-right:.6em;padding-left:.6em;border-radius:10rem}.badge-primary{color:#212529;background-color:#fac57c}a.badge-primary:focus,a.badge-primary:hover{color:#212529;background-color:#f8af4b}a.badge-primary.focus,a.badge-primary:focus{outline:0;box-shadow:0 0 0 .2rem rgba(250,197,124,.5)}.badge-secondary{color:#212529;background-color:#bad2be}a.badge-secondary:focus,a.badge-secondary:hover{color:#212529;background-color:#9bbea1}a.badge-secondary.focus,a.badge-secondary:focus{outline:0;box-shadow:0 0 0 .2rem rgba(186,210,190,.5)}.badge-success{color:#fff;background-color:#38553d}a.badge-success:focus,a.badge-success:hover{color:#fff;background-color:#243627}a.badge-success.focus,a.badge-success:focus{outline:0;box-shadow:0 0 0 .2rem rgba(56,85,61,.5)}.badge-info{color:#212529;background-color:#49ce5f}a.badge-info:focus,a.badge-info:hover{color:#212529;background-color:#30b446}a.badge-info.focus,a.badge-info:focus{outline:0;box-shadow:0 0 0 .2rem rgba(73,206,95,.5)}.badge-warning{color:#212529;background-color:#ffc107}a.badge-warning:focus,a.badge-warning:hover{color:#212529;background-color:#d39e00}a.badge-warning.focus,a.badge-warning:focus{outline:0;box-shadow:0 0 0 .2rem rgba(255,193,7,.5)}.badge-danger{color:#212529;background-color:#ed8d09}a.badge-danger:focus,a.badge-danger:hover{color:#212529;background-color:#bc7007}a.badge-danger.focus,a.badge-danger:focus{outline:0;box-shadow:0 0 0 .2rem rgba(237,141,9,.5)}.badge-light{color:#212529;background-color:#f8f9fa}a.badge-light:focus,a.badge-light:hover{color:#212529;background-color:#dae0e5}a.badge-light.focus,a.badge-light:focus{outline:0;box-shadow:0 0 0 .2rem rgba(248,249,250,.5)}.badge-dark{color:#fff;background-color:#343a40}a.badge-dark:focus,a.badge-dark:hover{color:#fff;background-color:#1d2124}a.badge-dark.focus,a.badge-dark:focus{outline:0;box-shadow:0 0 0 .2rem rgba(52,58,64,.5)}.jumbotron{padding:2rem 1rem;margin-bottom:2rem;background-color:#e9ecef;border-radius:1.5rem}@media (min-width:576px){.jumbotron{padding:4rem 2rem}}.jumbotron-fluid{padding-right:0;padding-left:0;border-radius:0}.alert{position:relative;padding:.75rem 1.25rem;margin-bottom:1rem;border:1px solid transparent;border-radius:1.5rem}.alert-heading{color:inherit}.alert-link{font-weight:700}.alert-dismissible{padding-right:4rem}.alert-dismissible .close{position:absolute;top:0;right:0;padding:.75rem 1.25rem;color:inherit}.alert-primary{color:#927751;background-color:#fef3e5;border-color:#feefda}.alert-primary hr{border-top-color:#fde4c1}.alert-primary .alert-link{color:#715c3f}.alert-secondary{color:#717e73;background-color:#f1f6f2;border-color:#ecf2ed}.alert-secondary hr{border-top-color:#dde8df}.alert-secondary .alert-link{color:#59635a}.alert-success{color:#2d3d30;background-color:#d7ddd8;border-color:#c7cfc9}.alert-success hr{border-top-color:#b9c3bc}.alert-success .alert-link{color:#172019}.alert-info{color:#367b42;background-color:#dbf5df;border-color:#ccf1d2}.alert-info hr{border-top-color:#b8ecc0}.alert-info .alert-link{color:#26582f}.alert-warning{color:#957514;background-color:#fff3cd;border-color:#ffeeba}.alert-warning hr{border-top-color:#ffe8a1}.alert-warning .alert-link{color:#68520e}.alert-danger{color:#8c5a15;background-color:#fbe8ce;border-color:#fadfba}.alert-danger hr{border-top-color:#f8d4a2}.alert-danger .alert-link{color:#603d0e}.alert-light{color:#919292;background-color:#fefefe;border-color:#fdfdfe}.alert-light hr{border-top-color:#ececf6}.alert-light .alert-link{color:#777979}.alert-dark{color:#2b2e32;background-color:#d6d8d9;border-color:#c6c8ca}.alert-dark hr{border-top-color:#b9bbbe}.alert-dark .alert-link{color:#131517}@keyframes progress-bar-stripes{from{background-position:1rem 0}to{background-position:0 0}}.progress{display:flex;height:1rem;overflow:hidden;font-size:.75rem;background-color:#e9ecef;border-radius:1.5rem}.progress-bar{display:flex;flex-direction:column;justify-content:center;color:#fff;text-align:center;white-space:nowrap;background-color:#fac57c;transition:width .6s ease}@media (prefers-reduced-motion:reduce){.progress-bar{transition:none}}.progress-bar-striped{background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-size:1rem 1rem}.progress-bar-animated{animation:progress-bar-stripes 1s linear infinite}@media (prefers-reduced-motion:reduce){.progress-bar-animated{animation:none}}.media{display:flex;align-items:flex-start}.media-body{flex:1}.list-group{display:flex;flex-direction:column;padding-left:0;margin-bottom:0}.list-group-item-action{width:100%;color:#495057;text-align:inherit}.list-group-item-action:focus,.list-group-item-action:hover{z-index:1;color:#495057;text-decoration:none;background-color:#f8f9fa}.list-group-item-action:active{color:#495057;background-color:#e9ecef}.list-group-item{position:relative;display:block;padding:.75rem 1.25rem;margin-bottom:-1px;background-color:#fff;border:1px solid rgba(34,34,34,.125)}.list-group-item:first-child{border-top-left-radius:1.5rem;border-top-right-radius:1.5rem}.list-group-item:last-child{margin-bottom:0;border-bottom-right-radius:1.5rem;border-bottom-left-radius:1.5rem}.list-group-item.disabled,.list-group-item:disabled{color:#6c757d;pointer-events:none;background-color:#fff}.list-group-item.active{z-index:2;color:#fff;background-color:#fac57c;border-color:#fac57c}.list-group-horizontal{flex-direction:row}.list-group-horizontal .list-group-item{margin-right:-1px;margin-bottom:0}.list-group-horizontal .list-group-item:first-child{border-top-left-radius:1.5rem;border-bottom-left-radius:1.5rem;border-top-right-radius:0}.list-group-horizontal .list-group-item:last-child{margin-right:0;border-top-right-radius:1.5rem;border-bottom-right-radius:1.5rem;border-bottom-left-radius:0}@media (min-width:576px){.list-group-horizontal-sm{flex-direction:row}.list-group-horizontal-sm .list-group-item{margin-right:-1px;margin-bottom:0}.list-group-horizontal-sm .list-group-item:first-child{border-top-left-radius:1.5rem;border-bottom-left-radius:1.5rem;border-top-right-radius:0}.list-group-horizontal-sm .list-group-item:last-child{margin-right:0;border-top-right-radius:1.5rem;border-bottom-right-radius:1.5rem;border-bottom-left-radius:0}}@media (min-width:768px){.list-group-horizontal-md{flex-direction:row}.list-group-horizontal-md .list-group-item{margin-right:-1px;margin-bottom:0}.list-group-horizontal-md .list-group-item:first-child{border-top-left-radius:1.5rem;border-bottom-left-radius:1.5rem;border-top-right-radius:0}.list-group-horizontal-md .list-group-item:last-child{margin-right:0;border-top-right-radius:1.5rem;border-bottom-right-radius:1.5rem;border-bottom-left-radius:0}}@media (min-width:992px){.list-group-horizontal-lg{flex-direction:row}.list-group-horizontal-lg .list-group-item{margin-right:-1px;margin-bottom:0}.list-group-horizontal-lg .list-group-item:first-child{border-top-left-radius:1.5rem;border-bottom-left-radius:1.5rem;border-top-right-radius:0}.list-group-horizontal-lg .list-group-item:last-child{margin-right:0;border-top-right-radius:1.5rem;border-bottom-right-radius:1.5rem;border-bottom-left-radius:0}}@media (min-width:1200px){.list-group-horizontal-xl{flex-direction:row}.list-group-horizontal-xl .list-group-item{margin-right:-1px;margin-bottom:0}.list-group-horizontal-xl .list-group-item:first-child{border-top-left-radius:1.5rem;border-bottom-left-radius:1.5rem;border-top-right-radius:0}.list-group-horizontal-xl .list-group-item:last-child{margin-right:0;border-top-right-radius:1.5rem;border-bottom-right-radius:1.5rem;border-bottom-left-radius:0}}.list-group-flush .list-group-item{border-right:0;border-left:0;border-radius:0}.list-group-flush .list-group-item:last-child{margin-bottom:-1px}.list-group-flush:first-child .list-group-item:first-child{border-top:0}.list-group-flush:last-child .list-group-item:last-child{margin-bottom:0;border-bottom:0}.list-group-item-primary{color:#927751;background-color:#feefda}.list-group-item-primary.list-group-item-action:focus,.list-group-item-primary.list-group-item-action:hover{color:#927751;background-color:#fde4c1}.list-group-item-primary.list-group-item-action.active{color:#fff;background-color:#927751;border-color:#927751}.list-group-item-secondary{color:#717e73;background-color:#ecf2ed}.list-group-item-secondary.list-group-item-action:focus,.list-group-item-secondary.list-group-item-action:hover{color:#717e73;background-color:#dde8df}.list-group-item-secondary.list-group-item-action.active{color:#fff;background-color:#717e73;border-color:#717e73}.list-group-item-success{color:#2d3d30;background-color:#c7cfc9}.list-group-item-success.list-group-item-action:focus,.list-group-item-success.list-group-item-action:hover{color:#2d3d30;background-color:#b9c3bc}.list-group-item-success.list-group-item-action.active{color:#fff;background-color:#2d3d30;border-color:#2d3d30}.list-group-item-info{color:#367b42;background-color:#ccf1d2}.list-group-item-info.list-group-item-action:focus,.list-group-item-info.list-group-item-action:hover{color:#367b42;background-color:#b8ecc0}.list-group-item-info.list-group-item-action.active{color:#fff;background-color:#367b42;border-color:#367b42}.list-group-item-warning{color:#957514;background-color:#ffeeba}.list-group-item-warning.list-group-item-action:focus,.list-group-item-warning.list-group-item-action:hover{color:#957514;background-color:#ffe8a1}.list-group-item-warning.list-group-item-action.active{color:#fff;background-color:#957514;border-color:#957514}.list-group-item-danger{color:#8c5a15;background-color:#fadfba}.list-group-item-danger.list-group-item-action:focus,.list-group-item-danger.list-group-item-action:hover{color:#8c5a15;background-color:#f8d4a2}.list-group-item-danger.list-group-item-action.active{color:#fff;background-color:#8c5a15;border-color:#8c5a15}.list-group-item-light{color:#919292;background-color:#fdfdfe}.list-group-item-light.list-group-item-action:focus,.list-group-item-light.list-group-item-action:hover{color:#919292;background-color:#ececf6}.list-group-item-light.list-group-item-action.active{color:#fff;background-color:#919292;border-color:#919292}.list-group-item-dark{color:#2b2e32;background-color:#c6c8ca}.list-group-item-dark.list-group-item-action:focus,.list-group-item-dark.list-group-item-action:hover{color:#2b2e32;background-color:#b9bbbe}.list-group-item-dark.list-group-item-action.active{color:#fff;background-color:#2b2e32;border-color:#2b2e32}.close{float:right;font-size:1.5rem;font-weight:700;line-height:1;color:#222;text-shadow:0 1px 0 #fff;opacity:.5}.close:hover{color:#222;text-decoration:none}.close:not(:disabled):not(.disabled):focus,.close:not(:disabled):not(.disabled):hover{opacity:.75}button.close{padding:0;background-color:transparent;border:0;appearance:none}a.close.disabled{pointer-events:none}.toast{max-width:350px;overflow:hidden;font-size:.875rem;background-color:rgba(255,255,255,.85);background-clip:padding-box;border:1px solid rgba(0,0,0,.1);box-shadow:0 .25rem .75rem rgba(34,34,34,.1);backdrop-filter:blur(10px);opacity:0;border-radius:.25rem}.toast:not(:last-child){margin-bottom:.75rem}.toast.showing{opacity:1}.toast.show{display:block;opacity:1}.toast.hide{display:none}.toast-header{display:flex;align-items:center;padding:.25rem .75rem;color:#6c757d;background-color:rgba(255,255,255,.85);background-clip:padding-box;border-bottom:1px solid rgba(0,0,0,.05)}.toast-body{padding:.75rem}.modal-open{overflow:hidden}.modal-open .modal{overflow-x:hidden;overflow-y:auto}.modal{position:fixed;top:0;left:0;z-index:1050;display:none;width:100%;height:100%;overflow:hidden;outline:0}.modal-dialog{position:relative;width:auto;margin:.5rem;pointer-events:none}.modal.fade .modal-dialog{transition:transform .3s ease-out;transform:translate(0,-50px)}@media (prefers-reduced-motion:reduce){.modal.fade .modal-dialog{transition:none}}.modal.show .modal-dialog{transform:none}.modal-dialog-scrollable{display:flex;max-height:calc(100% - 1rem)}.modal-dialog-scrollable .modal-content{max-height:calc(100vh - 1rem);overflow:hidden}.modal-dialog-scrollable .modal-footer,.modal-dialog-scrollable .modal-header{flex-shrink:0}.modal-dialog-scrollable .modal-body{overflow-y:auto}.modal-dialog-centered{display:flex;align-items:center;min-height:calc(100% - 1rem)}.modal-dialog-centered::before{display:block;height:calc(100vh - 1rem);content:""}.modal-dialog-centered.modal-dialog-scrollable{flex-direction:column;justify-content:center;height:100%}.modal-dialog-centered.modal-dialog-scrollable .modal-content{max-height:none}.modal-dialog-centered.modal-dialog-scrollable::before{content:none}.modal-content{position:relative;display:flex;flex-direction:column;width:100%;pointer-events:auto;background-color:#fff;background-clip:padding-box;border:1px solid rgba(34,34,34,.2);border-radius:1.5rem;outline:0}.modal-backdrop{position:fixed;top:0;left:0;z-index:1040;width:100vw;height:100vh;background-color:#222}.modal-backdrop.fade{opacity:0}.modal-backdrop.show{opacity:.5}.modal-header{display:flex;align-items:flex-start;justify-content:space-between;padding:1rem 1rem;border-bottom:1px solid #495057;border-top-left-radius:1.5rem;border-top-right-radius:1.5rem}.modal-header .close{padding:1rem 1rem;margin:-1rem -1rem -1rem auto}.modal-title{margin-bottom:0;line-height:1.5}.modal-body{position:relative;flex:1 1 auto;padding:1rem}.modal-footer{display:flex;align-items:center;justify-content:flex-end;padding:1rem;border-top:1px solid #495057;border-bottom-right-radius:1.5rem;border-bottom-left-radius:1.5rem}.modal-footer>:not(:first-child){margin-left:.25rem}.modal-footer>:not(:last-child){margin-right:.25rem}.modal-scrollbar-measure{position:absolute;top:-9999px;width:50px;height:50px;overflow:scroll}@media (min-width:576px){.modal-dialog{max-width:500px;margin:1.75rem auto}.modal-dialog-scrollable{max-height:calc(100% - 3.5rem)}.modal-dialog-scrollable .modal-content{max-height:calc(100vh - 3.5rem)}.modal-dialog-centered{min-height:calc(100% - 3.5rem)}.modal-dialog-centered::before{height:calc(100vh - 3.5rem)}.modal-sm{max-width:300px}}@media (min-width:992px){.modal-lg,.modal-xl{max-width:800px}}@media (min-width:1200px){.modal-xl{max-width:1140px}}.tooltip{position:absolute;z-index:1070;display:block;margin:0;font-family:-apple-system,BlinkMacSystemFont,"Droid Sans","Segoe UI",Helvetica,Arial,sans-serif;font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;white-space:normal;line-break:auto;font-size:.875rem;word-wrap:break-word;opacity:0}.tooltip.show{opacity:.9}.tooltip .arrow{position:absolute;display:block;width:.8rem;height:.4rem}.tooltip .arrow::before{position:absolute;content:"";border-color:transparent;border-style:solid}.bs-tooltip-auto[x-placement^=top],.bs-tooltip-top{padding:.4rem 0}.bs-tooltip-auto[x-placement^=top] .arrow,.bs-tooltip-top .arrow{bottom:0}.bs-tooltip-auto[x-placement^=top] .arrow::before,.bs-tooltip-top .arrow::before{top:0;border-width:.4rem .4rem 0;border-top-color:#222}.bs-tooltip-auto[x-placement^=right],.bs-tooltip-right{padding:0 .4rem}.bs-tooltip-auto[x-placement^=right] .arrow,.bs-tooltip-right .arrow{left:0;width:.4rem;height:.8rem}.bs-tooltip-auto[x-placement^=right] .arrow::before,.bs-tooltip-right .arrow::before{right:0;border-width:.4rem .4rem .4rem 0;border-right-color:#222}.bs-tooltip-auto[x-placement^=bottom],.bs-tooltip-bottom{padding:.4rem 0}.bs-tooltip-auto[x-placement^=bottom] .arrow,.bs-tooltip-bottom .arrow{top:0}.bs-tooltip-auto[x-placement^=bottom] .arrow::before,.bs-tooltip-bottom .arrow::before{bottom:0;border-width:0 .4rem .4rem;border-bottom-color:#222}.bs-tooltip-auto[x-placement^=left],.bs-tooltip-left{padding:0 .4rem}.bs-tooltip-auto[x-placement^=left] .arrow,.bs-tooltip-left .arrow{right:0;width:.4rem;height:.8rem}.bs-tooltip-auto[x-placement^=left] .arrow::before,.bs-tooltip-left .arrow::before{left:0;border-width:.4rem 0 .4rem .4rem;border-left-color:#222}.tooltip-inner{max-width:200px;padding:.25rem .5rem;color:#fff;text-align:center;background-color:#222;border-radius:1.5rem}.popover{position:absolute;top:0;left:0;z-index:1060;display:block;max-width:276px;font-family:-apple-system,BlinkMacSystemFont,"Droid Sans","Segoe UI",Helvetica,Arial,sans-serif;font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;white-space:normal;line-break:auto;font-size:.875rem;word-wrap:break-word;background-color:#fff;background-clip:padding-box;border:1px solid rgba(34,34,34,.2);border-radius:1.5rem}.popover .arrow{position:absolute;display:block;width:1rem;height:.5rem;margin:0 1.5rem}.popover .arrow::after,.popover .arrow::before{position:absolute;display:block;content:"";border-color:transparent;border-style:solid}.bs-popover-auto[x-placement^=top],.bs-popover-top{margin-bottom:.5rem}.bs-popover-auto[x-placement^=top]>.arrow,.bs-popover-top>.arrow{bottom:calc((.5rem + 1px) * -1)}.bs-popover-auto[x-placement^=top]>.arrow::before,.bs-popover-top>.arrow::before{bottom:0;border-width:.5rem .5rem 0;border-top-color:rgba(34,34,34,.25)}.bs-popover-auto[x-placement^=top]>.arrow::after,.bs-popover-top>.arrow::after{bottom:1px;border-width:.5rem .5rem 0;border-top-color:#fff}.bs-popover-auto[x-placement^=right],.bs-popover-right{margin-left:.5rem}.bs-popover-auto[x-placement^=right]>.arrow,.bs-popover-right>.arrow{left:calc((.5rem + 1px) * -1);width:.5rem;height:1rem;margin:1.5rem 0}.bs-popover-auto[x-placement^=right]>.arrow::before,.bs-popover-right>.arrow::before{left:0;border-width:.5rem .5rem .5rem 0;border-right-color:rgba(34,34,34,.25)}.bs-popover-auto[x-placement^=right]>.arrow::after,.bs-popover-right>.arrow::after{left:1px;border-width:.5rem .5rem .5rem 0;border-right-color:#fff}.bs-popover-auto[x-placement^=bottom],.bs-popover-bottom{margin-top:.5rem}.bs-popover-auto[x-placement^=bottom]>.arrow,.bs-popover-bottom>.arrow{top:calc((.5rem + 1px) * -1)}.bs-popover-auto[x-placement^=bottom]>.arrow::before,.bs-popover-bottom>.arrow::before{top:0;border-width:0 .5rem .5rem .5rem;border-bottom-color:rgba(34,34,34,.25)}.bs-popover-auto[x-placement^=bottom]>.arrow::after,.bs-popover-bottom>.arrow::after{top:1px;border-width:0 .5rem .5rem .5rem;border-bottom-color:#fff}.bs-popover-auto[x-placement^=bottom] .popover-header::before,.bs-popover-bottom .popover-header::before{position:absolute;top:0;left:50%;display:block;width:1rem;margin-left:-.5rem;content:"";border-bottom:1px solid #f7f7f7}.bs-popover-auto[x-placement^=left],.bs-popover-left{margin-right:.5rem}.bs-popover-auto[x-placement^=left]>.arrow,.bs-popover-left>.arrow{right:calc((.5rem + 1px) * -1);width:.5rem;height:1rem;margin:1.5rem 0}.bs-popover-auto[x-placement^=left]>.arrow::before,.bs-popover-left>.arrow::before{right:0;border-width:.5rem 0 .5rem .5rem;border-left-color:rgba(34,34,34,.25)}.bs-popover-auto[x-placement^=left]>.arrow::after,.bs-popover-left>.arrow::after{right:1px;border-width:.5rem 0 .5rem .5rem;border-left-color:#fff}.popover-header{padding:.5rem .75rem;margin-bottom:0;font-size:1rem;color:#495057;background-color:#f7f7f7;border-bottom:1px solid #ebebeb;border-top-left-radius:calc(1.5rem - 1px);border-top-right-radius:calc(1.5rem - 1px)}.popover-header:empty{display:none}.popover-body{padding:.5rem .75rem;color:#495057}.carousel{position:relative}.carousel.pointer-event{touch-action:pan-y}.carousel-inner{position:relative;width:100%;overflow:hidden}.carousel-inner::after{display:block;clear:both;content:""}.carousel-item{position:relative;display:none;float:left;width:100%;margin-right:-100%;backface-visibility:hidden;transition:transform .6s ease-in-out}@media (prefers-reduced-motion:reduce){.carousel-item{transition:none}}.carousel-item-next,.carousel-item-prev,.carousel-item.active{display:block}.active.carousel-item-right,.carousel-item-next:not(.carousel-item-left){transform:translateX(100%)}.active.carousel-item-left,.carousel-item-prev:not(.carousel-item-right){transform:translateX(-100%)}.carousel-fade .carousel-item{opacity:0;transition-property:opacity;transform:none}.carousel-fade .carousel-item-next.carousel-item-left,.carousel-fade .carousel-item-prev.carousel-item-right,.carousel-fade .carousel-item.active{z-index:1;opacity:1}.carousel-fade .active.carousel-item-left,.carousel-fade .active.carousel-item-right{z-index:0;opacity:0;transition:0s .6s opacity}@media (prefers-reduced-motion:reduce){.carousel-fade .active.carousel-item-left,.carousel-fade .active.carousel-item-right{transition:none}}.carousel-control-next,.carousel-control-prev{position:absolute;top:0;bottom:0;z-index:1;display:flex;align-items:center;justify-content:center;width:15%;color:#fff;text-align:center;opacity:.5;transition:opacity .15s ease}@media (prefers-reduced-motion:reduce){.carousel-control-next,.carousel-control-prev{transition:none}}.carousel-control-next:focus,.carousel-control-next:hover,.carousel-control-prev:focus,.carousel-control-prev:hover{color:#fff;text-decoration:none;outline:0;opacity:.9}.carousel-control-prev{left:0}.carousel-control-next{right:0}.carousel-control-next-icon,.carousel-control-prev-icon{display:inline-block;width:20px;height:20px;background:no-repeat 50%/100% 100%}.carousel-control-prev-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='%23ffffff' viewBox='0 0 8 8'%3e%3cpath d='M5.25 0l-4 4 4 4 1.5-1.5-2.5-2.5 2.5-2.5-1.5-1.5z'/%3e%3c/svg%3e")}.carousel-control-next-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='%23ffffff' viewBox='0 0 8 8'%3e%3cpath d='M2.75 0l-1.5 1.5 2.5 2.5-2.5 2.5 1.5 1.5 4-4-4-4z'/%3e%3c/svg%3e")}.carousel-indicators{position:absolute;right:0;bottom:0;left:0;z-index:15;display:flex;justify-content:center;padding-left:0;margin-right:15%;margin-left:15%;list-style:none}.carousel-indicators li{box-sizing:content-box;flex:0 1 auto;width:30px;height:3px;margin-right:3px;margin-left:3px;text-indent:-999px;cursor:pointer;background-color:#fff;background-clip:padding-box;border-top:10px solid transparent;border-bottom:10px solid transparent;opacity:.5;transition:opacity .6s ease}@media (prefers-reduced-motion:reduce){.carousel-indicators li{transition:none}}.carousel-indicators .active{opacity:1}.carousel-caption{position:absolute;right:15%;bottom:20px;left:15%;z-index:10;padding-top:20px;padding-bottom:20px;color:#fff;text-align:center}@keyframes spinner-border{to{transform:rotate(360deg)}}.spinner-border{display:inline-block;width:2rem;height:2rem;vertical-align:text-bottom;border:.25em solid currentColor;border-right-color:transparent;border-radius:50%;animation:spinner-border .75s linear infinite}.spinner-border-sm{width:1rem;height:1rem;border-width:.2em}@keyframes spinner-grow{0%{transform:scale(0)}50%{opacity:1}}.spinner-grow{display:inline-block;width:2rem;height:2rem;vertical-align:text-bottom;background-color:currentColor;border-radius:50%;opacity:0;animation:spinner-grow .75s linear infinite}.spinner-grow-sm{width:1rem;height:1rem}.align-baseline{vertical-align:baseline!important}.align-top{vertical-align:top!important}.align-middle{vertical-align:middle!important}.align-bottom{vertical-align:bottom!important}.align-text-bottom{vertical-align:text-bottom!important}.align-text-top{vertical-align:text-top!important}.bg-primary{background-color:#fac57c!important}a.bg-primary:focus,a.bg-primary:hover,button.bg-primary:focus,button.bg-primary:hover{background-color:#f8af4b!important}.bg-secondary{background-color:#bad2be!important}a.bg-secondary:focus,a.bg-secondary:hover,button.bg-secondary:focus,button.bg-secondary:hover{background-color:#9bbea1!important}.bg-success{background-color:#38553d!important}a.bg-success:focus,a.bg-success:hover,button.bg-success:focus,button.bg-success:hover{background-color:#243627!important}.bg-info{background-color:#49ce5f!important}a.bg-info:focus,a.bg-info:hover,button.bg-info:focus,button.bg-info:hover{background-color:#30b446!important}.bg-warning{background-color:#ffc107!important}a.bg-warning:focus,a.bg-warning:hover,button.bg-warning:focus,button.bg-warning:hover{background-color:#d39e00!important}.bg-danger{background-color:#ed8d09!important}a.bg-danger:focus,a.bg-danger:hover,button.bg-danger:focus,button.bg-danger:hover{background-color:#bc7007!important}.bg-light{background-color:#f8f9fa!important}a.bg-light:focus,a.bg-light:hover,button.bg-light:focus,button.bg-light:hover{background-color:#dae0e5!important}.bg-dark{background-color:#343a40!important}a.bg-dark:focus,a.bg-dark:hover,button.bg-dark:focus,button.bg-dark:hover{background-color:#1d2124!important}.bg-white{background-color:#fff!important}.bg-transparent{background-color:transparent!important}.border{border:1px solid #495057!important}.border-top{border-top:1px solid #495057!important}.border-right{border-right:1px solid #495057!important}.border-bottom{border-bottom:1px solid #495057!important}.border-left{border-left:1px solid #495057!important}.border-0{border:0!important}.border-top-0{border-top:0!important}.border-right-0{border-right:0!important}.border-bottom-0{border-bottom:0!important}.border-left-0{border-left:0!important}.border-primary{border-color:#fac57c!important}.border-secondary{border-color:#bad2be!important}.border-success{border-color:#38553d!important}.border-info{border-color:#49ce5f!important}.border-warning{border-color:#ffc107!important}.border-danger{border-color:#ed8d09!important}.border-light{border-color:#f8f9fa!important}.border-dark{border-color:#343a40!important}.border-white{border-color:#fff!important}.rounded-sm{border-radius:1rem!important}.rounded{border-radius:1.5rem!important}.rounded-top{border-top-left-radius:1.5rem!important;border-top-right-radius:1.5rem!important}.rounded-right{border-top-right-radius:1.5rem!important;border-bottom-right-radius:1.5rem!important}.rounded-bottom{border-bottom-right-radius:1.5rem!important;border-bottom-left-radius:1.5rem!important}.rounded-left{border-top-left-radius:1.5rem!important;border-bottom-left-radius:1.5rem!important}.rounded-lg{border-radius:1.5rem!important}.rounded-circle{border-radius:50%!important}.rounded-pill{border-radius:50rem!important}.rounded-0{border-radius:0!important}.clearfix::after{display:block;clear:both;content:""}.d-none{display:none!important}.d-inline{display:inline!important}.d-inline-block{display:inline-block!important}.d-block{display:block!important}.d-table{display:table!important}.d-table-row{display:table-row!important}.d-table-cell{display:table-cell!important}.d-flex{display:flex!important}.d-inline-flex{display:inline-flex!important}@media (min-width:576px){.d-sm-none{display:none!important}.d-sm-inline{display:inline!important}.d-sm-inline-block{display:inline-block!important}.d-sm-block{display:block!important}.d-sm-table{display:table!important}.d-sm-table-row{display:table-row!important}.d-sm-table-cell{display:table-cell!important}.d-sm-flex{display:flex!important}.d-sm-inline-flex{display:inline-flex!important}}@media (min-width:768px){.d-md-none{display:none!important}.d-md-inline{display:inline!important}.d-md-inline-block{display:inline-block!important}.d-md-block{display:block!important}.d-md-table{display:table!important}.d-md-table-row{display:table-row!important}.d-md-table-cell{display:table-cell!important}.d-md-flex{display:flex!important}.d-md-inline-flex{display:inline-flex!important}}@media (min-width:992px){.d-lg-none{display:none!important}.d-lg-inline{display:inline!important}.d-lg-inline-block{display:inline-block!important}.d-lg-block{display:block!important}.d-lg-table{display:table!important}.d-lg-table-row{display:table-row!important}.d-lg-table-cell{display:table-cell!important}.d-lg-flex{display:flex!important}.d-lg-inline-flex{display:inline-flex!important}}@media (min-width:1200px){.d-xl-none{display:none!important}.d-xl-inline{display:inline!important}.d-xl-inline-block{display:inline-block!important}.d-xl-block{display:block!important}.d-xl-table{display:table!important}.d-xl-table-row{display:table-row!important}.d-xl-table-cell{display:table-cell!important}.d-xl-flex{display:flex!important}.d-xl-inline-flex{display:inline-flex!important}}@media print{.d-print-none{display:none!important}.d-print-inline{display:inline!important}.d-print-inline-block{display:inline-block!important}.d-print-block{display:block!important}.d-print-table{display:table!important}.d-print-table-row{display:table-row!important}.d-print-table-cell{display:table-cell!important}.d-print-flex{display:flex!important}.d-print-inline-flex{display:inline-flex!important}}.embed-responsive{position:relative;display:block;width:100%;padding:0;overflow:hidden}.embed-responsive::before{display:block;content:""}.embed-responsive .embed-responsive-item,.embed-responsive embed,.embed-responsive iframe,.embed-responsive object,.embed-responsive video{position:absolute;top:0;bottom:0;left:0;width:100%;height:100%;border:0}.embed-responsive-21by9::before{padding-top:42.85714%}.embed-responsive-16by9::before{padding-top:56.25%}.embed-responsive-4by3::before{padding-top:75%}.embed-responsive-1by1::before{padding-top:100%}.flex-row{flex-direction:row!important}.flex-column{flex-direction:column!important}.flex-row-reverse{flex-direction:row-reverse!important}.flex-column-reverse{flex-direction:column-reverse!important}.flex-wrap{flex-wrap:wrap!important}.flex-nowrap{flex-wrap:nowrap!important}.flex-wrap-reverse{flex-wrap:wrap-reverse!important}.flex-fill{flex:1 1 auto!important}.flex-grow-0{flex-grow:0!important}.flex-grow-1{flex-grow:1!important}.flex-shrink-0{flex-shrink:0!important}.flex-shrink-1{flex-shrink:1!important}.justify-content-start{justify-content:flex-start!important}.justify-content-end{justify-content:flex-end!important}.justify-content-center{justify-content:center!important}.justify-content-between{justify-content:space-between!important}.justify-content-around{justify-content:space-around!important}.align-items-start{align-items:flex-start!important}.align-items-end{align-items:flex-end!important}.align-items-center{align-items:center!important}.align-items-baseline{align-items:baseline!important}.align-items-stretch{align-items:stretch!important}.align-content-start{align-content:flex-start!important}.align-content-end{align-content:flex-end!important}.align-content-center{align-content:center!important}.align-content-between{align-content:space-between!important}.align-content-around{align-content:space-around!important}.align-content-stretch{align-content:stretch!important}.align-self-auto{align-self:auto!important}.align-self-start{align-self:flex-start!important}.align-self-end{align-self:flex-end!important}.align-self-center{align-self:center!important}.align-self-baseline{align-self:baseline!important}.align-self-stretch{align-self:stretch!important}@media (min-width:576px){.flex-sm-row{flex-direction:row!important}.flex-sm-column{flex-direction:column!important}.flex-sm-row-reverse{flex-direction:row-reverse!important}.flex-sm-column-reverse{flex-direction:column-reverse!important}.flex-sm-wrap{flex-wrap:wrap!important}.flex-sm-nowrap{flex-wrap:nowrap!important}.flex-sm-wrap-reverse{flex-wrap:wrap-reverse!important}.flex-sm-fill{flex:1 1 auto!important}.flex-sm-grow-0{flex-grow:0!important}.flex-sm-grow-1{flex-grow:1!important}.flex-sm-shrink-0{flex-shrink:0!important}.flex-sm-shrink-1{flex-shrink:1!important}.justify-content-sm-start{justify-content:flex-start!important}.justify-content-sm-end{justify-content:flex-end!important}.justify-content-sm-center{justify-content:center!important}.justify-content-sm-between{justify-content:space-between!important}.justify-content-sm-around{justify-content:space-around!important}.align-items-sm-start{align-items:flex-start!important}.align-items-sm-end{align-items:flex-end!important}.align-items-sm-center{align-items:center!important}.align-items-sm-baseline{align-items:baseline!important}.align-items-sm-stretch{align-items:stretch!important}.align-content-sm-start{align-content:flex-start!important}.align-content-sm-end{align-content:flex-end!important}.align-content-sm-center{align-content:center!important}.align-content-sm-between{align-content:space-between!important}.align-content-sm-around{align-content:space-around!important}.align-content-sm-stretch{align-content:stretch!important}.align-self-sm-auto{align-self:auto!important}.align-self-sm-start{align-self:flex-start!important}.align-self-sm-end{align-self:flex-end!important}.align-self-sm-center{align-self:center!important}.align-self-sm-baseline{align-self:baseline!important}.align-self-sm-stretch{align-self:stretch!important}}@media (min-width:768px){.flex-md-row{flex-direction:row!important}.flex-md-column{flex-direction:column!important}.flex-md-row-reverse{flex-direction:row-reverse!important}.flex-md-column-reverse{flex-direction:column-reverse!important}.flex-md-wrap{flex-wrap:wrap!important}.flex-md-nowrap{flex-wrap:nowrap!important}.flex-md-wrap-reverse{flex-wrap:wrap-reverse!important}.flex-md-fill{flex:1 1 auto!important}.flex-md-grow-0{flex-grow:0!important}.flex-md-grow-1{flex-grow:1!important}.flex-md-shrink-0{flex-shrink:0!important}.flex-md-shrink-1{flex-shrink:1!important}.justify-content-md-start{justify-content:flex-start!important}.justify-content-md-end{justify-content:flex-end!important}.justify-content-md-center{justify-content:center!important}.justify-content-md-between{justify-content:space-between!important}.justify-content-md-around{justify-content:space-around!important}.align-items-md-start{align-items:flex-start!important}.align-items-md-end{align-items:flex-end!important}.align-items-md-center{align-items:center!important}.align-items-md-baseline{align-items:baseline!important}.align-items-md-stretch{align-items:stretch!important}.align-content-md-start{align-content:flex-start!important}.align-content-md-end{align-content:flex-end!important}.align-content-md-center{align-content:center!important}.align-content-md-between{align-content:space-between!important}.align-content-md-around{align-content:space-around!important}.align-content-md-stretch{align-content:stretch!important}.align-self-md-auto{align-self:auto!important}.align-self-md-start{align-self:flex-start!important}.align-self-md-end{align-self:flex-end!important}.align-self-md-center{align-self:center!important}.align-self-md-baseline{align-self:baseline!important}.align-self-md-stretch{align-self:stretch!important}}@media (min-width:992px){.flex-lg-row{flex-direction:row!important}.flex-lg-column{flex-direction:column!important}.flex-lg-row-reverse{flex-direction:row-reverse!important}.flex-lg-column-reverse{flex-direction:column-reverse!important}.flex-lg-wrap{flex-wrap:wrap!important}.flex-lg-nowrap{flex-wrap:nowrap!important}.flex-lg-wrap-reverse{flex-wrap:wrap-reverse!important}.flex-lg-fill{flex:1 1 auto!important}.flex-lg-grow-0{flex-grow:0!important}.flex-lg-grow-1{flex-grow:1!important}.flex-lg-shrink-0{flex-shrink:0!important}.flex-lg-shrink-1{flex-shrink:1!important}.justify-content-lg-start{justify-content:flex-start!important}.justify-content-lg-end{justify-content:flex-end!important}.justify-content-lg-center{justify-content:center!important}.justify-content-lg-between{justify-content:space-between!important}.justify-content-lg-around{justify-content:space-around!important}.align-items-lg-start{align-items:flex-start!important}.align-items-lg-end{align-items:flex-end!important}.align-items-lg-center{align-items:center!important}.align-items-lg-baseline{align-items:baseline!important}.align-items-lg-stretch{align-items:stretch!important}.align-content-lg-start{align-content:flex-start!important}.align-content-lg-end{align-content:flex-end!important}.align-content-lg-center{align-content:center!important}.align-content-lg-between{align-content:space-between!important}.align-content-lg-around{align-content:space-around!important}.align-content-lg-stretch{align-content:stretch!important}.align-self-lg-auto{align-self:auto!important}.align-self-lg-start{align-self:flex-start!important}.align-self-lg-end{align-self:flex-end!important}.align-self-lg-center{align-self:center!important}.align-self-lg-baseline{align-self:baseline!important}.align-self-lg-stretch{align-self:stretch!important}}@media (min-width:1200px){.flex-xl-row{flex-direction:row!important}.flex-xl-column{flex-direction:column!important}.flex-xl-row-reverse{flex-direction:row-reverse!important}.flex-xl-column-reverse{flex-direction:column-reverse!important}.flex-xl-wrap{flex-wrap:wrap!important}.flex-xl-nowrap{flex-wrap:nowrap!important}.flex-xl-wrap-reverse{flex-wrap:wrap-reverse!important}.flex-xl-fill{flex:1 1 auto!important}.flex-xl-grow-0{flex-grow:0!important}.flex-xl-grow-1{flex-grow:1!important}.flex-xl-shrink-0{flex-shrink:0!important}.flex-xl-shrink-1{flex-shrink:1!important}.justify-content-xl-start{justify-content:flex-start!important}.justify-content-xl-end{justify-content:flex-end!important}.justify-content-xl-center{justify-content:center!important}.justify-content-xl-between{justify-content:space-between!important}.justify-content-xl-around{justify-content:space-around!important}.align-items-xl-start{align-items:flex-start!important}.align-items-xl-end{align-items:flex-end!important}.align-items-xl-center{align-items:center!important}.align-items-xl-baseline{align-items:baseline!important}.align-items-xl-stretch{align-items:stretch!important}.align-content-xl-start{align-content:flex-start!important}.align-content-xl-end{align-content:flex-end!important}.align-content-xl-center{align-content:center!important}.align-content-xl-between{align-content:space-between!important}.align-content-xl-around{align-content:space-around!important}.align-content-xl-stretch{align-content:stretch!important}.align-self-xl-auto{align-self:auto!important}.align-self-xl-start{align-self:flex-start!important}.align-self-xl-end{align-self:flex-end!important}.align-self-xl-center{align-self:center!important}.align-self-xl-baseline{align-self:baseline!important}.align-self-xl-stretch{align-self:stretch!important}}.float-left{float:left!important}.float-right{float:right!important}.float-none{float:none!important}@media (min-width:576px){.float-sm-left{float:left!important}.float-sm-right{float:right!important}.float-sm-none{float:none!important}}@media (min-width:768px){.float-md-left{float:left!important}.float-md-right{float:right!important}.float-md-none{float:none!important}}@media (min-width:992px){.float-lg-left{float:left!important}.float-lg-right{float:right!important}.float-lg-none{float:none!important}}@media (min-width:1200px){.float-xl-left{float:left!important}.float-xl-right{float:right!important}.float-xl-none{float:none!important}}.overflow-auto{overflow:auto!important}.overflow-hidden{overflow:hidden!important}.position-static{position:static!important}.position-relative{position:relative!important}.position-absolute{position:absolute!important}.position-fixed{position:fixed!important}.position-sticky{position:sticky!important}.fixed-top{position:fixed;top:0;right:0;left:0;z-index:1030}.fixed-bottom{position:fixed;right:0;bottom:0;left:0;z-index:1030}@supports (position:sticky){.sticky-top{position:sticky;top:0;z-index:1020}}.sr-only{position:absolute;width:1px;height:1px;padding:0;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;overflow:visible;clip:auto;white-space:normal}.shadow-sm{box-shadow:0 .125rem .25rem rgba(34,34,34,.075)!important}.shadow{box-shadow:0 .5rem 1rem rgba(34,34,34,.15)!important}.shadow-lg{box-shadow:0 1rem 3rem rgba(34,34,34,.175)!important}.shadow-none{box-shadow:none!important}.w-25{width:25%!important}.w-50{width:50%!important}.w-75{width:75%!important}.w-100{width:100%!important}.w-auto{width:auto!important}.h-25{height:25%!important}.h-50{height:50%!important}.h-75{height:75%!important}.h-100{height:100%!important}.h-auto{height:auto!important}.mw-100{max-width:100%!important}.mh-100{max-height:100%!important}.min-vw-100{min-width:100vw!important}.min-vh-100{min-height:100vh!important}.vw-100{width:100vw!important}.vh-100{height:100vh!important}.stretched-link::after{position:absolute;top:0;right:0;bottom:0;left:0;z-index:1;pointer-events:auto;content:"";background-color:rgba(0,0,0,0)}.m-0{margin:0!important}.mt-0,.my-0{margin-top:0!important}.mr-0,.mx-0{margin-right:0!important}.mb-0,.my-0{margin-bottom:0!important}.ml-0,.mx-0{margin-left:0!important}.m-1{margin:.25rem!important}.mt-1,.my-1{margin-top:.25rem!important}.mr-1,.mx-1{margin-right:.25rem!important}.mb-1,.my-1{margin-bottom:.25rem!important}.ml-1,.mx-1{margin-left:.25rem!important}.m-2{margin:.5rem!important}.mt-2,.my-2{margin-top:.5rem!important}.mr-2,.mx-2{margin-right:.5rem!important}.mb-2,.my-2{margin-bottom:.5rem!important}.ml-2,.mx-2{margin-left:.5rem!important}.m-3{margin:1rem!important}.mt-3,.my-3{margin-top:1rem!important}.mr-3,.mx-3{margin-right:1rem!important}.mb-3,.my-3{margin-bottom:1rem!important}.ml-3,.mx-3{margin-left:1rem!important}.m-4{margin:1.5rem!important}.mt-4,.my-4{margin-top:1.5rem!important}.mr-4,.mx-4{margin-right:1.5rem!important}.mb-4,.my-4{margin-bottom:1.5rem!important}.ml-4,.mx-4{margin-left:1.5rem!important}.m-5{margin:3rem!important}.mt-5,.my-5{margin-top:3rem!important}.mr-5,.mx-5{margin-right:3rem!important}.mb-5,.my-5{margin-bottom:3rem!important}.ml-5,.mx-5{margin-left:3rem!important}.p-0{padding:0!important}.pt-0,.py-0{padding-top:0!important}.pr-0,.px-0{padding-right:0!important}.pb-0,.py-0{padding-bottom:0!important}.pl-0,.px-0{padding-left:0!important}.p-1{padding:.25rem!important}.pt-1,.py-1{padding-top:.25rem!important}.pr-1,.px-1{padding-right:.25rem!important}.pb-1,.py-1{padding-bottom:.25rem!important}.pl-1,.px-1{padding-left:.25rem!important}.p-2{padding:.5rem!important}.pt-2,.py-2{padding-top:.5rem!important}.pr-2,.px-2{padding-right:.5rem!important}.pb-2,.py-2{padding-bottom:.5rem!important}.pl-2,.px-2{padding-left:.5rem!important}.p-3{padding:1rem!important}.pt-3,.py-3{padding-top:1rem!important}.pr-3,.px-3{padding-right:1rem!important}.pb-3,.py-3{padding-bottom:1rem!important}.pl-3,.px-3{padding-left:1rem!important}.p-4{padding:1.5rem!important}.pt-4,.py-4{padding-top:1.5rem!important}.pr-4,.px-4{padding-right:1.5rem!important}.pb-4,.py-4{padding-bottom:1.5rem!important}.pl-4,.px-4{padding-left:1.5rem!important}.p-5{padding:3rem!important}.pt-5,.py-5{padding-top:3rem!important}.pr-5,.px-5{padding-right:3rem!important}.pb-5,.py-5{padding-bottom:3rem!important}.pl-5,.px-5{padding-left:3rem!important}.m-n1{margin:-.25rem!important}.mt-n1,.my-n1{margin-top:-.25rem!important}.mr-n1,.mx-n1{margin-right:-.25rem!important}.mb-n1,.my-n1{margin-bottom:-.25rem!important}.ml-n1,.mx-n1{margin-left:-.25rem!important}.m-n2{margin:-.5rem!important}.mt-n2,.my-n2{margin-top:-.5rem!important}.mr-n2,.mx-n2{margin-right:-.5rem!important}.mb-n2,.my-n2{margin-bottom:-.5rem!important}.ml-n2,.mx-n2{margin-left:-.5rem!important}.m-n3{margin:-1rem!important}.mt-n3,.my-n3{margin-top:-1rem!important}.mr-n3,.mx-n3{margin-right:-1rem!important}.mb-n3,.my-n3{margin-bottom:-1rem!important}.ml-n3,.mx-n3{margin-left:-1rem!important}.m-n4{margin:-1.5rem!important}.mt-n4,.my-n4{margin-top:-1.5rem!important}.mr-n4,.mx-n4{margin-right:-1.5rem!important}.mb-n4,.my-n4{margin-bottom:-1.5rem!important}.ml-n4,.mx-n4{margin-left:-1.5rem!important}.m-n5{margin:-3rem!important}.mt-n5,.my-n5{margin-top:-3rem!important}.mr-n5,.mx-n5{margin-right:-3rem!important}.mb-n5,.my-n5{margin-bottom:-3rem!important}.ml-n5,.mx-n5{margin-left:-3rem!important}.m-auto{margin:auto!important}.mt-auto,.my-auto{margin-top:auto!important}.mr-auto,.mx-auto{margin-right:auto!important}.mb-auto,.my-auto{margin-bottom:auto!important}.ml-auto,.mx-auto{margin-left:auto!important}@media (min-width:576px){.m-sm-0{margin:0!important}.mt-sm-0,.my-sm-0{margin-top:0!important}.mr-sm-0,.mx-sm-0{margin-right:0!important}.mb-sm-0,.my-sm-0{margin-bottom:0!important}.ml-sm-0,.mx-sm-0{margin-left:0!important}.m-sm-1{margin:.25rem!important}.mt-sm-1,.my-sm-1{margin-top:.25rem!important}.mr-sm-1,.mx-sm-1{margin-right:.25rem!important}.mb-sm-1,.my-sm-1{margin-bottom:.25rem!important}.ml-sm-1,.mx-sm-1{margin-left:.25rem!important}.m-sm-2{margin:.5rem!important}.mt-sm-2,.my-sm-2{margin-top:.5rem!important}.mr-sm-2,.mx-sm-2{margin-right:.5rem!important}.mb-sm-2,.my-sm-2{margin-bottom:.5rem!important}.ml-sm-2,.mx-sm-2{margin-left:.5rem!important}.m-sm-3{margin:1rem!important}.mt-sm-3,.my-sm-3{margin-top:1rem!important}.mr-sm-3,.mx-sm-3{margin-right:1rem!important}.mb-sm-3,.my-sm-3{margin-bottom:1rem!important}.ml-sm-3,.mx-sm-3{margin-left:1rem!important}.m-sm-4{margin:1.5rem!important}.mt-sm-4,.my-sm-4{margin-top:1.5rem!important}.mr-sm-4,.mx-sm-4{margin-right:1.5rem!important}.mb-sm-4,.my-sm-4{margin-bottom:1.5rem!important}.ml-sm-4,.mx-sm-4{margin-left:1.5rem!important}.m-sm-5{margin:3rem!important}.mt-sm-5,.my-sm-5{margin-top:3rem!important}.mr-sm-5,.mx-sm-5{margin-right:3rem!important}.mb-sm-5,.my-sm-5{margin-bottom:3rem!important}.ml-sm-5,.mx-sm-5{margin-left:3rem!important}.p-sm-0{padding:0!important}.pt-sm-0,.py-sm-0{padding-top:0!important}.pr-sm-0,.px-sm-0{padding-right:0!important}.pb-sm-0,.py-sm-0{padding-bottom:0!important}.pl-sm-0,.px-sm-0{padding-left:0!important}.p-sm-1{padding:.25rem!important}.pt-sm-1,.py-sm-1{padding-top:.25rem!important}.pr-sm-1,.px-sm-1{padding-right:.25rem!important}.pb-sm-1,.py-sm-1{padding-bottom:.25rem!important}.pl-sm-1,.px-sm-1{padding-left:.25rem!important}.p-sm-2{padding:.5rem!important}.pt-sm-2,.py-sm-2{padding-top:.5rem!important}.pr-sm-2,.px-sm-2{padding-right:.5rem!important}.pb-sm-2,.py-sm-2{padding-bottom:.5rem!important}.pl-sm-2,.px-sm-2{padding-left:.5rem!important}.p-sm-3{padding:1rem!important}.pt-sm-3,.py-sm-3{padding-top:1rem!important}.pr-sm-3,.px-sm-3{padding-right:1rem!important}.pb-sm-3,.py-sm-3{padding-bottom:1rem!important}.pl-sm-3,.px-sm-3{padding-left:1rem!important}.p-sm-4{padding:1.5rem!important}.pt-sm-4,.py-sm-4{padding-top:1.5rem!important}.pr-sm-4,.px-sm-4{padding-right:1.5rem!important}.pb-sm-4,.py-sm-4{padding-bottom:1.5rem!important}.pl-sm-4,.px-sm-4{padding-left:1.5rem!important}.p-sm-5{padding:3rem!important}.pt-sm-5,.py-sm-5{padding-top:3rem!important}.pr-sm-5,.px-sm-5{padding-right:3rem!important}.pb-sm-5,.py-sm-5{padding-bottom:3rem!important}.pl-sm-5,.px-sm-5{padding-left:3rem!important}.m-sm-n1{margin:-.25rem!important}.mt-sm-n1,.my-sm-n1{margin-top:-.25rem!important}.mr-sm-n1,.mx-sm-n1{margin-right:-.25rem!important}.mb-sm-n1,.my-sm-n1{margin-bottom:-.25rem!important}.ml-sm-n1,.mx-sm-n1{margin-left:-.25rem!important}.m-sm-n2{margin:-.5rem!important}.mt-sm-n2,.my-sm-n2{margin-top:-.5rem!important}.mr-sm-n2,.mx-sm-n2{margin-right:-.5rem!important}.mb-sm-n2,.my-sm-n2{margin-bottom:-.5rem!important}.ml-sm-n2,.mx-sm-n2{margin-left:-.5rem!important}.m-sm-n3{margin:-1rem!important}.mt-sm-n3,.my-sm-n3{margin-top:-1rem!important}.mr-sm-n3,.mx-sm-n3{margin-right:-1rem!important}.mb-sm-n3,.my-sm-n3{margin-bottom:-1rem!important}.ml-sm-n3,.mx-sm-n3{margin-left:-1rem!important}.m-sm-n4{margin:-1.5rem!important}.mt-sm-n4,.my-sm-n4{margin-top:-1.5rem!important}.mr-sm-n4,.mx-sm-n4{margin-right:-1.5rem!important}.mb-sm-n4,.my-sm-n4{margin-bottom:-1.5rem!important}.ml-sm-n4,.mx-sm-n4{margin-left:-1.5rem!important}.m-sm-n5{margin:-3rem!important}.mt-sm-n5,.my-sm-n5{margin-top:-3rem!important}.mr-sm-n5,.mx-sm-n5{margin-right:-3rem!important}.mb-sm-n5,.my-sm-n5{margin-bottom:-3rem!important}.ml-sm-n5,.mx-sm-n5{margin-left:-3rem!important}.m-sm-auto{margin:auto!important}.mt-sm-auto,.my-sm-auto{margin-top:auto!important}.mr-sm-auto,.mx-sm-auto{margin-right:auto!important}.mb-sm-auto,.my-sm-auto{margin-bottom:auto!important}.ml-sm-auto,.mx-sm-auto{margin-left:auto!important}}@media (min-width:768px){.m-md-0{margin:0!important}.mt-md-0,.my-md-0{margin-top:0!important}.mr-md-0,.mx-md-0{margin-right:0!important}.mb-md-0,.my-md-0{margin-bottom:0!important}.ml-md-0,.mx-md-0{margin-left:0!important}.m-md-1{margin:.25rem!important}.mt-md-1,.my-md-1{margin-top:.25rem!important}.mr-md-1,.mx-md-1{margin-right:.25rem!important}.mb-md-1,.my-md-1{margin-bottom:.25rem!important}.ml-md-1,.mx-md-1{margin-left:.25rem!important}.m-md-2{margin:.5rem!important}.mt-md-2,.my-md-2{margin-top:.5rem!important}.mr-md-2,.mx-md-2{margin-right:.5rem!important}.mb-md-2,.my-md-2{margin-bottom:.5rem!important}.ml-md-2,.mx-md-2{margin-left:.5rem!important}.m-md-3{margin:1rem!important}.mt-md-3,.my-md-3{margin-top:1rem!important}.mr-md-3,.mx-md-3{margin-right:1rem!important}.mb-md-3,.my-md-3{margin-bottom:1rem!important}.ml-md-3,.mx-md-3{margin-left:1rem!important}.m-md-4{margin:1.5rem!important}.mt-md-4,.my-md-4{margin-top:1.5rem!important}.mr-md-4,.mx-md-4{margin-right:1.5rem!important}.mb-md-4,.my-md-4{margin-bottom:1.5rem!important}.ml-md-4,.mx-md-4{margin-left:1.5rem!important}.m-md-5{margin:3rem!important}.mt-md-5,.my-md-5{margin-top:3rem!important}.mr-md-5,.mx-md-5{margin-right:3rem!important}.mb-md-5,.my-md-5{margin-bottom:3rem!important}.ml-md-5,.mx-md-5{margin-left:3rem!important}.p-md-0{padding:0!important}.pt-md-0,.py-md-0{padding-top:0!important}.pr-md-0,.px-md-0{padding-right:0!important}.pb-md-0,.py-md-0{padding-bottom:0!important}.pl-md-0,.px-md-0{padding-left:0!important}.p-md-1{padding:.25rem!important}.pt-md-1,.py-md-1{padding-top:.25rem!important}.pr-md-1,.px-md-1{padding-right:.25rem!important}.pb-md-1,.py-md-1{padding-bottom:.25rem!important}.pl-md-1,.px-md-1{padding-left:.25rem!important}.p-md-2{padding:.5rem!important}.pt-md-2,.py-md-2{padding-top:.5rem!important}.pr-md-2,.px-md-2{padding-right:.5rem!important}.pb-md-2,.py-md-2{padding-bottom:.5rem!important}.pl-md-2,.px-md-2{padding-left:.5rem!important}.p-md-3{padding:1rem!important}.pt-md-3,.py-md-3{padding-top:1rem!important}.pr-md-3,.px-md-3{padding-right:1rem!important}.pb-md-3,.py-md-3{padding-bottom:1rem!important}.pl-md-3,.px-md-3{padding-left:1rem!important}.p-md-4{padding:1.5rem!important}.pt-md-4,.py-md-4{padding-top:1.5rem!important}.pr-md-4,.px-md-4{padding-right:1.5rem!important}.pb-md-4,.py-md-4{padding-bottom:1.5rem!important}.pl-md-4,.px-md-4{padding-left:1.5rem!important}.p-md-5{padding:3rem!important}.pt-md-5,.py-md-5{padding-top:3rem!important}.pr-md-5,.px-md-5{padding-right:3rem!important}.pb-md-5,.py-md-5{padding-bottom:3rem!important}.pl-md-5,.px-md-5{padding-left:3rem!important}.m-md-n1{margin:-.25rem!important}.mt-md-n1,.my-md-n1{margin-top:-.25rem!important}.mr-md-n1,.mx-md-n1{margin-right:-.25rem!important}.mb-md-n1,.my-md-n1{margin-bottom:-.25rem!important}.ml-md-n1,.mx-md-n1{margin-left:-.25rem!important}.m-md-n2{margin:-.5rem!important}.mt-md-n2,.my-md-n2{margin-top:-.5rem!important}.mr-md-n2,.mx-md-n2{margin-right:-.5rem!important}.mb-md-n2,.my-md-n2{margin-bottom:-.5rem!important}.ml-md-n2,.mx-md-n2{margin-left:-.5rem!important}.m-md-n3{margin:-1rem!important}.mt-md-n3,.my-md-n3{margin-top:-1rem!important}.mr-md-n3,.mx-md-n3{margin-right:-1rem!important}.mb-md-n3,.my-md-n3{margin-bottom:-1rem!important}.ml-md-n3,.mx-md-n3{margin-left:-1rem!important}.m-md-n4{margin:-1.5rem!important}.mt-md-n4,.my-md-n4{margin-top:-1.5rem!important}.mr-md-n4,.mx-md-n4{margin-right:-1.5rem!important}.mb-md-n4,.my-md-n4{margin-bottom:-1.5rem!important}.ml-md-n4,.mx-md-n4{margin-left:-1.5rem!important}.m-md-n5{margin:-3rem!important}.mt-md-n5,.my-md-n5{margin-top:-3rem!important}.mr-md-n5,.mx-md-n5{margin-right:-3rem!important}.mb-md-n5,.my-md-n5{margin-bottom:-3rem!important}.ml-md-n5,.mx-md-n5{margin-left:-3rem!important}.m-md-auto{margin:auto!important}.mt-md-auto,.my-md-auto{margin-top:auto!important}.mr-md-auto,.mx-md-auto{margin-right:auto!important}.mb-md-auto,.my-md-auto{margin-bottom:auto!important}.ml-md-auto,.mx-md-auto{margin-left:auto!important}}@media (min-width:992px){.m-lg-0{margin:0!important}.mt-lg-0,.my-lg-0{margin-top:0!important}.mr-lg-0,.mx-lg-0{margin-right:0!important}.mb-lg-0,.my-lg-0{margin-bottom:0!important}.ml-lg-0,.mx-lg-0{margin-left:0!important}.m-lg-1{margin:.25rem!important}.mt-lg-1,.my-lg-1{margin-top:.25rem!important}.mr-lg-1,.mx-lg-1{margin-right:.25rem!important}.mb-lg-1,.my-lg-1{margin-bottom:.25rem!important}.ml-lg-1,.mx-lg-1{margin-left:.25rem!important}.m-lg-2{margin:.5rem!important}.mt-lg-2,.my-lg-2{margin-top:.5rem!important}.mr-lg-2,.mx-lg-2{margin-right:.5rem!important}.mb-lg-2,.my-lg-2{margin-bottom:.5rem!important}.ml-lg-2,.mx-lg-2{margin-left:.5rem!important}.m-lg-3{margin:1rem!important}.mt-lg-3,.my-lg-3{margin-top:1rem!important}.mr-lg-3,.mx-lg-3{margin-right:1rem!important}.mb-lg-3,.my-lg-3{margin-bottom:1rem!important}.ml-lg-3,.mx-lg-3{margin-left:1rem!important}.m-lg-4{margin:1.5rem!important}.mt-lg-4,.my-lg-4{margin-top:1.5rem!important}.mr-lg-4,.mx-lg-4{margin-right:1.5rem!important}.mb-lg-4,.my-lg-4{margin-bottom:1.5rem!important}.ml-lg-4,.mx-lg-4{margin-left:1.5rem!important}.m-lg-5{margin:3rem!important}.mt-lg-5,.my-lg-5{margin-top:3rem!important}.mr-lg-5,.mx-lg-5{margin-right:3rem!important}.mb-lg-5,.my-lg-5{margin-bottom:3rem!important}.ml-lg-5,.mx-lg-5{margin-left:3rem!important}.p-lg-0{padding:0!important}.pt-lg-0,.py-lg-0{padding-top:0!important}.pr-lg-0,.px-lg-0{padding-right:0!important}.pb-lg-0,.py-lg-0{padding-bottom:0!important}.pl-lg-0,.px-lg-0{padding-left:0!important}.p-lg-1{padding:.25rem!important}.pt-lg-1,.py-lg-1{padding-top:.25rem!important}.pr-lg-1,.px-lg-1{padding-right:.25rem!important}.pb-lg-1,.py-lg-1{padding-bottom:.25rem!important}.pl-lg-1,.px-lg-1{padding-left:.25rem!important}.p-lg-2{padding:.5rem!important}.pt-lg-2,.py-lg-2{padding-top:.5rem!important}.pr-lg-2,.px-lg-2{padding-right:.5rem!important}.pb-lg-2,.py-lg-2{padding-bottom:.5rem!important}.pl-lg-2,.px-lg-2{padding-left:.5rem!important}.p-lg-3{padding:1rem!important}.pt-lg-3,.py-lg-3{padding-top:1rem!important}.pr-lg-3,.px-lg-3{padding-right:1rem!important}.pb-lg-3,.py-lg-3{padding-bottom:1rem!important}.pl-lg-3,.px-lg-3{padding-left:1rem!important}.p-lg-4{padding:1.5rem!important}.pt-lg-4,.py-lg-4{padding-top:1.5rem!important}.pr-lg-4,.px-lg-4{padding-right:1.5rem!important}.pb-lg-4,.py-lg-4{padding-bottom:1.5rem!important}.pl-lg-4,.px-lg-4{padding-left:1.5rem!important}.p-lg-5{padding:3rem!important}.pt-lg-5,.py-lg-5{padding-top:3rem!important}.pr-lg-5,.px-lg-5{padding-right:3rem!important}.pb-lg-5,.py-lg-5{padding-bottom:3rem!important}.pl-lg-5,.px-lg-5{padding-left:3rem!important}.m-lg-n1{margin:-.25rem!important}.mt-lg-n1,.my-lg-n1{margin-top:-.25rem!important}.mr-lg-n1,.mx-lg-n1{margin-right:-.25rem!important}.mb-lg-n1,.my-lg-n1{margin-bottom:-.25rem!important}.ml-lg-n1,.mx-lg-n1{margin-left:-.25rem!important}.m-lg-n2{margin:-.5rem!important}.mt-lg-n2,.my-lg-n2{margin-top:-.5rem!important}.mr-lg-n2,.mx-lg-n2{margin-right:-.5rem!important}.mb-lg-n2,.my-lg-n2{margin-bottom:-.5rem!important}.ml-lg-n2,.mx-lg-n2{margin-left:-.5rem!important}.m-lg-n3{margin:-1rem!important}.mt-lg-n3,.my-lg-n3{margin-top:-1rem!important}.mr-lg-n3,.mx-lg-n3{margin-right:-1rem!important}.mb-lg-n3,.my-lg-n3{margin-bottom:-1rem!important}.ml-lg-n3,.mx-lg-n3{margin-left:-1rem!important}.m-lg-n4{margin:-1.5rem!important}.mt-lg-n4,.my-lg-n4{margin-top:-1.5rem!important}.mr-lg-n4,.mx-lg-n4{margin-right:-1.5rem!important}.mb-lg-n4,.my-lg-n4{margin-bottom:-1.5rem!important}.ml-lg-n4,.mx-lg-n4{margin-left:-1.5rem!important}.m-lg-n5{margin:-3rem!important}.mt-lg-n5,.my-lg-n5{margin-top:-3rem!important}.mr-lg-n5,.mx-lg-n5{margin-right:-3rem!important}.mb-lg-n5,.my-lg-n5{margin-bottom:-3rem!important}.ml-lg-n5,.mx-lg-n5{margin-left:-3rem!important}.m-lg-auto{margin:auto!important}.mt-lg-auto,.my-lg-auto{margin-top:auto!important}.mr-lg-auto,.mx-lg-auto{margin-right:auto!important}.mb-lg-auto,.my-lg-auto{margin-bottom:auto!important}.ml-lg-auto,.mx-lg-auto{margin-left:auto!important}}@media (min-width:1200px){.m-xl-0{margin:0!important}.mt-xl-0,.my-xl-0{margin-top:0!important}.mr-xl-0,.mx-xl-0{margin-right:0!important}.mb-xl-0,.my-xl-0{margin-bottom:0!important}.ml-xl-0,.mx-xl-0{margin-left:0!important}.m-xl-1{margin:.25rem!important}.mt-xl-1,.my-xl-1{margin-top:.25rem!important}.mr-xl-1,.mx-xl-1{margin-right:.25rem!important}.mb-xl-1,.my-xl-1{margin-bottom:.25rem!important}.ml-xl-1,.mx-xl-1{margin-left:.25rem!important}.m-xl-2{margin:.5rem!important}.mt-xl-2,.my-xl-2{margin-top:.5rem!important}.mr-xl-2,.mx-xl-2{margin-right:.5rem!important}.mb-xl-2,.my-xl-2{margin-bottom:.5rem!important}.ml-xl-2,.mx-xl-2{margin-left:.5rem!important}.m-xl-3{margin:1rem!important}.mt-xl-3,.my-xl-3{margin-top:1rem!important}.mr-xl-3,.mx-xl-3{margin-right:1rem!important}.mb-xl-3,.my-xl-3{margin-bottom:1rem!important}.ml-xl-3,.mx-xl-3{margin-left:1rem!important}.m-xl-4{margin:1.5rem!important}.mt-xl-4,.my-xl-4{margin-top:1.5rem!important}.mr-xl-4,.mx-xl-4{margin-right:1.5rem!important}.mb-xl-4,.my-xl-4{margin-bottom:1.5rem!important}.ml-xl-4,.mx-xl-4{margin-left:1.5rem!important}.m-xl-5{margin:3rem!important}.mt-xl-5,.my-xl-5{margin-top:3rem!important}.mr-xl-5,.mx-xl-5{margin-right:3rem!important}.mb-xl-5,.my-xl-5{margin-bottom:3rem!important}.ml-xl-5,.mx-xl-5{margin-left:3rem!important}.p-xl-0{padding:0!important}.pt-xl-0,.py-xl-0{padding-top:0!important}.pr-xl-0,.px-xl-0{padding-right:0!important}.pb-xl-0,.py-xl-0{padding-bottom:0!important}.pl-xl-0,.px-xl-0{padding-left:0!important}.p-xl-1{padding:.25rem!important}.pt-xl-1,.py-xl-1{padding-top:.25rem!important}.pr-xl-1,.px-xl-1{padding-right:.25rem!important}.pb-xl-1,.py-xl-1{padding-bottom:.25rem!important}.pl-xl-1,.px-xl-1{padding-left:.25rem!important}.p-xl-2{padding:.5rem!important}.pt-xl-2,.py-xl-2{padding-top:.5rem!important}.pr-xl-2,.px-xl-2{padding-right:.5rem!important}.pb-xl-2,.py-xl-2{padding-bottom:.5rem!important}.pl-xl-2,.px-xl-2{padding-left:.5rem!important}.p-xl-3{padding:1rem!important}.pt-xl-3,.py-xl-3{padding-top:1rem!important}.pr-xl-3,.px-xl-3{padding-right:1rem!important}.pb-xl-3,.py-xl-3{padding-bottom:1rem!important}.pl-xl-3,.px-xl-3{padding-left:1rem!important}.p-xl-4{padding:1.5rem!important}.pt-xl-4,.py-xl-4{padding-top:1.5rem!important}.pr-xl-4,.px-xl-4{padding-right:1.5rem!important}.pb-xl-4,.py-xl-4{padding-bottom:1.5rem!important}.pl-xl-4,.px-xl-4{padding-left:1.5rem!important}.p-xl-5{padding:3rem!important}.pt-xl-5,.py-xl-5{padding-top:3rem!important}.pr-xl-5,.px-xl-5{padding-right:3rem!important}.pb-xl-5,.py-xl-5{padding-bottom:3rem!important}.pl-xl-5,.px-xl-5{padding-left:3rem!important}.m-xl-n1{margin:-.25rem!important}.mt-xl-n1,.my-xl-n1{margin-top:-.25rem!important}.mr-xl-n1,.mx-xl-n1{margin-right:-.25rem!important}.mb-xl-n1,.my-xl-n1{margin-bottom:-.25rem!important}.ml-xl-n1,.mx-xl-n1{margin-left:-.25rem!important}.m-xl-n2{margin:-.5rem!important}.mt-xl-n2,.my-xl-n2{margin-top:-.5rem!important}.mr-xl-n2,.mx-xl-n2{margin-right:-.5rem!important}.mb-xl-n2,.my-xl-n2{margin-bottom:-.5rem!important}.ml-xl-n2,.mx-xl-n2{margin-left:-.5rem!important}.m-xl-n3{margin:-1rem!important}.mt-xl-n3,.my-xl-n3{margin-top:-1rem!important}.mr-xl-n3,.mx-xl-n3{margin-right:-1rem!important}.mb-xl-n3,.my-xl-n3{margin-bottom:-1rem!important}.ml-xl-n3,.mx-xl-n3{margin-left:-1rem!important}.m-xl-n4{margin:-1.5rem!important}.mt-xl-n4,.my-xl-n4{margin-top:-1.5rem!important}.mr-xl-n4,.mx-xl-n4{margin-right:-1.5rem!important}.mb-xl-n4,.my-xl-n4{margin-bottom:-1.5rem!important}.ml-xl-n4,.mx-xl-n4{margin-left:-1.5rem!important}.m-xl-n5{margin:-3rem!important}.mt-xl-n5,.my-xl-n5{margin-top:-3rem!important}.mr-xl-n5,.mx-xl-n5{margin-right:-3rem!important}.mb-xl-n5,.my-xl-n5{margin-bottom:-3rem!important}.ml-xl-n5,.mx-xl-n5{margin-left:-3rem!important}.m-xl-auto{margin:auto!important}.mt-xl-auto,.my-xl-auto{margin-top:auto!important}.mr-xl-auto,.mx-xl-auto{margin-right:auto!important}.mb-xl-auto,.my-xl-auto{margin-bottom:auto!important}.ml-xl-auto,.mx-xl-auto{margin-left:auto!important}}.text-monospace{font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace!important}.text-justify{text-align:justify!important}.text-wrap{white-space:normal!important}.text-nowrap{white-space:nowrap!important}.text-truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.text-left{text-align:left!important}.text-right{text-align:right!important}.text-center{text-align:center!important}@media (min-width:576px){.text-sm-left{text-align:left!important}.text-sm-right{text-align:right!important}.text-sm-center{text-align:center!important}}@media (min-width:768px){.text-md-left{text-align:left!important}.text-md-right{text-align:right!important}.text-md-center{text-align:center!important}}@media (min-width:992px){.text-lg-left{text-align:left!important}.text-lg-right{text-align:right!important}.text-lg-center{text-align:center!important}}@media (min-width:1200px){.text-xl-left{text-align:left!important}.text-xl-right{text-align:right!important}.text-xl-center{text-align:center!important}}.text-lowercase{text-transform:lowercase!important}.text-uppercase{text-transform:uppercase!important}.text-capitalize{text-transform:capitalize!important}.font-weight-light{font-weight:300!important}.font-weight-lighter{font-weight:lighter!important}.font-weight-normal{font-weight:400!important}.font-weight-bold{font-weight:700!important}.font-weight-bolder{font-weight:bolder!important}.font-italic{font-style:italic!important}.text-white{color:#fff!important}.text-primary{color:#fac57c!important}a.text-primary:focus,a.text-primary:hover{color:#f7a432!important}.text-secondary{color:#bad2be!important}a.text-secondary:focus,a.text-secondary:hover{color:#8cb492!important}.text-success{color:#38553d!important}a.text-success:focus,a.text-success:hover{color:#19271c!important}.text-info{color:#49ce5f!important}a.text-info:focus,a.text-info:hover{color:#2ba03e!important}.text-warning{color:#ffc107!important}a.text-warning:focus,a.text-warning:hover{color:#ba8b00!important}.text-danger{color:#ed8d09!important}a.text-danger:focus,a.text-danger:hover{color:#a46206!important}.text-light{color:#f8f9fa!important}a.text-light:focus,a.text-light:hover{color:#cbd3da!important}.text-dark{color:#343a40!important}a.text-dark:focus,a.text-dark:hover{color:#121416!important}.text-body{color:#495057!important}.text-muted{color:#6c757d!important}.text-black-50{color:rgba(34,34,34,.5)!important}.text-white-50{color:rgba(255,255,255,.5)!important}.text-hide{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.text-decoration-none{text-decoration:none!important}.text-break{word-break:break-word!important;overflow-wrap:break-word!important}.text-reset{color:inherit!important}.visible{visibility:visible!important}.invisible{visibility:hidden!important}@media print{*,::after,::before{text-shadow:none!important;box-shadow:none!important}a:not(.btn){text-decoration:underline}abbr[title]::after{content:" (" attr(title) ")"}pre{white-space:pre-wrap!important}blockquote,pre{border:1px solid #adb5bd;page-break-inside:avoid}thead{display:table-header-group}img,tr{page-break-inside:avoid}h2,h3,p{orphans:3;widows:3}h2,h3{page-break-after:avoid}@page{size:a3}body{min-width:992px!important}.container{min-width:992px!important}.navbar{display:none}.badge{border:1px solid #222}.table{border-collapse:collapse!important}.table td,.table th{background-color:#fff!important}.table-bordered td,.table-bordered th{border:1px solid #dee2e6!important}.table-dark{color:inherit}.table-dark tbody+tbody,.table-dark td,.table-dark th,.table-dark thead th{border-color:#495057}.table .thead-dark th{color:inherit;border-color:#495057}}
\ No newline at end of file
+:root{--blue:#007bff;--indigo:#6610f2;--purple:#6f42c1;--pink:#e83e8c;--red:#d8486a;--orange:#f1641e;--yellow:#ffc107;--green:#00C853;--teal:#20c997;--cyan:#02bdc2;--white:#ffffff;--gray:#6c757d;--gray-dark:#343a40;--primary:#f1641e;--secondary:#00C853;--success:#6610f2;--info:#007bff;--warning:#ffc107;--danger:#873208;--light:#f8f9fa;--dark:#343a40;--breakpoint-xs:0;--breakpoint-sm:576px;--breakpoint-md:768px;--breakpoint-lg:992px;--breakpoint-xl:1200px;--font-family-sans-serif:-apple-system,BlinkMacSystemFont,"Droid Sans","Segoe UI","Helvetica",Arial,sans-serif;--font-family-monospace:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace}*,::after,::before{box-sizing:border-box}html{font-family:sans-serif;line-height:1.15;-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:rgba(34,34,34,0)}article,aside,figcaption,figure,footer,header,hgroup,main,nav,section{display:block}body{margin:0;font-family:-apple-system,BlinkMacSystemFont,"Droid Sans","Segoe UI",Helvetica,Arial,sans-serif;font-size:1rem;font-weight:400;line-height:1.5;color:#495057;text-align:left;background-color:#fff}[tabindex="-1"]:focus:not(:focus-visible){outline:0!important}hr{box-sizing:content-box;height:0;overflow:visible}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem}p{margin-top:0;margin-bottom:1rem}abbr[data-original-title],abbr[title]{text-decoration:underline;text-decoration:underline dotted;cursor:help;border-bottom:0;text-decoration-skip-ink:none}address{margin-bottom:1rem;font-style:normal;line-height:inherit}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:600}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}b,strong{font-weight:bolder}small{font-size:80%}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:#f1641e;text-decoration:none;background-color:transparent}a:hover{color:#b7440b;text-decoration:underline}a:not([href]){color:inherit;text-decoration:none}a:not([href]):hover{color:inherit;text-decoration:none}code,kbd,pre,samp{font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;font-size:1em}pre{margin-top:0;margin-bottom:1rem;overflow:auto;-ms-overflow-style:scrollbar}figure{margin:0 0 1rem}img{vertical-align:middle;border-style:none}svg{overflow:hidden;vertical-align:middle}table{border-collapse:collapse}caption{padding-top:.75rem;padding-bottom:.75rem;color:#6c757d;text-align:left;caption-side:bottom}th{text-align:inherit}label{display:inline-block;margin-bottom:.5rem}button{border-radius:0}button:focus{outline:1px dotted;outline:5px auto -webkit-focus-ring-color}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,input{overflow:visible}button,select{text-transform:none}[role=button]{cursor:pointer}select{word-wrap:normal}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]:not(:disabled),[type=reset]:not(:disabled),[type=submit]:not(:disabled),button:not(:disabled){cursor:pointer}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{padding:0;border-style:none}input[type=checkbox],input[type=radio]{box-sizing:border-box;padding:0}textarea{overflow:auto;resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{display:block;width:100%;max-width:100%;padding:0;margin-bottom:.5rem;font-size:1.5rem;line-height:inherit;color:inherit;white-space:normal}progress{vertical-align:baseline}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:none}[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}output{display:inline-block}summary{display:list-item;cursor:pointer}template{display:none}[hidden]{display:none!important}.h1,.h2,.h3,.h4,.h5,.h6,h1,h2,h3,h4,h5,h6{margin-bottom:.5rem;font-weight:500;line-height:1.2;color:#495057}.h1,h1{font-size:2.5rem}.h2,h2{font-size:2rem}.h3,h3{font-size:1.75rem}.h4,h4{font-size:1.5rem}.h5,h5{font-size:1.25rem}.h6,h6{font-size:1rem}.lead{font-size:1.25rem;font-weight:300}.display-1{font-size:6rem;font-weight:300;line-height:1.2}.display-2{font-size:5.5rem;font-weight:300;line-height:1.2}.display-3{font-size:4.5rem;font-weight:300;line-height:1.2}.display-4{font-size:3.5rem;font-weight:300;line-height:1.2}hr{margin-top:1rem;margin-bottom:1rem;border:0;border-top:1px solid rgba(34,34,34,.1)}.small,small{font-size:80%;font-weight:400}.mark,mark{padding:.2em;background-color:#fffcef}.list-unstyled{padding-left:0;list-style:none}.list-inline{padding-left:0;list-style:none}.list-inline-item{display:inline-block}.list-inline-item:not(:last-child){margin-right:.5rem}.initialism{font-size:90%;text-transform:uppercase}.blockquote{margin-bottom:1rem;font-size:1.25rem}.blockquote-footer{display:block;font-size:80%;color:#6c757d}.blockquote-footer::before{content:"\2014\00A0"}.img-fluid{max-width:100%;height:auto}.img-thumbnail{padding:.25rem;background-color:#fff;border:1px solid #dee2e6;border-radius:.5rem;max-width:100%;height:auto}.figure{display:inline-block}.figure-img{margin-bottom:.5rem;line-height:1}.figure-caption{font-size:90%;color:#6c757d}code{font-size:87.5%;color:#e83e8c;word-wrap:break-word}a>code{color:inherit}kbd{padding:.2rem .4rem;font-size:87.5%;color:#fff;background-color:#212529;border-radius:1rem}kbd kbd{padding:0;font-size:100%;font-weight:600}pre{display:block;font-size:87.5%;color:#212529}pre code{font-size:inherit;color:inherit;word-break:normal}.pre-scrollable{max-height:340px;overflow-y:scroll}.container{width:100%;padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}@media (min-width:576px){.container{max-width:540px}}@media (min-width:768px){.container{max-width:720px}}@media (min-width:992px){.container{max-width:960px}}@media (min-width:1200px){.container{max-width:1140px}}.container-fluid,.container-lg,.container-md,.container-sm,.container-xl{width:100%;padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}@media (min-width:576px){.container,.container-sm{max-width:540px}}@media (min-width:768px){.container,.container-md,.container-sm{max-width:720px}}@media (min-width:992px){.container,.container-lg,.container-md,.container-sm{max-width:960px}}@media (min-width:1200px){.container,.container-lg,.container-md,.container-sm,.container-xl{max-width:1140px}}.row{display:flex;flex-wrap:wrap;margin-right:-15px;margin-left:-15px}.no-gutters{margin-right:0;margin-left:0}.no-gutters>.col,.no-gutters>[class*=col-]{padding-right:0;padding-left:0}.col,.col-1,.col-10,.col-11,.col-12,.col-2,.col-3,.col-4,.col-5,.col-6,.col-7,.col-8,.col-9,.col-auto,.col-lg,.col-lg-1,.col-lg-10,.col-lg-11,.col-lg-12,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9,.col-lg-auto,.col-md,.col-md-1,.col-md-10,.col-md-11,.col-md-12,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9,.col-md-auto,.col-sm,.col-sm-1,.col-sm-10,.col-sm-11,.col-sm-12,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9,.col-sm-auto,.col-xl,.col-xl-1,.col-xl-10,.col-xl-11,.col-xl-12,.col-xl-2,.col-xl-3,.col-xl-4,.col-xl-5,.col-xl-6,.col-xl-7,.col-xl-8,.col-xl-9,.col-xl-auto{position:relative;width:100%;padding-right:15px;padding-left:15px}.col{flex-basis:0;flex-grow:1;min-width:0;max-width:100%}.row-cols-1>*{flex:0 0 100%;max-width:100%}.row-cols-2>*{flex:0 0 50%;max-width:50%}.row-cols-3>*{flex:0 0 33.33333%;max-width:33.33333%}.row-cols-4>*{flex:0 0 25%;max-width:25%}.row-cols-5>*{flex:0 0 20%;max-width:20%}.row-cols-6>*{flex:0 0 16.66667%;max-width:16.66667%}.col-auto{flex:0 0 auto;width:auto;max-width:100%}.col-1{flex:0 0 8.33333%;max-width:8.33333%}.col-2{flex:0 0 16.66667%;max-width:16.66667%}.col-3{flex:0 0 25%;max-width:25%}.col-4{flex:0 0 33.33333%;max-width:33.33333%}.col-5{flex:0 0 41.66667%;max-width:41.66667%}.col-6{flex:0 0 50%;max-width:50%}.col-7{flex:0 0 58.33333%;max-width:58.33333%}.col-8{flex:0 0 66.66667%;max-width:66.66667%}.col-9{flex:0 0 75%;max-width:75%}.col-10{flex:0 0 83.33333%;max-width:83.33333%}.col-11{flex:0 0 91.66667%;max-width:91.66667%}.col-12{flex:0 0 100%;max-width:100%}.order-first{order:-1}.order-last{order:13}.order-0{order:0}.order-1{order:1}.order-2{order:2}.order-3{order:3}.order-4{order:4}.order-5{order:5}.order-6{order:6}.order-7{order:7}.order-8{order:8}.order-9{order:9}.order-10{order:10}.order-11{order:11}.order-12{order:12}.offset-1{margin-left:8.33333%}.offset-2{margin-left:16.66667%}.offset-3{margin-left:25%}.offset-4{margin-left:33.33333%}.offset-5{margin-left:41.66667%}.offset-6{margin-left:50%}.offset-7{margin-left:58.33333%}.offset-8{margin-left:66.66667%}.offset-9{margin-left:75%}.offset-10{margin-left:83.33333%}.offset-11{margin-left:91.66667%}@media (min-width:576px){.col-sm{flex-basis:0;flex-grow:1;min-width:0;max-width:100%}.row-cols-sm-1>*{flex:0 0 100%;max-width:100%}.row-cols-sm-2>*{flex:0 0 50%;max-width:50%}.row-cols-sm-3>*{flex:0 0 33.33333%;max-width:33.33333%}.row-cols-sm-4>*{flex:0 0 25%;max-width:25%}.row-cols-sm-5>*{flex:0 0 20%;max-width:20%}.row-cols-sm-6>*{flex:0 0 16.66667%;max-width:16.66667%}.col-sm-auto{flex:0 0 auto;width:auto;max-width:100%}.col-sm-1{flex:0 0 8.33333%;max-width:8.33333%}.col-sm-2{flex:0 0 16.66667%;max-width:16.66667%}.col-sm-3{flex:0 0 25%;max-width:25%}.col-sm-4{flex:0 0 33.33333%;max-width:33.33333%}.col-sm-5{flex:0 0 41.66667%;max-width:41.66667%}.col-sm-6{flex:0 0 50%;max-width:50%}.col-sm-7{flex:0 0 58.33333%;max-width:58.33333%}.col-sm-8{flex:0 0 66.66667%;max-width:66.66667%}.col-sm-9{flex:0 0 75%;max-width:75%}.col-sm-10{flex:0 0 83.33333%;max-width:83.33333%}.col-sm-11{flex:0 0 91.66667%;max-width:91.66667%}.col-sm-12{flex:0 0 100%;max-width:100%}.order-sm-first{order:-1}.order-sm-last{order:13}.order-sm-0{order:0}.order-sm-1{order:1}.order-sm-2{order:2}.order-sm-3{order:3}.order-sm-4{order:4}.order-sm-5{order:5}.order-sm-6{order:6}.order-sm-7{order:7}.order-sm-8{order:8}.order-sm-9{order:9}.order-sm-10{order:10}.order-sm-11{order:11}.order-sm-12{order:12}.offset-sm-0{margin-left:0}.offset-sm-1{margin-left:8.33333%}.offset-sm-2{margin-left:16.66667%}.offset-sm-3{margin-left:25%}.offset-sm-4{margin-left:33.33333%}.offset-sm-5{margin-left:41.66667%}.offset-sm-6{margin-left:50%}.offset-sm-7{margin-left:58.33333%}.offset-sm-8{margin-left:66.66667%}.offset-sm-9{margin-left:75%}.offset-sm-10{margin-left:83.33333%}.offset-sm-11{margin-left:91.66667%}}@media (min-width:768px){.col-md{flex-basis:0;flex-grow:1;min-width:0;max-width:100%}.row-cols-md-1>*{flex:0 0 100%;max-width:100%}.row-cols-md-2>*{flex:0 0 50%;max-width:50%}.row-cols-md-3>*{flex:0 0 33.33333%;max-width:33.33333%}.row-cols-md-4>*{flex:0 0 25%;max-width:25%}.row-cols-md-5>*{flex:0 0 20%;max-width:20%}.row-cols-md-6>*{flex:0 0 16.66667%;max-width:16.66667%}.col-md-auto{flex:0 0 auto;width:auto;max-width:100%}.col-md-1{flex:0 0 8.33333%;max-width:8.33333%}.col-md-2{flex:0 0 16.66667%;max-width:16.66667%}.col-md-3{flex:0 0 25%;max-width:25%}.col-md-4{flex:0 0 33.33333%;max-width:33.33333%}.col-md-5{flex:0 0 41.66667%;max-width:41.66667%}.col-md-6{flex:0 0 50%;max-width:50%}.col-md-7{flex:0 0 58.33333%;max-width:58.33333%}.col-md-8{flex:0 0 66.66667%;max-width:66.66667%}.col-md-9{flex:0 0 75%;max-width:75%}.col-md-10{flex:0 0 83.33333%;max-width:83.33333%}.col-md-11{flex:0 0 91.66667%;max-width:91.66667%}.col-md-12{flex:0 0 100%;max-width:100%}.order-md-first{order:-1}.order-md-last{order:13}.order-md-0{order:0}.order-md-1{order:1}.order-md-2{order:2}.order-md-3{order:3}.order-md-4{order:4}.order-md-5{order:5}.order-md-6{order:6}.order-md-7{order:7}.order-md-8{order:8}.order-md-9{order:9}.order-md-10{order:10}.order-md-11{order:11}.order-md-12{order:12}.offset-md-0{margin-left:0}.offset-md-1{margin-left:8.33333%}.offset-md-2{margin-left:16.66667%}.offset-md-3{margin-left:25%}.offset-md-4{margin-left:33.33333%}.offset-md-5{margin-left:41.66667%}.offset-md-6{margin-left:50%}.offset-md-7{margin-left:58.33333%}.offset-md-8{margin-left:66.66667%}.offset-md-9{margin-left:75%}.offset-md-10{margin-left:83.33333%}.offset-md-11{margin-left:91.66667%}}@media (min-width:992px){.col-lg{flex-basis:0;flex-grow:1;min-width:0;max-width:100%}.row-cols-lg-1>*{flex:0 0 100%;max-width:100%}.row-cols-lg-2>*{flex:0 0 50%;max-width:50%}.row-cols-lg-3>*{flex:0 0 33.33333%;max-width:33.33333%}.row-cols-lg-4>*{flex:0 0 25%;max-width:25%}.row-cols-lg-5>*{flex:0 0 20%;max-width:20%}.row-cols-lg-6>*{flex:0 0 16.66667%;max-width:16.66667%}.col-lg-auto{flex:0 0 auto;width:auto;max-width:100%}.col-lg-1{flex:0 0 8.33333%;max-width:8.33333%}.col-lg-2{flex:0 0 16.66667%;max-width:16.66667%}.col-lg-3{flex:0 0 25%;max-width:25%}.col-lg-4{flex:0 0 33.33333%;max-width:33.33333%}.col-lg-5{flex:0 0 41.66667%;max-width:41.66667%}.col-lg-6{flex:0 0 50%;max-width:50%}.col-lg-7{flex:0 0 58.33333%;max-width:58.33333%}.col-lg-8{flex:0 0 66.66667%;max-width:66.66667%}.col-lg-9{flex:0 0 75%;max-width:75%}.col-lg-10{flex:0 0 83.33333%;max-width:83.33333%}.col-lg-11{flex:0 0 91.66667%;max-width:91.66667%}.col-lg-12{flex:0 0 100%;max-width:100%}.order-lg-first{order:-1}.order-lg-last{order:13}.order-lg-0{order:0}.order-lg-1{order:1}.order-lg-2{order:2}.order-lg-3{order:3}.order-lg-4{order:4}.order-lg-5{order:5}.order-lg-6{order:6}.order-lg-7{order:7}.order-lg-8{order:8}.order-lg-9{order:9}.order-lg-10{order:10}.order-lg-11{order:11}.order-lg-12{order:12}.offset-lg-0{margin-left:0}.offset-lg-1{margin-left:8.33333%}.offset-lg-2{margin-left:16.66667%}.offset-lg-3{margin-left:25%}.offset-lg-4{margin-left:33.33333%}.offset-lg-5{margin-left:41.66667%}.offset-lg-6{margin-left:50%}.offset-lg-7{margin-left:58.33333%}.offset-lg-8{margin-left:66.66667%}.offset-lg-9{margin-left:75%}.offset-lg-10{margin-left:83.33333%}.offset-lg-11{margin-left:91.66667%}}@media (min-width:1200px){.col-xl{flex-basis:0;flex-grow:1;min-width:0;max-width:100%}.row-cols-xl-1>*{flex:0 0 100%;max-width:100%}.row-cols-xl-2>*{flex:0 0 50%;max-width:50%}.row-cols-xl-3>*{flex:0 0 33.33333%;max-width:33.33333%}.row-cols-xl-4>*{flex:0 0 25%;max-width:25%}.row-cols-xl-5>*{flex:0 0 20%;max-width:20%}.row-cols-xl-6>*{flex:0 0 16.66667%;max-width:16.66667%}.col-xl-auto{flex:0 0 auto;width:auto;max-width:100%}.col-xl-1{flex:0 0 8.33333%;max-width:8.33333%}.col-xl-2{flex:0 0 16.66667%;max-width:16.66667%}.col-xl-3{flex:0 0 25%;max-width:25%}.col-xl-4{flex:0 0 33.33333%;max-width:33.33333%}.col-xl-5{flex:0 0 41.66667%;max-width:41.66667%}.col-xl-6{flex:0 0 50%;max-width:50%}.col-xl-7{flex:0 0 58.33333%;max-width:58.33333%}.col-xl-8{flex:0 0 66.66667%;max-width:66.66667%}.col-xl-9{flex:0 0 75%;max-width:75%}.col-xl-10{flex:0 0 83.33333%;max-width:83.33333%}.col-xl-11{flex:0 0 91.66667%;max-width:91.66667%}.col-xl-12{flex:0 0 100%;max-width:100%}.order-xl-first{order:-1}.order-xl-last{order:13}.order-xl-0{order:0}.order-xl-1{order:1}.order-xl-2{order:2}.order-xl-3{order:3}.order-xl-4{order:4}.order-xl-5{order:5}.order-xl-6{order:6}.order-xl-7{order:7}.order-xl-8{order:8}.order-xl-9{order:9}.order-xl-10{order:10}.order-xl-11{order:11}.order-xl-12{order:12}.offset-xl-0{margin-left:0}.offset-xl-1{margin-left:8.33333%}.offset-xl-2{margin-left:16.66667%}.offset-xl-3{margin-left:25%}.offset-xl-4{margin-left:33.33333%}.offset-xl-5{margin-left:41.66667%}.offset-xl-6{margin-left:50%}.offset-xl-7{margin-left:58.33333%}.offset-xl-8{margin-left:66.66667%}.offset-xl-9{margin-left:75%}.offset-xl-10{margin-left:83.33333%}.offset-xl-11{margin-left:91.66667%}}.table{width:100%;margin-bottom:1rem;color:#495057}.table td,.table th{padding:.75rem;vertical-align:top;border-top:1px solid #495057}.table thead th{vertical-align:bottom;border-bottom:2px solid #495057}.table tbody+tbody{border-top:2px solid #495057}.table-sm td,.table-sm th{padding:.3rem}.table-bordered{border:1px solid #495057}.table-bordered td,.table-bordered th{border:1px solid #495057}.table-bordered thead td,.table-bordered thead th{border-bottom-width:2px}.table-borderless tbody+tbody,.table-borderless td,.table-borderless th,.table-borderless thead th{border:0}.table-striped tbody tr:nth-of-type(odd){background-color:rgba(34,34,34,.05)}.table-hover tbody tr:hover{color:#495057;background-color:rgba(34,34,34,.075)}.table-primary,.table-primary>td,.table-primary>th{background-color:#fbd4c0}.table-primary tbody+tbody,.table-primary td,.table-primary th,.table-primary thead th{border-color:#f8ae8a}.table-hover .table-primary:hover{background-color:#f9c4a8}.table-hover .table-primary:hover>td,.table-hover .table-primary:hover>th{background-color:#f9c4a8}.table-secondary,.table-secondary>td,.table-secondary>th{background-color:#b8f0cf}.table-secondary tbody+tbody,.table-secondary td,.table-secondary th,.table-secondary thead th{border-color:#7ae2a6}.table-hover .table-secondary:hover{background-color:#a3ecc1}.table-hover .table-secondary:hover>td,.table-hover .table-secondary:hover>th{background-color:#a3ecc1}.table-success,.table-success>td,.table-success>th{background-color:#d4bcfb}.table-success tbody+tbody,.table-success td,.table-success th,.table-success thead th{border-color:#af83f8}.table-hover .table-success:hover{background-color:#c5a4fa}.table-hover .table-success:hover>td,.table-hover .table-success:hover>th{background-color:#c5a4fa}.table-info,.table-info>td,.table-info>th{background-color:#b8daff}.table-info tbody+tbody,.table-info td,.table-info th,.table-info thead th{border-color:#7abaff}.table-hover .table-info:hover{background-color:#9fcdff}.table-hover .table-info:hover>td,.table-hover .table-info:hover>th{background-color:#9fcdff}.table-warning,.table-warning>td,.table-warning>th{background-color:#ffeeba}.table-warning tbody+tbody,.table-warning td,.table-warning th,.table-warning thead th{border-color:#ffdf7e}.table-hover .table-warning:hover{background-color:#ffe8a1}.table-hover .table-warning:hover>td,.table-hover .table-warning:hover>th{background-color:#ffe8a1}.table-danger,.table-danger>td,.table-danger>th{background-color:#ddc6ba}.table-danger tbody+tbody,.table-danger td,.table-danger th,.table-danger thead th{border-color:#c1957f}.table-hover .table-danger:hover{background-color:#d5b8a9}.table-hover .table-danger:hover>td,.table-hover .table-danger:hover>th{background-color:#d5b8a9}.table-light,.table-light>td,.table-light>th{background-color:#fdfdfe}.table-light tbody+tbody,.table-light td,.table-light th,.table-light thead th{border-color:#fbfcfc}.table-hover .table-light:hover{background-color:#ececf6}.table-hover .table-light:hover>td,.table-hover .table-light:hover>th{background-color:#ececf6}.table-dark,.table-dark>td,.table-dark>th{background-color:#c6c8ca}.table-dark tbody+tbody,.table-dark td,.table-dark th,.table-dark thead th{border-color:#95999c}.table-hover .table-dark:hover{background-color:#b9bbbe}.table-hover .table-dark:hover>td,.table-hover .table-dark:hover>th{background-color:#b9bbbe}.table-active,.table-active>td,.table-active>th{background-color:rgba(34,34,34,.075)}.table-hover .table-active:hover{background-color:rgba(21,21,21,.075)}.table-hover .table-active:hover>td,.table-hover .table-active:hover>th{background-color:rgba(21,21,21,.075)}.table .thead-dark th{color:#fff;background-color:#343a40;border-color:#454d55}.table .thead-light th{color:#495057;background-color:#e9ecef;border-color:#495057}.table-dark{color:#fff;background-color:#343a40}.table-dark td,.table-dark th,.table-dark thead th{border-color:#454d55}.table-dark.table-bordered{border:0}.table-dark.table-striped tbody tr:nth-of-type(odd){background-color:rgba(255,255,255,.05)}.table-dark.table-hover tbody tr:hover{color:#fff;background-color:rgba(255,255,255,.075)}@media (max-width:575.98px){.table-responsive-sm{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch}.table-responsive-sm>.table-bordered{border:0}}@media (max-width:767.98px){.table-responsive-md{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch}.table-responsive-md>.table-bordered{border:0}}@media (max-width:991.98px){.table-responsive-lg{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch}.table-responsive-lg>.table-bordered{border:0}}@media (max-width:1199.98px){.table-responsive-xl{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch}.table-responsive-xl>.table-bordered{border:0}}.table-responsive{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch}.table-responsive>.table-bordered{border:0}.form-control{display:block;width:100%;height:calc(1.5em + .75rem + 2px);padding:.375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5;color:#495057;background-color:#fff;background-clip:padding-box;border:1px solid #ced4da;border-radius:.5rem;transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-control{transition:none}}.form-control::-ms-expand{background-color:transparent;border:0}.form-control:-moz-focusring{color:transparent;text-shadow:0 0 0 #495057}.form-control:focus{color:#495057;background-color:#fff;border-color:#f8b796;outline:0;box-shadow:0 0 0 .2rem rgba(241,100,30,.75)}.form-control::placeholder{color:#6c757d;opacity:1}.form-control:disabled,.form-control[readonly]{background-color:#e9ecef;opacity:1}input[type=date].form-control,input[type=datetime-local].form-control,input[type=month].form-control,input[type=time].form-control{appearance:none}select.form-control:focus::-ms-value{color:#495057;background-color:#fff}.form-control-file,.form-control-range{display:block;width:100%}.col-form-label{padding-top:calc(.375rem + 1px);padding-bottom:calc(.375rem + 1px);margin-bottom:0;font-size:inherit;line-height:1.5}.col-form-label-lg{padding-top:calc(.5rem + 1px);padding-bottom:calc(.5rem + 1px);font-size:1.25rem;line-height:1.5}.col-form-label-sm{padding-top:calc(.25rem + 1px);padding-bottom:calc(.25rem + 1px);font-size:.875rem;line-height:1.5}.form-control-plaintext{display:block;width:100%;padding:.375rem 0;margin-bottom:0;font-size:1rem;line-height:1.5;color:#495057;background-color:transparent;border:solid transparent;border-width:1px 0}.form-control-plaintext.form-control-lg,.form-control-plaintext.form-control-sm{padding-right:0;padding-left:0}.form-control-sm{height:calc(1.5em + .5rem + 2px);padding:.25rem .5rem;font-size:.875rem;line-height:1.5;border-radius:1rem}.form-control-lg{height:calc(1.5em + 1rem + 2px);padding:.5rem 1rem;font-size:1.25rem;line-height:1.5;border-radius:.5rem}select.form-control[multiple],select.form-control[size]{height:auto}textarea.form-control{height:auto}.form-group{margin-bottom:1rem}.form-text{display:block;margin-top:.25rem}.form-row{display:flex;flex-wrap:wrap;margin-right:-5px;margin-left:-5px}.form-row>.col,.form-row>[class*=col-]{padding-right:5px;padding-left:5px}.form-check{position:relative;display:block;padding-left:1.25rem}.form-check-input{position:absolute;margin-top:.3rem;margin-left:-1.25rem}.form-check-input:disabled~.form-check-label,.form-check-input[disabled]~.form-check-label{color:#6c757d}.form-check-label{margin-bottom:0}.form-check-inline{display:inline-flex;align-items:center;padding-left:0;margin-right:.75rem}.form-check-inline .form-check-input{position:static;margin-top:0;margin-right:.3125rem;margin-left:0}.valid-feedback{display:none;width:100%;margin-top:.25rem;font-size:80%;color:#007bff}.valid-tooltip{position:absolute;top:100%;z-index:5;display:none;max-width:100%;padding:.25rem .5rem;margin-top:.1rem;font-size:.875rem;line-height:1.5;color:#fff;background-color:rgba(0,123,255,.9);border-radius:.5rem}.is-valid~.valid-feedback,.is-valid~.valid-tooltip,.was-validated :valid~.valid-feedback,.was-validated :valid~.valid-tooltip{display:block}.form-control.is-valid,.was-validated .form-control:valid{border-color:#007bff;padding-right:calc(1.5em + .75rem);background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='8' height='8' viewBox='0 0 8 8'%3e%3cpath fill='%23007bff' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right calc(.375em + .1875rem) center;background-size:calc(.75em + .375rem) calc(.75em + .375rem)}.form-control.is-valid:focus,.was-validated .form-control:valid:focus{border-color:#007bff;box-shadow:0 0 0 .2rem rgba(0,123,255,.25)}.was-validated textarea.form-control:valid,textarea.form-control.is-valid{padding-right:calc(1.5em + .75rem);background-position:top calc(.375em + .1875rem) right calc(.375em + .1875rem)}.custom-select.is-valid,.was-validated .custom-select:valid{border-color:#007bff;padding-right:calc(.75em + 2.3125rem);background:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='4' height='5' viewBox='0 0 4 5'%3e%3cpath fill='%23343a40' d='M2 0L0 2h4zm0 5L0 3h4z'/%3e%3c/svg%3e") no-repeat right .75rem center/8px 10px,url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='8' height='8' viewBox='0 0 8 8'%3e%3cpath fill='%23007bff' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e") #fff no-repeat center right 1.75rem/calc(.75em + .375rem) calc(.75em + .375rem)}.custom-select.is-valid:focus,.was-validated .custom-select:valid:focus{border-color:#007bff;box-shadow:0 0 0 .2rem rgba(0,123,255,.25)}.form-check-input.is-valid~.form-check-label,.was-validated .form-check-input:valid~.form-check-label{color:#007bff}.form-check-input.is-valid~.valid-feedback,.form-check-input.is-valid~.valid-tooltip,.was-validated .form-check-input:valid~.valid-feedback,.was-validated .form-check-input:valid~.valid-tooltip{display:block}.custom-control-input.is-valid~.custom-control-label,.was-validated .custom-control-input:valid~.custom-control-label{color:#007bff}.custom-control-input.is-valid~.custom-control-label::before,.was-validated .custom-control-input:valid~.custom-control-label::before{border-color:#007bff}.custom-control-input.is-valid:checked~.custom-control-label::before,.was-validated .custom-control-input:valid:checked~.custom-control-label::before{border-color:#3395ff;background-color:#3395ff}.custom-control-input.is-valid:focus~.custom-control-label::before,.was-validated .custom-control-input:valid:focus~.custom-control-label::before{box-shadow:0 0 0 .2rem rgba(0,123,255,.25)}.custom-control-input.is-valid:focus:not(:checked)~.custom-control-label::before,.was-validated .custom-control-input:valid:focus:not(:checked)~.custom-control-label::before{border-color:#007bff}.custom-file-input.is-valid~.custom-file-label,.was-validated .custom-file-input:valid~.custom-file-label{border-color:#007bff}.custom-file-input.is-valid:focus~.custom-file-label,.was-validated .custom-file-input:valid:focus~.custom-file-label{border-color:#007bff;box-shadow:0 0 0 .2rem rgba(0,123,255,.25)}.invalid-feedback{display:none;width:100%;margin-top:.25rem;font-size:80%;color:#873208}.invalid-tooltip{position:absolute;top:100%;z-index:5;display:none;max-width:100%;padding:.25rem .5rem;margin-top:.1rem;font-size:.875rem;line-height:1.5;color:#fff;background-color:rgba(135,50,8,.9);border-radius:.5rem}.is-invalid~.invalid-feedback,.is-invalid~.invalid-tooltip,.was-validated :invalid~.invalid-feedback,.was-validated :invalid~.invalid-tooltip{display:block}.form-control.is-invalid,.was-validated .form-control:invalid{border-color:#873208;padding-right:calc(1.5em + .75rem);background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='none' stroke='%23873208' viewBox='0 0 12 12'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23873208' stroke='none'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right calc(.375em + .1875rem) center;background-size:calc(.75em + .375rem) calc(.75em + .375rem)}.form-control.is-invalid:focus,.was-validated .form-control:invalid:focus{border-color:#873208;box-shadow:0 0 0 .2rem rgba(135,50,8,.25)}.was-validated textarea.form-control:invalid,textarea.form-control.is-invalid{padding-right:calc(1.5em + .75rem);background-position:top calc(.375em + .1875rem) right calc(.375em + .1875rem)}.custom-select.is-invalid,.was-validated .custom-select:invalid{border-color:#873208;padding-right:calc(.75em + 2.3125rem);background:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='4' height='5' viewBox='0 0 4 5'%3e%3cpath fill='%23343a40' d='M2 0L0 2h4zm0 5L0 3h4z'/%3e%3c/svg%3e") no-repeat right .75rem center/8px 10px,url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='none' stroke='%23873208' viewBox='0 0 12 12'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23873208' stroke='none'/%3e%3c/svg%3e") #fff no-repeat center right 1.75rem/calc(.75em + .375rem) calc(.75em + .375rem)}.custom-select.is-invalid:focus,.was-validated .custom-select:invalid:focus{border-color:#873208;box-shadow:0 0 0 .2rem rgba(135,50,8,.25)}.form-check-input.is-invalid~.form-check-label,.was-validated .form-check-input:invalid~.form-check-label{color:#873208}.form-check-input.is-invalid~.invalid-feedback,.form-check-input.is-invalid~.invalid-tooltip,.was-validated .form-check-input:invalid~.invalid-feedback,.was-validated .form-check-input:invalid~.invalid-tooltip{display:block}.custom-control-input.is-invalid~.custom-control-label,.was-validated .custom-control-input:invalid~.custom-control-label{color:#873208}.custom-control-input.is-invalid~.custom-control-label::before,.was-validated .custom-control-input:invalid~.custom-control-label::before{border-color:#873208}.custom-control-input.is-invalid:checked~.custom-control-label::before,.was-validated .custom-control-input:invalid:checked~.custom-control-label::before{border-color:#b7440b;background-color:#b7440b}.custom-control-input.is-invalid:focus~.custom-control-label::before,.was-validated .custom-control-input:invalid:focus~.custom-control-label::before{box-shadow:0 0 0 .2rem rgba(135,50,8,.25)}.custom-control-input.is-invalid:focus:not(:checked)~.custom-control-label::before,.was-validated .custom-control-input:invalid:focus:not(:checked)~.custom-control-label::before{border-color:#873208}.custom-file-input.is-invalid~.custom-file-label,.was-validated .custom-file-input:invalid~.custom-file-label{border-color:#873208}.custom-file-input.is-invalid:focus~.custom-file-label,.was-validated .custom-file-input:invalid:focus~.custom-file-label{border-color:#873208;box-shadow:0 0 0 .2rem rgba(135,50,8,.25)}.form-inline{display:flex;flex-flow:row wrap;align-items:center}.form-inline .form-check{width:100%}@media (min-width:576px){.form-inline label{display:flex;align-items:center;justify-content:center;margin-bottom:0}.form-inline .form-group{display:flex;flex:0 0 auto;flex-flow:row wrap;align-items:center;margin-bottom:0}.form-inline .form-control{display:inline-block;width:auto;vertical-align:middle}.form-inline .form-control-plaintext{display:inline-block}.form-inline .custom-select,.form-inline .input-group{width:auto}.form-inline .form-check{display:flex;align-items:center;justify-content:center;width:auto;padding-left:0}.form-inline .form-check-input{position:relative;flex-shrink:0;margin-top:0;margin-right:.25rem;margin-left:0}.form-inline .custom-control{align-items:center;justify-content:center}.form-inline .custom-control-label{margin-bottom:0}}.btn{display:inline-block;font-weight:400;color:#495057;text-align:center;vertical-align:middle;user-select:none;background-color:transparent;border:1px solid transparent;padding:.375rem .75rem;font-size:1rem;line-height:1.5;border-radius:.5rem;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.btn{transition:none}}.btn:hover{color:#495057;text-decoration:none}.btn.focus,.btn:focus{outline:0;box-shadow:0 0 0 .2rem rgba(241,100,30,.75)}.btn.disabled,.btn:disabled{opacity:.65}.btn:not(:disabled):not(.disabled){cursor:pointer}a.btn.disabled,fieldset:disabled a.btn{pointer-events:none}.btn-primary{color:#fff;background-color:#f1641e;border-color:#f1641e}.btn-primary:hover{color:#fff;background-color:#db520e;border-color:#cf4d0d}.btn-primary.focus,.btn-primary:focus{color:#fff;background-color:#db520e;border-color:#cf4d0d;box-shadow:0 0 0 .2rem rgba(243,123,64,.5)}.btn-primary.disabled,.btn-primary:disabled{color:#fff;background-color:#f1641e;border-color:#f1641e}.btn-primary:not(:disabled):not(.disabled).active,.btn-primary:not(:disabled):not(.disabled):active,.show>.btn-primary.dropdown-toggle{color:#fff;background-color:#cf4d0d;border-color:#c3490c}.btn-primary:not(:disabled):not(.disabled).active:focus,.btn-primary:not(:disabled):not(.disabled):active:focus,.show>.btn-primary.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(243,123,64,.5)}.btn-secondary{color:#fff;background-color:#00c853;border-color:#00c853}.btn-secondary:hover{color:#fff;background-color:#00a243;border-color:#00953e}.btn-secondary.focus,.btn-secondary:focus{color:#fff;background-color:#00a243;border-color:#00953e;box-shadow:0 0 0 .2rem rgba(38,208,109,.5)}.btn-secondary.disabled,.btn-secondary:disabled{color:#fff;background-color:#00c853;border-color:#00c853}.btn-secondary:not(:disabled):not(.disabled).active,.btn-secondary:not(:disabled):not(.disabled):active,.show>.btn-secondary.dropdown-toggle{color:#fff;background-color:#00953e;border-color:#008839}.btn-secondary:not(:disabled):not(.disabled).active:focus,.btn-secondary:not(:disabled):not(.disabled):active:focus,.show>.btn-secondary.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(38,208,109,.5)}.btn-success{color:#fff;background-color:#6610f2;border-color:#6610f2}.btn-success:hover{color:#fff;background-color:#560bd0;border-color:#510bc4}.btn-success.focus,.btn-success:focus{color:#fff;background-color:#560bd0;border-color:#510bc4;box-shadow:0 0 0 .2rem rgba(125,52,244,.5)}.btn-success.disabled,.btn-success:disabled{color:#fff;background-color:#6610f2;border-color:#6610f2}.btn-success:not(:disabled):not(.disabled).active,.btn-success:not(:disabled):not(.disabled):active,.show>.btn-success.dropdown-toggle{color:#fff;background-color:#510bc4;border-color:#4c0ab8}.btn-success:not(:disabled):not(.disabled).active:focus,.btn-success:not(:disabled):not(.disabled):active:focus,.show>.btn-success.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(125,52,244,.5)}.btn-info{color:#fff;background-color:#007bff;border-color:#007bff}.btn-info:hover{color:#fff;background-color:#0069d9;border-color:#0062cc}.btn-info.focus,.btn-info:focus{color:#fff;background-color:#0069d9;border-color:#0062cc;box-shadow:0 0 0 .2rem rgba(38,143,255,.5)}.btn-info.disabled,.btn-info:disabled{color:#fff;background-color:#007bff;border-color:#007bff}.btn-info:not(:disabled):not(.disabled).active,.btn-info:not(:disabled):not(.disabled):active,.show>.btn-info.dropdown-toggle{color:#fff;background-color:#0062cc;border-color:#005cbf}.btn-info:not(:disabled):not(.disabled).active:focus,.btn-info:not(:disabled):not(.disabled):active:focus,.show>.btn-info.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(38,143,255,.5)}.btn-warning{color:#212529;background-color:#ffc107;border-color:#ffc107}.btn-warning:hover{color:#212529;background-color:#e0a800;border-color:#d39e00}.btn-warning.focus,.btn-warning:focus{color:#212529;background-color:#e0a800;border-color:#d39e00;box-shadow:0 0 0 .2rem rgba(222,170,12,.5)}.btn-warning.disabled,.btn-warning:disabled{color:#212529;background-color:#ffc107;border-color:#ffc107}.btn-warning:not(:disabled):not(.disabled).active,.btn-warning:not(:disabled):not(.disabled):active,.show>.btn-warning.dropdown-toggle{color:#212529;background-color:#d39e00;border-color:#c69500}.btn-warning:not(:disabled):not(.disabled).active:focus,.btn-warning:not(:disabled):not(.disabled):active:focus,.show>.btn-warning.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(222,170,12,.5)}.btn-danger{color:#fff;background-color:#873208;border-color:#873208}.btn-danger:hover{color:#fff;background-color:#632506;border-color:#572105}.btn-danger.focus,.btn-danger:focus{color:#fff;background-color:#632506;border-color:#572105;box-shadow:0 0 0 .2rem rgba(153,81,45,.5)}.btn-danger.disabled,.btn-danger:disabled{color:#fff;background-color:#873208;border-color:#873208}.btn-danger:not(:disabled):not(.disabled).active,.btn-danger:not(:disabled):not(.disabled):active,.show>.btn-danger.dropdown-toggle{color:#fff;background-color:#572105;border-color:#4b1c05}.btn-danger:not(:disabled):not(.disabled).active:focus,.btn-danger:not(:disabled):not(.disabled):active:focus,.show>.btn-danger.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(153,81,45,.5)}.btn-light{color:#212529;background-color:#f8f9fa;border-color:#f8f9fa}.btn-light:hover{color:#212529;background-color:#e2e6ea;border-color:#dae0e5}.btn-light.focus,.btn-light:focus{color:#212529;background-color:#e2e6ea;border-color:#dae0e5;box-shadow:0 0 0 .2rem rgba(216,217,219,.5)}.btn-light.disabled,.btn-light:disabled{color:#212529;background-color:#f8f9fa;border-color:#f8f9fa}.btn-light:not(:disabled):not(.disabled).active,.btn-light:not(:disabled):not(.disabled):active,.show>.btn-light.dropdown-toggle{color:#212529;background-color:#dae0e5;border-color:#d3d9df}.btn-light:not(:disabled):not(.disabled).active:focus,.btn-light:not(:disabled):not(.disabled):active:focus,.show>.btn-light.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(216,217,219,.5)}.btn-dark{color:#fff;background-color:#343a40;border-color:#343a40}.btn-dark:hover{color:#fff;background-color:#23272b;border-color:#1d2124}.btn-dark.focus,.btn-dark:focus{color:#fff;background-color:#23272b;border-color:#1d2124;box-shadow:0 0 0 .2rem rgba(82,88,93,.5)}.btn-dark.disabled,.btn-dark:disabled{color:#fff;background-color:#343a40;border-color:#343a40}.btn-dark:not(:disabled):not(.disabled).active,.btn-dark:not(:disabled):not(.disabled):active,.show>.btn-dark.dropdown-toggle{color:#fff;background-color:#1d2124;border-color:#171a1d}.btn-dark:not(:disabled):not(.disabled).active:focus,.btn-dark:not(:disabled):not(.disabled):active:focus,.show>.btn-dark.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(82,88,93,.5)}.btn-outline-primary{color:#f1641e;border-color:#f1641e}.btn-outline-primary:hover{color:#fff;background-color:#f1641e;border-color:#f1641e}.btn-outline-primary.focus,.btn-outline-primary:focus{box-shadow:0 0 0 .2rem rgba(241,100,30,.5)}.btn-outline-primary.disabled,.btn-outline-primary:disabled{color:#f1641e;background-color:transparent}.btn-outline-primary:not(:disabled):not(.disabled).active,.btn-outline-primary:not(:disabled):not(.disabled):active,.show>.btn-outline-primary.dropdown-toggle{color:#fff;background-color:#f1641e;border-color:#f1641e}.btn-outline-primary:not(:disabled):not(.disabled).active:focus,.btn-outline-primary:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-primary.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(241,100,30,.5)}.btn-outline-secondary{color:#00c853;border-color:#00c853}.btn-outline-secondary:hover{color:#fff;background-color:#00c853;border-color:#00c853}.btn-outline-secondary.focus,.btn-outline-secondary:focus{box-shadow:0 0 0 .2rem rgba(0,200,83,.5)}.btn-outline-secondary.disabled,.btn-outline-secondary:disabled{color:#00c853;background-color:transparent}.btn-outline-secondary:not(:disabled):not(.disabled).active,.btn-outline-secondary:not(:disabled):not(.disabled):active,.show>.btn-outline-secondary.dropdown-toggle{color:#fff;background-color:#00c853;border-color:#00c853}.btn-outline-secondary:not(:disabled):not(.disabled).active:focus,.btn-outline-secondary:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-secondary.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(0,200,83,.5)}.btn-outline-success{color:#6610f2;border-color:#6610f2}.btn-outline-success:hover{color:#fff;background-color:#6610f2;border-color:#6610f2}.btn-outline-success.focus,.btn-outline-success:focus{box-shadow:0 0 0 .2rem rgba(102,16,242,.5)}.btn-outline-success.disabled,.btn-outline-success:disabled{color:#6610f2;background-color:transparent}.btn-outline-success:not(:disabled):not(.disabled).active,.btn-outline-success:not(:disabled):not(.disabled):active,.show>.btn-outline-success.dropdown-toggle{color:#fff;background-color:#6610f2;border-color:#6610f2}.btn-outline-success:not(:disabled):not(.disabled).active:focus,.btn-outline-success:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-success.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(102,16,242,.5)}.btn-outline-info{color:#007bff;border-color:#007bff}.btn-outline-info:hover{color:#fff;background-color:#007bff;border-color:#007bff}.btn-outline-info.focus,.btn-outline-info:focus{box-shadow:0 0 0 .2rem rgba(0,123,255,.5)}.btn-outline-info.disabled,.btn-outline-info:disabled{color:#007bff;background-color:transparent}.btn-outline-info:not(:disabled):not(.disabled).active,.btn-outline-info:not(:disabled):not(.disabled):active,.show>.btn-outline-info.dropdown-toggle{color:#fff;background-color:#007bff;border-color:#007bff}.btn-outline-info:not(:disabled):not(.disabled).active:focus,.btn-outline-info:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-info.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(0,123,255,.5)}.btn-outline-warning{color:#ffc107;border-color:#ffc107}.btn-outline-warning:hover{color:#212529;background-color:#ffc107;border-color:#ffc107}.btn-outline-warning.focus,.btn-outline-warning:focus{box-shadow:0 0 0 .2rem rgba(255,193,7,.5)}.btn-outline-warning.disabled,.btn-outline-warning:disabled{color:#ffc107;background-color:transparent}.btn-outline-warning:not(:disabled):not(.disabled).active,.btn-outline-warning:not(:disabled):not(.disabled):active,.show>.btn-outline-warning.dropdown-toggle{color:#212529;background-color:#ffc107;border-color:#ffc107}.btn-outline-warning:not(:disabled):not(.disabled).active:focus,.btn-outline-warning:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-warning.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(255,193,7,.5)}.btn-outline-danger{color:#873208;border-color:#873208}.btn-outline-danger:hover{color:#fff;background-color:#873208;border-color:#873208}.btn-outline-danger.focus,.btn-outline-danger:focus{box-shadow:0 0 0 .2rem rgba(135,50,8,.5)}.btn-outline-danger.disabled,.btn-outline-danger:disabled{color:#873208;background-color:transparent}.btn-outline-danger:not(:disabled):not(.disabled).active,.btn-outline-danger:not(:disabled):not(.disabled):active,.show>.btn-outline-danger.dropdown-toggle{color:#fff;background-color:#873208;border-color:#873208}.btn-outline-danger:not(:disabled):not(.disabled).active:focus,.btn-outline-danger:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-danger.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(135,50,8,.5)}.btn-outline-light{color:#f8f9fa;border-color:#f8f9fa}.btn-outline-light:hover{color:#212529;background-color:#f8f9fa;border-color:#f8f9fa}.btn-outline-light.focus,.btn-outline-light:focus{box-shadow:0 0 0 .2rem rgba(248,249,250,.5)}.btn-outline-light.disabled,.btn-outline-light:disabled{color:#f8f9fa;background-color:transparent}.btn-outline-light:not(:disabled):not(.disabled).active,.btn-outline-light:not(:disabled):not(.disabled):active,.show>.btn-outline-light.dropdown-toggle{color:#212529;background-color:#f8f9fa;border-color:#f8f9fa}.btn-outline-light:not(:disabled):not(.disabled).active:focus,.btn-outline-light:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-light.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(248,249,250,.5)}.btn-outline-dark{color:#343a40;border-color:#343a40}.btn-outline-dark:hover{color:#fff;background-color:#343a40;border-color:#343a40}.btn-outline-dark.focus,.btn-outline-dark:focus{box-shadow:0 0 0 .2rem rgba(52,58,64,.5)}.btn-outline-dark.disabled,.btn-outline-dark:disabled{color:#343a40;background-color:transparent}.btn-outline-dark:not(:disabled):not(.disabled).active,.btn-outline-dark:not(:disabled):not(.disabled):active,.show>.btn-outline-dark.dropdown-toggle{color:#fff;background-color:#343a40;border-color:#343a40}.btn-outline-dark:not(:disabled):not(.disabled).active:focus,.btn-outline-dark:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-dark.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(52,58,64,.5)}.btn-link{font-weight:400;color:#f1641e;text-decoration:none}.btn-link:hover{color:#b7440b;text-decoration:underline}.btn-link.focus,.btn-link:focus{text-decoration:underline}.btn-link.disabled,.btn-link:disabled{color:#6c757d;pointer-events:none}.btn-group-lg>.btn,.btn-lg{padding:.5rem 1rem;font-size:1.25rem;line-height:1.5;border-radius:.5rem}.btn-group-sm>.btn,.btn-sm{padding:.25rem .5rem;font-size:.875rem;line-height:1.5;border-radius:1rem}.btn-block{display:block;width:100%}.btn-block+.btn-block{margin-top:.5rem}input[type=button].btn-block,input[type=reset].btn-block,input[type=submit].btn-block{width:100%}.fade{transition:opacity .15s linear}@media (prefers-reduced-motion:reduce){.fade{transition:none}}.fade:not(.show){opacity:0}.collapse:not(.show){display:none}.collapsing{position:relative;height:0;overflow:hidden;transition:height .35s ease}@media (prefers-reduced-motion:reduce){.collapsing{transition:none}}.dropdown,.dropleft,.dropright,.dropup{position:relative}.dropdown-toggle{white-space:nowrap}.dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:.3em solid;border-right:.3em solid transparent;border-bottom:0;border-left:.3em solid transparent}.dropdown-toggle:empty::after{margin-left:0}.dropdown-menu{position:absolute;top:100%;left:0;z-index:1000;display:none;float:left;min-width:10rem;padding:.5rem 0;margin:.125rem 0 0;font-size:1rem;color:#495057;text-align:left;list-style:none;background-color:#fff;background-clip:padding-box;border:1px solid rgba(34,34,34,.15);border-radius:.5rem}.dropdown-menu-left{right:auto;left:0}.dropdown-menu-right{right:0;left:auto}@media (min-width:576px){.dropdown-menu-sm-left{right:auto;left:0}.dropdown-menu-sm-right{right:0;left:auto}}@media (min-width:768px){.dropdown-menu-md-left{right:auto;left:0}.dropdown-menu-md-right{right:0;left:auto}}@media (min-width:992px){.dropdown-menu-lg-left{right:auto;left:0}.dropdown-menu-lg-right{right:0;left:auto}}@media (min-width:1200px){.dropdown-menu-xl-left{right:auto;left:0}.dropdown-menu-xl-right{right:0;left:auto}}.dropup .dropdown-menu{top:auto;bottom:100%;margin-top:0;margin-bottom:.125rem}.dropup .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:0;border-right:.3em solid transparent;border-bottom:.3em solid;border-left:.3em solid transparent}.dropup .dropdown-toggle:empty::after{margin-left:0}.dropright .dropdown-menu{top:0;right:auto;left:100%;margin-top:0;margin-left:.125rem}.dropright .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:.3em solid transparent;border-right:0;border-bottom:.3em solid transparent;border-left:.3em solid}.dropright .dropdown-toggle:empty::after{margin-left:0}.dropright .dropdown-toggle::after{vertical-align:0}.dropleft .dropdown-menu{top:0;right:100%;left:auto;margin-top:0;margin-right:.125rem}.dropleft .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:""}.dropleft .dropdown-toggle::after{display:none}.dropleft .dropdown-toggle::before{display:inline-block;margin-right:.255em;vertical-align:.255em;content:"";border-top:.3em solid transparent;border-right:.3em solid;border-bottom:.3em solid transparent}.dropleft .dropdown-toggle:empty::after{margin-left:0}.dropleft .dropdown-toggle::before{vertical-align:0}.dropdown-menu[x-placement^=bottom],.dropdown-menu[x-placement^=left],.dropdown-menu[x-placement^=right],.dropdown-menu[x-placement^=top]{right:auto;bottom:auto}.dropdown-divider{height:0;margin:.5rem 0;overflow:hidden;border-top:1px solid #e9ecef}.dropdown-item{display:block;width:100%;padding:.25rem 1.5rem;clear:both;font-weight:400;color:#212529;text-align:inherit;white-space:nowrap;background-color:transparent;border:0}.dropdown-item:focus,.dropdown-item:hover{color:#16181b;text-decoration:none;background-color:#f8f9fa}.dropdown-item.active,.dropdown-item:active{color:#fff;text-decoration:none;background-color:#f1641e}.dropdown-item.disabled,.dropdown-item:disabled{color:#6c757d;pointer-events:none;background-color:transparent}.dropdown-menu.show{display:block}.dropdown-header{display:block;padding:.5rem 1.5rem;margin-bottom:0;font-size:.875rem;color:#6c757d;white-space:nowrap}.dropdown-item-text{display:block;padding:.25rem 1.5rem;color:#212529}.btn-group,.btn-group-vertical{position:relative;display:inline-flex;vertical-align:middle}.btn-group-vertical>.btn,.btn-group>.btn{position:relative;flex:1 1 auto}.btn-group-vertical>.btn:hover,.btn-group>.btn:hover{z-index:1}.btn-group-vertical>.btn.active,.btn-group-vertical>.btn:active,.btn-group-vertical>.btn:focus,.btn-group>.btn.active,.btn-group>.btn:active,.btn-group>.btn:focus{z-index:1}.btn-toolbar{display:flex;flex-wrap:wrap;justify-content:flex-start}.btn-toolbar .input-group{width:auto}.btn-group>.btn-group:not(:first-child),.btn-group>.btn:not(:first-child){margin-left:-1px}.btn-group>.btn-group:not(:last-child)>.btn,.btn-group>.btn:not(:last-child):not(.dropdown-toggle){border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn-group:not(:first-child)>.btn,.btn-group>.btn:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.dropdown-toggle-split{padding-right:.5625rem;padding-left:.5625rem}.dropdown-toggle-split::after,.dropright .dropdown-toggle-split::after,.dropup .dropdown-toggle-split::after{margin-left:0}.dropleft .dropdown-toggle-split::before{margin-right:0}.btn-group-sm>.btn+.dropdown-toggle-split,.btn-sm+.dropdown-toggle-split{padding-right:.375rem;padding-left:.375rem}.btn-group-lg>.btn+.dropdown-toggle-split,.btn-lg+.dropdown-toggle-split{padding-right:.75rem;padding-left:.75rem}.btn-group-vertical{flex-direction:column;align-items:flex-start;justify-content:center}.btn-group-vertical>.btn,.btn-group-vertical>.btn-group{width:100%}.btn-group-vertical>.btn-group:not(:first-child),.btn-group-vertical>.btn:not(:first-child){margin-top:-1px}.btn-group-vertical>.btn-group:not(:last-child)>.btn,.btn-group-vertical>.btn:not(:last-child):not(.dropdown-toggle){border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn-group:not(:first-child)>.btn,.btn-group-vertical>.btn:not(:first-child){border-top-left-radius:0;border-top-right-radius:0}.btn-group-toggle>.btn,.btn-group-toggle>.btn-group>.btn{margin-bottom:0}.btn-group-toggle>.btn input[type=checkbox],.btn-group-toggle>.btn input[type=radio],.btn-group-toggle>.btn-group>.btn input[type=checkbox],.btn-group-toggle>.btn-group>.btn input[type=radio]{position:absolute;clip:rect(0,0,0,0);pointer-events:none}.input-group{position:relative;display:flex;flex-wrap:wrap;align-items:stretch;width:100%}.input-group>.custom-file,.input-group>.custom-select,.input-group>.form-control,.input-group>.form-control-plaintext{position:relative;flex:1 1 auto;width:1%;min-width:0;margin-bottom:0}.input-group>.custom-file+.custom-file,.input-group>.custom-file+.custom-select,.input-group>.custom-file+.form-control,.input-group>.custom-select+.custom-file,.input-group>.custom-select+.custom-select,.input-group>.custom-select+.form-control,.input-group>.form-control+.custom-file,.input-group>.form-control+.custom-select,.input-group>.form-control+.form-control,.input-group>.form-control-plaintext+.custom-file,.input-group>.form-control-plaintext+.custom-select,.input-group>.form-control-plaintext+.form-control{margin-left:-1px}.input-group>.custom-file .custom-file-input:focus~.custom-file-label,.input-group>.custom-select:focus,.input-group>.form-control:focus{z-index:3}.input-group>.custom-file .custom-file-input:focus{z-index:4}.input-group>.custom-select:not(:last-child),.input-group>.form-control:not(:last-child){border-top-right-radius:0;border-bottom-right-radius:0}.input-group>.custom-select:not(:first-child),.input-group>.form-control:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.input-group>.custom-file{display:flex;align-items:center}.input-group>.custom-file:not(:last-child) .custom-file-label,.input-group>.custom-file:not(:last-child) .custom-file-label::after{border-top-right-radius:0;border-bottom-right-radius:0}.input-group>.custom-file:not(:first-child) .custom-file-label{border-top-left-radius:0;border-bottom-left-radius:0}.input-group-append,.input-group-prepend{display:flex}.input-group-append .btn,.input-group-prepend .btn{position:relative;z-index:2}.input-group-append .btn:focus,.input-group-prepend .btn:focus{z-index:3}.input-group-append .btn+.btn,.input-group-append .btn+.input-group-text,.input-group-append .input-group-text+.btn,.input-group-append .input-group-text+.input-group-text,.input-group-prepend .btn+.btn,.input-group-prepend .btn+.input-group-text,.input-group-prepend .input-group-text+.btn,.input-group-prepend .input-group-text+.input-group-text{margin-left:-1px}.input-group-prepend{margin-right:-1px}.input-group-append{margin-left:-1px}.input-group-text{display:flex;align-items:center;padding:.375rem .75rem;margin-bottom:0;font-size:1rem;font-weight:400;line-height:1.5;color:#495057;text-align:center;white-space:nowrap;background-color:#e9ecef;border:1px solid #ced4da;border-radius:.5rem}.input-group-text input[type=checkbox],.input-group-text input[type=radio]{margin-top:0}.input-group-lg>.custom-select,.input-group-lg>.form-control:not(textarea){height:calc(1.5em + 1rem + 2px)}.input-group-lg>.custom-select,.input-group-lg>.form-control,.input-group-lg>.input-group-append>.btn,.input-group-lg>.input-group-append>.input-group-text,.input-group-lg>.input-group-prepend>.btn,.input-group-lg>.input-group-prepend>.input-group-text{padding:.5rem 1rem;font-size:1.25rem;line-height:1.5;border-radius:.5rem}.input-group-sm>.custom-select,.input-group-sm>.form-control:not(textarea){height:calc(1.5em + .5rem + 2px)}.input-group-sm>.custom-select,.input-group-sm>.form-control,.input-group-sm>.input-group-append>.btn,.input-group-sm>.input-group-append>.input-group-text,.input-group-sm>.input-group-prepend>.btn,.input-group-sm>.input-group-prepend>.input-group-text{padding:.25rem .5rem;font-size:.875rem;line-height:1.5;border-radius:1rem}.input-group-lg>.custom-select,.input-group-sm>.custom-select{padding-right:1.75rem}.input-group>.input-group-append:last-child>.btn:not(:last-child):not(.dropdown-toggle),.input-group>.input-group-append:last-child>.input-group-text:not(:last-child),.input-group>.input-group-append:not(:last-child)>.btn,.input-group>.input-group-append:not(:last-child)>.input-group-text,.input-group>.input-group-prepend>.btn,.input-group>.input-group-prepend>.input-group-text{border-top-right-radius:0;border-bottom-right-radius:0}.input-group>.input-group-append>.btn,.input-group>.input-group-append>.input-group-text,.input-group>.input-group-prepend:first-child>.btn:not(:first-child),.input-group>.input-group-prepend:first-child>.input-group-text:not(:first-child),.input-group>.input-group-prepend:not(:first-child)>.btn,.input-group>.input-group-prepend:not(:first-child)>.input-group-text{border-top-left-radius:0;border-bottom-left-radius:0}.custom-control{position:relative;display:block;min-height:1.5rem;padding-left:1.5rem}.custom-control-inline{display:inline-flex;margin-right:1rem}.custom-control-input{position:absolute;left:0;z-index:-1;width:1rem;height:1.25rem;opacity:0}.custom-control-input:checked~.custom-control-label::before{color:#fff;border-color:#f1641e;background-color:#f1641e}.custom-control-input:focus~.custom-control-label::before{box-shadow:0 0 0 .2rem rgba(241,100,30,.75)}.custom-control-input:focus:not(:checked)~.custom-control-label::before{border-color:#f8b796}.custom-control-input:not(:disabled):active~.custom-control-label::before{color:#fff;background-color:#fbd8c6;border-color:#fbd8c6}.custom-control-input:disabled~.custom-control-label,.custom-control-input[disabled]~.custom-control-label{color:#6c757d}.custom-control-input:disabled~.custom-control-label::before,.custom-control-input[disabled]~.custom-control-label::before{background-color:#e9ecef}.custom-control-label{position:relative;margin-bottom:0;vertical-align:top}.custom-control-label::before{position:absolute;top:.25rem;left:-1.5rem;display:block;width:1rem;height:1rem;pointer-events:none;content:"";background-color:#fff;border:#adb5bd solid 1px}.custom-control-label::after{position:absolute;top:.25rem;left:-1.5rem;display:block;width:1rem;height:1rem;content:"";background:no-repeat 50%/50% 50%}.custom-checkbox .custom-control-label::before{border-radius:.5rem}.custom-checkbox .custom-control-input:checked~.custom-control-label::after{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='8' height='8' viewBox='0 0 8 8'%3e%3cpath fill='%23ffffff' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26l2.974 2.99L8 2.193z'/%3e%3c/svg%3e")}.custom-checkbox .custom-control-input:indeterminate~.custom-control-label::before{border-color:#f1641e;background-color:#f1641e}.custom-checkbox .custom-control-input:indeterminate~.custom-control-label::after{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='4' height='4' viewBox='0 0 4 4'%3e%3cpath stroke='%23ffffff' d='M0 2h4'/%3e%3c/svg%3e")}.custom-checkbox .custom-control-input:disabled:checked~.custom-control-label::before{background-color:rgba(241,100,30,.5)}.custom-checkbox .custom-control-input:disabled:indeterminate~.custom-control-label::before{background-color:rgba(241,100,30,.5)}.custom-radio .custom-control-label::before{border-radius:50%}.custom-radio .custom-control-input:checked~.custom-control-label::after{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%23ffffff'/%3e%3c/svg%3e")}.custom-radio .custom-control-input:disabled:checked~.custom-control-label::before{background-color:rgba(241,100,30,.5)}.custom-switch{padding-left:2.25rem}.custom-switch .custom-control-label::before{left:-2.25rem;width:1.75rem;pointer-events:all;border-radius:.5rem}.custom-switch .custom-control-label::after{top:calc(.25rem + 2px);left:calc(-2.25rem + 2px);width:calc(1rem - 4px);height:calc(1rem - 4px);background-color:#adb5bd;border-radius:.5rem;transition:transform .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.custom-switch .custom-control-label::after{transition:none}}.custom-switch .custom-control-input:checked~.custom-control-label::after{background-color:#fff;transform:translateX(.75rem)}.custom-switch .custom-control-input:disabled:checked~.custom-control-label::before{background-color:rgba(241,100,30,.5)}.custom-select{display:inline-block;width:100%;height:calc(1.5em + .75rem + 2px);padding:.375rem 1.75rem .375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5;color:#495057;vertical-align:middle;background:#fff url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='4' height='5' viewBox='0 0 4 5'%3e%3cpath fill='%23343a40' d='M2 0L0 2h4zm0 5L0 3h4z'/%3e%3c/svg%3e") no-repeat right .75rem center/8px 10px;border:1px solid #ced4da;border-radius:.5rem;appearance:none}.custom-select:focus{border-color:#f8b796;outline:0;box-shadow:0 0 0 .2rem rgba(241,100,30,.75)}.custom-select:focus::-ms-value{color:#495057;background-color:#fff}.custom-select[multiple],.custom-select[size]:not([size="1"]){height:auto;padding-right:.75rem;background-image:none}.custom-select:disabled{color:#6c757d;background-color:#e9ecef}.custom-select::-ms-expand{display:none}.custom-select:-moz-focusring{color:transparent;text-shadow:0 0 0 #495057}.custom-select-sm{height:calc(1.5em + .5rem + 2px);padding-top:.25rem;padding-bottom:.25rem;padding-left:.5rem;font-size:.875rem}.custom-select-lg{height:calc(1.5em + 1rem + 2px);padding-top:.5rem;padding-bottom:.5rem;padding-left:1rem;font-size:1.25rem}.custom-file{position:relative;display:inline-block;width:100%;height:calc(1.5em + .75rem + 2px);margin-bottom:0}.custom-file-input{position:relative;z-index:2;width:100%;height:calc(1.5em + .75rem + 2px);margin:0;opacity:0}.custom-file-input:focus~.custom-file-label{border-color:#f8b796;box-shadow:0 0 0 .2rem rgba(241,100,30,.75)}.custom-file-input:disabled~.custom-file-label,.custom-file-input[disabled]~.custom-file-label{background-color:#e9ecef}.custom-file-input:lang(en)~.custom-file-label::after{content:"Browse"}.custom-file-input~.custom-file-label[data-browse]::after{content:attr(data-browse)}.custom-file-label{position:absolute;top:0;right:0;left:0;z-index:1;height:calc(1.5em + .75rem + 2px);padding:.375rem .75rem;font-weight:400;line-height:1.5;color:#495057;background-color:#fff;border:1px solid #ced4da;border-radius:.5rem}.custom-file-label::after{position:absolute;top:0;right:0;bottom:0;z-index:3;display:block;height:calc(1.5em + .75rem);padding:.375rem .75rem;line-height:1.5;color:#495057;content:"Browse";background-color:#e9ecef;border-left:inherit;border-radius:0 .5rem .5rem 0}.custom-range{width:100%;height:1.4rem;padding:0;background-color:transparent;appearance:none}.custom-range:focus{outline:0}.custom-range:focus::-webkit-slider-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .2rem rgba(241,100,30,.75)}.custom-range:focus::-moz-range-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .2rem rgba(241,100,30,.75)}.custom-range:focus::-ms-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .2rem rgba(241,100,30,.75)}.custom-range::-moz-focus-outer{border:0}.custom-range::-webkit-slider-thumb{width:1rem;height:1rem;margin-top:-.25rem;background-color:#f1641e;border:0;border-radius:1rem;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;appearance:none}@media (prefers-reduced-motion:reduce){.custom-range::-webkit-slider-thumb{transition:none}}.custom-range::-webkit-slider-thumb:active{background-color:#fbd8c6}.custom-range::-webkit-slider-runnable-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:#dee2e6;border-color:transparent;border-radius:1rem}.custom-range::-moz-range-thumb{width:1rem;height:1rem;background-color:#f1641e;border:0;border-radius:1rem;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;appearance:none}@media (prefers-reduced-motion:reduce){.custom-range::-moz-range-thumb{transition:none}}.custom-range::-moz-range-thumb:active{background-color:#fbd8c6}.custom-range::-moz-range-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:#dee2e6;border-color:transparent;border-radius:1rem}.custom-range::-ms-thumb{width:1rem;height:1rem;margin-top:0;margin-right:.2rem;margin-left:.2rem;background-color:#f1641e;border:0;border-radius:1rem;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;appearance:none}@media (prefers-reduced-motion:reduce){.custom-range::-ms-thumb{transition:none}}.custom-range::-ms-thumb:active{background-color:#fbd8c6}.custom-range::-ms-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:transparent;border-color:transparent;border-width:.5rem}.custom-range::-ms-fill-lower{background-color:#dee2e6;border-radius:1rem}.custom-range::-ms-fill-upper{margin-right:15px;background-color:#dee2e6;border-radius:1rem}.custom-range:disabled::-webkit-slider-thumb{background-color:#adb5bd}.custom-range:disabled::-webkit-slider-runnable-track{cursor:default}.custom-range:disabled::-moz-range-thumb{background-color:#adb5bd}.custom-range:disabled::-moz-range-track{cursor:default}.custom-range:disabled::-ms-thumb{background-color:#adb5bd}.custom-control-label::before,.custom-file-label,.custom-select{transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.custom-control-label::before,.custom-file-label,.custom-select{transition:none}}.nav{display:flex;flex-wrap:wrap;padding-left:0;margin-bottom:0;list-style:none}.nav-link{display:block;padding:.5rem 1rem}.nav-link:focus,.nav-link:hover{text-decoration:none}.nav-link.disabled{color:#6c757d;pointer-events:none;cursor:default}.nav-tabs{border-bottom:1px solid #dee2e6}.nav-tabs .nav-item{margin-bottom:-1px}.nav-tabs .nav-link{border:1px solid transparent;border-top-left-radius:.5rem;border-top-right-radius:.5rem}.nav-tabs .nav-link:focus,.nav-tabs .nav-link:hover{border-color:#e9ecef #e9ecef #dee2e6}.nav-tabs .nav-link.disabled{color:#6c757d;background-color:transparent;border-color:transparent}.nav-tabs .nav-item.show .nav-link,.nav-tabs .nav-link.active{color:#495057;background-color:#fff;border-color:#dee2e6 #dee2e6 #fff}.nav-tabs .dropdown-menu{margin-top:-1px;border-top-left-radius:0;border-top-right-radius:0}.nav-pills .nav-link{border-radius:.5rem}.nav-pills .nav-link.active,.nav-pills .show>.nav-link{color:#fff;background-color:#f1641e}.nav-fill .nav-item{flex:1 1 auto;text-align:center}.nav-justified .nav-item{flex-basis:0;flex-grow:1;text-align:center}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.navbar{position:relative;display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;padding:.5rem 1rem}.navbar .container,.navbar .container-fluid,.navbar .container-lg,.navbar .container-md,.navbar .container-sm,.navbar .container-xl{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between}.navbar-brand{display:inline-block;padding-top:.3125rem;padding-bottom:.3125rem;margin-right:1rem;font-size:1.25rem;line-height:inherit;white-space:nowrap}.navbar-brand:focus,.navbar-brand:hover{text-decoration:none}.navbar-nav{display:flex;flex-direction:column;padding-left:0;margin-bottom:0;list-style:none}.navbar-nav .nav-link{padding-right:0;padding-left:0}.navbar-nav .dropdown-menu{position:static;float:none}.navbar-text{display:inline-block;padding-top:.5rem;padding-bottom:.5rem}.navbar-collapse{flex-basis:100%;flex-grow:1;align-items:center}.navbar-toggler{padding:.25rem .75rem;font-size:1.25rem;line-height:1;background-color:transparent;border:1px solid transparent;border-radius:.5rem}.navbar-toggler:focus,.navbar-toggler:hover{text-decoration:none}.navbar-toggler-icon{display:inline-block;width:1.5em;height:1.5em;vertical-align:middle;content:"";background:no-repeat center center;background-size:100% 100%}@media (max-width:575.98px){.navbar-expand-sm>.container,.navbar-expand-sm>.container-fluid,.navbar-expand-sm>.container-lg,.navbar-expand-sm>.container-md,.navbar-expand-sm>.container-sm,.navbar-expand-sm>.container-xl{padding-right:0;padding-left:0}}@media (min-width:576px){.navbar-expand-sm{flex-flow:row nowrap;justify-content:flex-start}.navbar-expand-sm .navbar-nav{flex-direction:row}.navbar-expand-sm .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-sm .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-sm>.container,.navbar-expand-sm>.container-fluid,.navbar-expand-sm>.container-lg,.navbar-expand-sm>.container-md,.navbar-expand-sm>.container-sm,.navbar-expand-sm>.container-xl{flex-wrap:nowrap}.navbar-expand-sm .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-sm .navbar-toggler{display:none}}@media (max-width:767.98px){.navbar-expand-md>.container,.navbar-expand-md>.container-fluid,.navbar-expand-md>.container-lg,.navbar-expand-md>.container-md,.navbar-expand-md>.container-sm,.navbar-expand-md>.container-xl{padding-right:0;padding-left:0}}@media (min-width:768px){.navbar-expand-md{flex-flow:row nowrap;justify-content:flex-start}.navbar-expand-md .navbar-nav{flex-direction:row}.navbar-expand-md .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-md .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-md>.container,.navbar-expand-md>.container-fluid,.navbar-expand-md>.container-lg,.navbar-expand-md>.container-md,.navbar-expand-md>.container-sm,.navbar-expand-md>.container-xl{flex-wrap:nowrap}.navbar-expand-md .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-md .navbar-toggler{display:none}}@media (max-width:991.98px){.navbar-expand-lg>.container,.navbar-expand-lg>.container-fluid,.navbar-expand-lg>.container-lg,.navbar-expand-lg>.container-md,.navbar-expand-lg>.container-sm,.navbar-expand-lg>.container-xl{padding-right:0;padding-left:0}}@media (min-width:992px){.navbar-expand-lg{flex-flow:row nowrap;justify-content:flex-start}.navbar-expand-lg .navbar-nav{flex-direction:row}.navbar-expand-lg .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-lg .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-lg>.container,.navbar-expand-lg>.container-fluid,.navbar-expand-lg>.container-lg,.navbar-expand-lg>.container-md,.navbar-expand-lg>.container-sm,.navbar-expand-lg>.container-xl{flex-wrap:nowrap}.navbar-expand-lg .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-lg .navbar-toggler{display:none}}@media (max-width:1199.98px){.navbar-expand-xl>.container,.navbar-expand-xl>.container-fluid,.navbar-expand-xl>.container-lg,.navbar-expand-xl>.container-md,.navbar-expand-xl>.container-sm,.navbar-expand-xl>.container-xl{padding-right:0;padding-left:0}}@media (min-width:1200px){.navbar-expand-xl{flex-flow:row nowrap;justify-content:flex-start}.navbar-expand-xl .navbar-nav{flex-direction:row}.navbar-expand-xl .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-xl .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-xl>.container,.navbar-expand-xl>.container-fluid,.navbar-expand-xl>.container-lg,.navbar-expand-xl>.container-md,.navbar-expand-xl>.container-sm,.navbar-expand-xl>.container-xl{flex-wrap:nowrap}.navbar-expand-xl .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-xl .navbar-toggler{display:none}}.navbar-expand{flex-flow:row nowrap;justify-content:flex-start}.navbar-expand>.container,.navbar-expand>.container-fluid,.navbar-expand>.container-lg,.navbar-expand>.container-md,.navbar-expand>.container-sm,.navbar-expand>.container-xl{padding-right:0;padding-left:0}.navbar-expand .navbar-nav{flex-direction:row}.navbar-expand .navbar-nav .dropdown-menu{position:absolute}.navbar-expand .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand>.container,.navbar-expand>.container-fluid,.navbar-expand>.container-lg,.navbar-expand>.container-md,.navbar-expand>.container-sm,.navbar-expand>.container-xl{flex-wrap:nowrap}.navbar-expand .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand .navbar-toggler{display:none}.navbar-light .navbar-brand{color:#212529}.navbar-light .navbar-brand:focus,.navbar-light .navbar-brand:hover{color:#212529}.navbar-light .navbar-nav .nav-link{color:#6c757d}.navbar-light .navbar-nav .nav-link:focus,.navbar-light .navbar-nav .nav-link:hover{color:#212529}.navbar-light .navbar-nav .nav-link.disabled{color:rgba(34,34,34,.3)}.navbar-light .navbar-nav .active>.nav-link,.navbar-light .navbar-nav .nav-link.active,.navbar-light .navbar-nav .nav-link.show,.navbar-light .navbar-nav .show>.nav-link{color:#212529}.navbar-light .navbar-toggler{color:#6c757d;border-color:rgba(34,34,34,.1)}.navbar-light .navbar-toggler-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='30' height='30' viewBox='0 0 30 30'%3e%3cpath stroke='%236c757d' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e")}.navbar-light .navbar-text{color:#6c757d}.navbar-light .navbar-text a{color:#212529}.navbar-light .navbar-text a:focus,.navbar-light .navbar-text a:hover{color:#212529}.navbar-dark .navbar-brand{color:#fff}.navbar-dark .navbar-brand:focus,.navbar-dark .navbar-brand:hover{color:#fff}.navbar-dark .navbar-nav .nav-link{color:rgba(255,255,255,.5)}.navbar-dark .navbar-nav .nav-link:focus,.navbar-dark .navbar-nav .nav-link:hover{color:rgba(255,255,255,.75)}.navbar-dark .navbar-nav .nav-link.disabled{color:rgba(255,255,255,.25)}.navbar-dark .navbar-nav .active>.nav-link,.navbar-dark .navbar-nav .nav-link.active,.navbar-dark .navbar-nav .nav-link.show,.navbar-dark .navbar-nav .show>.nav-link{color:#fff}.navbar-dark .navbar-toggler{color:rgba(255,255,255,.5);border-color:rgba(34,34,34,.1)}.navbar-dark .navbar-toggler-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='30' height='30' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.5%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e")}.navbar-dark .navbar-text{color:rgba(255,255,255,.5)}.navbar-dark .navbar-text a{color:#fff}.navbar-dark .navbar-text a:focus,.navbar-dark .navbar-text a:hover{color:#fff}.card{position:relative;display:flex;flex-direction:column;min-width:0;word-wrap:break-word;background-color:#f8f9fa;background-clip:border-box;border:1px solid rgba(34,34,34,.125);border-radius:.5rem}.card>hr{margin-right:0;margin-left:0}.card>.list-group{border-top:inherit;border-bottom:inherit}.card>.list-group:first-child{border-top-width:0;border-top-left-radius:calc(.5rem - 1px);border-top-right-radius:calc(.5rem - 1px)}.card>.list-group:last-child{border-bottom-width:0;border-bottom-right-radius:calc(.5rem - 1px);border-bottom-left-radius:calc(.5rem - 1px)}.card-body{flex:1 1 auto;min-height:1px;padding:1.25rem;color:#495057}.card-title{margin-bottom:.75rem}.card-subtitle{margin-top:-.375rem;margin-bottom:0}.card-text:last-child{margin-bottom:0}.card-link:hover{text-decoration:none}.card-link+.card-link{margin-left:1.25rem}.card-header{padding:.75rem 1.25rem;margin-bottom:0;color:#495057;background-color:rgba(34,34,34,.03);border-bottom:1px solid rgba(34,34,34,.125)}.card-header:first-child{border-radius:calc(.5rem - 1px) calc(.5rem - 1px) 0 0}.card-header+.list-group .list-group-item:first-child{border-top:0}.card-footer{padding:.75rem 1.25rem;color:#495057;background-color:rgba(34,34,34,.03);border-top:1px solid rgba(34,34,34,.125)}.card-footer:last-child{border-radius:0 0 calc(.5rem - 1px) calc(.5rem - 1px)}.card-header-tabs{margin-right:-.625rem;margin-bottom:-.75rem;margin-left:-.625rem;border-bottom:0}.card-header-pills{margin-right:-.625rem;margin-left:-.625rem}.card-img-overlay{position:absolute;top:0;right:0;bottom:0;left:0;padding:1.25rem}.card-img,.card-img-bottom,.card-img-top{flex-shrink:0;width:100%}.card-img,.card-img-top{border-top-left-radius:calc(.5rem - 1px);border-top-right-radius:calc(.5rem - 1px)}.card-img,.card-img-bottom{border-bottom-right-radius:calc(.5rem - 1px);border-bottom-left-radius:calc(.5rem - 1px)}.card-deck .card{margin-bottom:15px}@media (min-width:576px){.card-deck{display:flex;flex-flow:row wrap;margin-right:-15px;margin-left:-15px}.card-deck .card{flex:1 0 0%;margin-right:15px;margin-bottom:0;margin-left:15px}}.card-group>.card{margin-bottom:15px}@media (min-width:576px){.card-group{display:flex;flex-flow:row wrap}.card-group>.card{flex:1 0 0%;margin-bottom:0}.card-group>.card+.card{margin-left:0;border-left:0}.card-group>.card:not(:last-child){border-top-right-radius:0;border-bottom-right-radius:0}.card-group>.card:not(:last-child) .card-header,.card-group>.card:not(:last-child) .card-img-top{border-top-right-radius:0}.card-group>.card:not(:last-child) .card-footer,.card-group>.card:not(:last-child) .card-img-bottom{border-bottom-right-radius:0}.card-group>.card:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.card-group>.card:not(:first-child) .card-header,.card-group>.card:not(:first-child) .card-img-top{border-top-left-radius:0}.card-group>.card:not(:first-child) .card-footer,.card-group>.card:not(:first-child) .card-img-bottom{border-bottom-left-radius:0}}.card-columns .card{margin-bottom:.75rem}@media (min-width:576px){.card-columns{column-count:3;column-gap:1.25rem;orphans:1;widows:1}.card-columns .card{display:inline-block;width:100%}}.accordion>.card{overflow:hidden}.accordion>.card:not(:last-of-type){border-bottom:0;border-bottom-right-radius:0;border-bottom-left-radius:0}.accordion>.card:not(:first-of-type){border-top-left-radius:0;border-top-right-radius:0}.accordion>.card>.card-header{border-radius:0;margin-bottom:-1px}.breadcrumb{display:flex;flex-wrap:wrap;padding:.75rem 1rem;margin-bottom:1rem;list-style:none;background-color:#e9ecef;border-radius:.5rem}.breadcrumb-item{display:flex}.breadcrumb-item+.breadcrumb-item{padding-left:.5rem}.breadcrumb-item+.breadcrumb-item::before{display:inline-block;padding-right:.5rem;color:#6c757d;content:"/"}.breadcrumb-item+.breadcrumb-item:hover::before{text-decoration:underline}.breadcrumb-item+.breadcrumb-item:hover::before{text-decoration:none}.breadcrumb-item.active{color:#6c757d}.pagination{display:flex;padding-left:0;list-style:none;border-radius:.5rem}.page-link{position:relative;display:block;padding:.5rem .75rem;margin-left:-1px;line-height:1.25;color:#f1641e;background-color:#fff;border:1px solid #dee2e6}.page-link:hover{z-index:2;color:#b7440b;text-decoration:none;background-color:#e9ecef;border-color:#dee2e6}.page-link:focus{z-index:3;outline:0;box-shadow:0 0 0 .2rem rgba(241,100,30,.75)}.page-item:first-child .page-link{margin-left:0;border-top-left-radius:.5rem;border-bottom-left-radius:.5rem}.page-item:last-child .page-link{border-top-right-radius:.5rem;border-bottom-right-radius:.5rem}.page-item.active .page-link{z-index:3;color:#fff;background-color:#f1641e;border-color:#f1641e}.page-item.disabled .page-link{color:#6c757d;pointer-events:none;cursor:auto;background-color:#fff;border-color:#dee2e6}.pagination-lg .page-link{padding:.75rem 1.5rem;font-size:1.25rem;line-height:1.5}.pagination-lg .page-item:first-child .page-link{border-top-left-radius:.5rem;border-bottom-left-radius:.5rem}.pagination-lg .page-item:last-child .page-link{border-top-right-radius:.5rem;border-bottom-right-radius:.5rem}.pagination-sm .page-link{padding:.25rem .5rem;font-size:.875rem;line-height:1.5}.pagination-sm .page-item:first-child .page-link{border-top-left-radius:1rem;border-bottom-left-radius:1rem}.pagination-sm .page-item:last-child .page-link{border-top-right-radius:1rem;border-bottom-right-radius:1rem}.badge{display:inline-block;padding:.25em .4em;font-size:75%;font-weight:600;line-height:1;text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:.5rem;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.badge{transition:none}}a.badge:focus,a.badge:hover{text-decoration:none}.badge:empty{display:none}.btn .badge{position:relative;top:-1px}.badge-pill{padding-right:.6em;padding-left:.6em;border-radius:10rem}.badge-primary{color:#fff;background-color:#f1641e}a.badge-primary:focus,a.badge-primary:hover{color:#fff;background-color:#cf4d0d}a.badge-primary.focus,a.badge-primary:focus{outline:0;box-shadow:0 0 0 .2rem rgba(241,100,30,.5)}.badge-secondary{color:#fff;background-color:#00c853}a.badge-secondary:focus,a.badge-secondary:hover{color:#fff;background-color:#00953e}a.badge-secondary.focus,a.badge-secondary:focus{outline:0;box-shadow:0 0 0 .2rem rgba(0,200,83,.5)}.badge-success{color:#fff;background-color:#6610f2}a.badge-success:focus,a.badge-success:hover{color:#fff;background-color:#510bc4}a.badge-success.focus,a.badge-success:focus{outline:0;box-shadow:0 0 0 .2rem rgba(102,16,242,.5)}.badge-info{color:#fff;background-color:#007bff}a.badge-info:focus,a.badge-info:hover{color:#fff;background-color:#0062cc}a.badge-info.focus,a.badge-info:focus{outline:0;box-shadow:0 0 0 .2rem rgba(0,123,255,.5)}.badge-warning{color:#212529;background-color:#ffc107}a.badge-warning:focus,a.badge-warning:hover{color:#212529;background-color:#d39e00}a.badge-warning.focus,a.badge-warning:focus{outline:0;box-shadow:0 0 0 .2rem rgba(255,193,7,.5)}.badge-danger{color:#fff;background-color:#873208}a.badge-danger:focus,a.badge-danger:hover{color:#fff;background-color:#572105}a.badge-danger.focus,a.badge-danger:focus{outline:0;box-shadow:0 0 0 .2rem rgba(135,50,8,.5)}.badge-light{color:#212529;background-color:#f8f9fa}a.badge-light:focus,a.badge-light:hover{color:#212529;background-color:#dae0e5}a.badge-light.focus,a.badge-light:focus{outline:0;box-shadow:0 0 0 .2rem rgba(248,249,250,.5)}.badge-dark{color:#fff;background-color:#343a40}a.badge-dark:focus,a.badge-dark:hover{color:#fff;background-color:#1d2124}a.badge-dark.focus,a.badge-dark:focus{outline:0;box-shadow:0 0 0 .2rem rgba(52,58,64,.5)}.jumbotron{padding:2rem 1rem;margin-bottom:2rem;background-color:#e9ecef;border-radius:.5rem}@media (min-width:576px){.jumbotron{padding:4rem 2rem}}.jumbotron-fluid{padding-right:0;padding-left:0;border-radius:0}.alert{position:relative;padding:.75rem 1.25rem;margin-bottom:1rem;border:1px solid transparent;border-radius:.5rem}.alert-heading{color:inherit}.alert-link{font-weight:600}.alert-dismissible{padding-right:4rem}.alert-dismissible .close{position:absolute;top:0;right:0;padding:.75rem 1.25rem;color:inherit}.alert-primary{color:#8e4420;background-color:#fce0d2;border-color:#fbd4c0}.alert-primary hr{border-top-color:#f9c4a8}.alert-primary .alert-link{color:#643017}.alert-secondary{color:#10783b;background-color:#ccf4dd;border-color:#b8f0cf}.alert-secondary hr{border-top-color:#a3ecc1}.alert-secondary .alert-link{color:#0a4b25}.alert-success{color:#45198e;background-color:#e0cffc;border-color:#d4bcfb}.alert-success hr{border-top-color:#c5a4fa}.alert-success .alert-link{color:#301163}.alert-info{color:#105095;background-color:#cce5ff;border-color:#b8daff}.alert-info hr{border-top-color:#9fcdff}.alert-info .alert-link{color:#0b3767}.alert-warning{color:#957514;background-color:#fff3cd;border-color:#ffeeba}.alert-warning hr{border-top-color:#ffe8a1}.alert-warning .alert-link{color:#68520e}.alert-danger{color:#572b15;background-color:#e7d6ce;border-color:#ddc6ba}.alert-danger hr{border-top-color:#d5b8a9}.alert-danger .alert-link{color:#2e170b}.alert-light{color:#919292;background-color:#fefefe;border-color:#fdfdfe}.alert-light hr{border-top-color:#ececf6}.alert-light .alert-link{color:#777979}.alert-dark{color:#2b2e32;background-color:#d6d8d9;border-color:#c6c8ca}.alert-dark hr{border-top-color:#b9bbbe}.alert-dark .alert-link{color:#131517}@keyframes progress-bar-stripes{from{background-position:1rem 0}to{background-position:0 0}}.progress{display:flex;height:1rem;overflow:hidden;line-height:0;font-size:.75rem;background-color:#e9ecef;border-radius:.5rem}.progress-bar{display:flex;flex-direction:column;justify-content:center;overflow:hidden;color:#fff;text-align:center;white-space:nowrap;background-color:#f1641e;transition:width .6s ease}@media (prefers-reduced-motion:reduce){.progress-bar{transition:none}}.progress-bar-striped{background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-size:1rem 1rem}.progress-bar-animated{animation:progress-bar-stripes 1s linear infinite}@media (prefers-reduced-motion:reduce){.progress-bar-animated{animation:none}}.media{display:flex;align-items:flex-start}.media-body{flex:1}.list-group{display:flex;flex-direction:column;padding-left:0;margin-bottom:0;border-radius:.5rem}.list-group-item-action{width:100%;color:#495057;text-align:inherit}.list-group-item-action:focus,.list-group-item-action:hover{z-index:1;color:#495057;text-decoration:none;background-color:#f8f9fa}.list-group-item-action:active{color:#495057;background-color:#e9ecef}.list-group-item{position:relative;display:block;padding:.75rem 1.25rem;background-color:#fff;border:1px solid rgba(34,34,34,.125)}.list-group-item:first-child{border-top-left-radius:inherit;border-top-right-radius:inherit}.list-group-item:last-child{border-bottom-right-radius:inherit;border-bottom-left-radius:inherit}.list-group-item.disabled,.list-group-item:disabled{color:#6c757d;pointer-events:none;background-color:#fff}.list-group-item.active{z-index:2;color:#fff;background-color:#f1641e;border-color:#f1641e}.list-group-item+.list-group-item{border-top-width:0}.list-group-item+.list-group-item.active{margin-top:-1px;border-top-width:1px}.list-group-horizontal{flex-direction:row}.list-group-horizontal>.list-group-item:first-child{border-bottom-left-radius:.5rem;border-top-right-radius:0}.list-group-horizontal>.list-group-item:last-child{border-top-right-radius:.5rem;border-bottom-left-radius:0}.list-group-horizontal>.list-group-item.active{margin-top:0}.list-group-horizontal>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}@media (min-width:576px){.list-group-horizontal-sm{flex-direction:row}.list-group-horizontal-sm>.list-group-item:first-child{border-bottom-left-radius:.5rem;border-top-right-radius:0}.list-group-horizontal-sm>.list-group-item:last-child{border-top-right-radius:.5rem;border-bottom-left-radius:0}.list-group-horizontal-sm>.list-group-item.active{margin-top:0}.list-group-horizontal-sm>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-sm>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}@media (min-width:768px){.list-group-horizontal-md{flex-direction:row}.list-group-horizontal-md>.list-group-item:first-child{border-bottom-left-radius:.5rem;border-top-right-radius:0}.list-group-horizontal-md>.list-group-item:last-child{border-top-right-radius:.5rem;border-bottom-left-radius:0}.list-group-horizontal-md>.list-group-item.active{margin-top:0}.list-group-horizontal-md>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-md>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}@media (min-width:992px){.list-group-horizontal-lg{flex-direction:row}.list-group-horizontal-lg>.list-group-item:first-child{border-bottom-left-radius:.5rem;border-top-right-radius:0}.list-group-horizontal-lg>.list-group-item:last-child{border-top-right-radius:.5rem;border-bottom-left-radius:0}.list-group-horizontal-lg>.list-group-item.active{margin-top:0}.list-group-horizontal-lg>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-lg>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}@media (min-width:1200px){.list-group-horizontal-xl{flex-direction:row}.list-group-horizontal-xl>.list-group-item:first-child{border-bottom-left-radius:.5rem;border-top-right-radius:0}.list-group-horizontal-xl>.list-group-item:last-child{border-top-right-radius:.5rem;border-bottom-left-radius:0}.list-group-horizontal-xl>.list-group-item.active{margin-top:0}.list-group-horizontal-xl>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-xl>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}.list-group-flush{border-radius:0}.list-group-flush>.list-group-item{border-width:0 0 1px}.list-group-flush>.list-group-item:last-child{border-bottom-width:0}.list-group-item-primary{color:#8e4420;background-color:#fbd4c0}.list-group-item-primary.list-group-item-action:focus,.list-group-item-primary.list-group-item-action:hover{color:#8e4420;background-color:#f9c4a8}.list-group-item-primary.list-group-item-action.active{color:#fff;background-color:#8e4420;border-color:#8e4420}.list-group-item-secondary{color:#10783b;background-color:#b8f0cf}.list-group-item-secondary.list-group-item-action:focus,.list-group-item-secondary.list-group-item-action:hover{color:#10783b;background-color:#a3ecc1}.list-group-item-secondary.list-group-item-action.active{color:#fff;background-color:#10783b;border-color:#10783b}.list-group-item-success{color:#45198e;background-color:#d4bcfb}.list-group-item-success.list-group-item-action:focus,.list-group-item-success.list-group-item-action:hover{color:#45198e;background-color:#c5a4fa}.list-group-item-success.list-group-item-action.active{color:#fff;background-color:#45198e;border-color:#45198e}.list-group-item-info{color:#105095;background-color:#b8daff}.list-group-item-info.list-group-item-action:focus,.list-group-item-info.list-group-item-action:hover{color:#105095;background-color:#9fcdff}.list-group-item-info.list-group-item-action.active{color:#fff;background-color:#105095;border-color:#105095}.list-group-item-warning{color:#957514;background-color:#ffeeba}.list-group-item-warning.list-group-item-action:focus,.list-group-item-warning.list-group-item-action:hover{color:#957514;background-color:#ffe8a1}.list-group-item-warning.list-group-item-action.active{color:#fff;background-color:#957514;border-color:#957514}.list-group-item-danger{color:#572b15;background-color:#ddc6ba}.list-group-item-danger.list-group-item-action:focus,.list-group-item-danger.list-group-item-action:hover{color:#572b15;background-color:#d5b8a9}.list-group-item-danger.list-group-item-action.active{color:#fff;background-color:#572b15;border-color:#572b15}.list-group-item-light{color:#919292;background-color:#fdfdfe}.list-group-item-light.list-group-item-action:focus,.list-group-item-light.list-group-item-action:hover{color:#919292;background-color:#ececf6}.list-group-item-light.list-group-item-action.active{color:#fff;background-color:#919292;border-color:#919292}.list-group-item-dark{color:#2b2e32;background-color:#c6c8ca}.list-group-item-dark.list-group-item-action:focus,.list-group-item-dark.list-group-item-action:hover{color:#2b2e32;background-color:#b9bbbe}.list-group-item-dark.list-group-item-action.active{color:#fff;background-color:#2b2e32;border-color:#2b2e32}.close{float:right;font-size:1.5rem;font-weight:600;line-height:1;color:#222;text-shadow:0 1px 0 #fff;opacity:.5}.close:hover{color:#222;text-decoration:none}.close:not(:disabled):not(.disabled):focus,.close:not(:disabled):not(.disabled):hover{opacity:.75}button.close{padding:0;background-color:transparent;border:0}a.close.disabled{pointer-events:none}.toast{max-width:350px;overflow:hidden;font-size:.875rem;background-color:rgba(255,255,255,.85);background-clip:padding-box;border:1px solid rgba(0,0,0,.1);box-shadow:0 .25rem .75rem rgba(34,34,34,.1);backdrop-filter:blur(10px);opacity:0;border-radius:.25rem}.toast:not(:last-child){margin-bottom:.75rem}.toast.showing{opacity:1}.toast.show{display:block;opacity:1}.toast.hide{display:none}.toast-header{display:flex;align-items:center;padding:.25rem .75rem;color:#6c757d;background-color:rgba(255,255,255,.85);background-clip:padding-box;border-bottom:1px solid rgba(0,0,0,.05)}.toast-body{padding:.75rem}.modal-open{overflow:hidden}.modal-open .modal{overflow-x:hidden;overflow-y:auto}.modal{position:fixed;top:0;left:0;z-index:1050;display:none;width:100%;height:100%;overflow:hidden;outline:0}.modal-dialog{position:relative;width:auto;margin:.5rem;pointer-events:none}.modal.fade .modal-dialog{transition:transform .3s ease-out;transform:translate(0,-50px)}@media (prefers-reduced-motion:reduce){.modal.fade .modal-dialog{transition:none}}.modal.show .modal-dialog{transform:none}.modal.modal-static .modal-dialog{transform:scale(1.02)}.modal-dialog-scrollable{display:flex;max-height:calc(100% - 1rem)}.modal-dialog-scrollable .modal-content{max-height:calc(100vh - 1rem);overflow:hidden}.modal-dialog-scrollable .modal-footer,.modal-dialog-scrollable .modal-header{flex-shrink:0}.modal-dialog-scrollable .modal-body{overflow-y:auto}.modal-dialog-centered{display:flex;align-items:center;min-height:calc(100% - 1rem)}.modal-dialog-centered::before{display:block;height:calc(100vh - 1rem);height:min-content;content:""}.modal-dialog-centered.modal-dialog-scrollable{flex-direction:column;justify-content:center;height:100%}.modal-dialog-centered.modal-dialog-scrollable .modal-content{max-height:none}.modal-dialog-centered.modal-dialog-scrollable::before{content:none}.modal-content{position:relative;display:flex;flex-direction:column;width:100%;pointer-events:auto;background-color:#fff;background-clip:padding-box;border:1px solid rgba(34,34,34,.2);border-radius:.5rem;outline:0}.modal-backdrop{position:fixed;top:0;left:0;z-index:1040;width:100vw;height:100vh;background-color:#222}.modal-backdrop.fade{opacity:0}.modal-backdrop.show{opacity:.5}.modal-header{display:flex;align-items:flex-start;justify-content:space-between;padding:1rem 1rem;border-bottom:1px solid #495057;border-top-left-radius:calc(.5rem - 1px);border-top-right-radius:calc(.5rem - 1px)}.modal-header .close{padding:1rem 1rem;margin:-1rem -1rem -1rem auto}.modal-title{margin-bottom:0;line-height:1.5}.modal-body{position:relative;flex:1 1 auto;padding:1rem}.modal-footer{display:flex;flex-wrap:wrap;align-items:center;justify-content:flex-end;padding:.75rem;border-top:1px solid #495057;border-bottom-right-radius:calc(.5rem - 1px);border-bottom-left-radius:calc(.5rem - 1px)}.modal-footer>*{margin:.25rem}.modal-scrollbar-measure{position:absolute;top:-9999px;width:50px;height:50px;overflow:scroll}@media (min-width:576px){.modal-dialog{max-width:500px;margin:1.75rem auto}.modal-dialog-scrollable{max-height:calc(100% - 3.5rem)}.modal-dialog-scrollable .modal-content{max-height:calc(100vh - 3.5rem)}.modal-dialog-centered{min-height:calc(100% - 3.5rem)}.modal-dialog-centered::before{height:calc(100vh - 3.5rem);height:min-content}.modal-sm{max-width:300px}}@media (min-width:992px){.modal-lg,.modal-xl{max-width:800px}}@media (min-width:1200px){.modal-xl{max-width:1140px}}.tooltip{position:absolute;z-index:1070;display:block;margin:0;font-family:-apple-system,BlinkMacSystemFont,"Droid Sans","Segoe UI",Helvetica,Arial,sans-serif;font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;white-space:normal;line-break:auto;font-size:.875rem;word-wrap:break-word;opacity:0}.tooltip.show{opacity:.9}.tooltip .arrow{position:absolute;display:block;width:.8rem;height:.4rem}.tooltip .arrow::before{position:absolute;content:"";border-color:transparent;border-style:solid}.bs-tooltip-auto[x-placement^=top],.bs-tooltip-top{padding:.4rem 0}.bs-tooltip-auto[x-placement^=top] .arrow,.bs-tooltip-top .arrow{bottom:0}.bs-tooltip-auto[x-placement^=top] .arrow::before,.bs-tooltip-top .arrow::before{top:0;border-width:.4rem .4rem 0;border-top-color:#222}.bs-tooltip-auto[x-placement^=right],.bs-tooltip-right{padding:0 .4rem}.bs-tooltip-auto[x-placement^=right] .arrow,.bs-tooltip-right .arrow{left:0;width:.4rem;height:.8rem}.bs-tooltip-auto[x-placement^=right] .arrow::before,.bs-tooltip-right .arrow::before{right:0;border-width:.4rem .4rem .4rem 0;border-right-color:#222}.bs-tooltip-auto[x-placement^=bottom],.bs-tooltip-bottom{padding:.4rem 0}.bs-tooltip-auto[x-placement^=bottom] .arrow,.bs-tooltip-bottom .arrow{top:0}.bs-tooltip-auto[x-placement^=bottom] .arrow::before,.bs-tooltip-bottom .arrow::before{bottom:0;border-width:0 .4rem .4rem;border-bottom-color:#222}.bs-tooltip-auto[x-placement^=left],.bs-tooltip-left{padding:0 .4rem}.bs-tooltip-auto[x-placement^=left] .arrow,.bs-tooltip-left .arrow{right:0;width:.4rem;height:.8rem}.bs-tooltip-auto[x-placement^=left] .arrow::before,.bs-tooltip-left .arrow::before{left:0;border-width:.4rem 0 .4rem .4rem;border-left-color:#222}.tooltip-inner{max-width:200px;padding:.25rem .5rem;color:#fff;text-align:center;background-color:#222;border-radius:.5rem}.popover{position:absolute;top:0;left:0;z-index:1060;display:block;max-width:276px;font-family:-apple-system,BlinkMacSystemFont,"Droid Sans","Segoe UI",Helvetica,Arial,sans-serif;font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;white-space:normal;line-break:auto;font-size:.875rem;word-wrap:break-word;background-color:#fff;background-clip:padding-box;border:1px solid rgba(34,34,34,.2);border-radius:.5rem}.popover .arrow{position:absolute;display:block;width:1rem;height:.5rem;margin:0 .5rem}.popover .arrow::after,.popover .arrow::before{position:absolute;display:block;content:"";border-color:transparent;border-style:solid}.bs-popover-auto[x-placement^=top],.bs-popover-top{margin-bottom:.5rem}.bs-popover-auto[x-placement^=top]>.arrow,.bs-popover-top>.arrow{bottom:calc(-.5rem - 1px)}.bs-popover-auto[x-placement^=top]>.arrow::before,.bs-popover-top>.arrow::before{bottom:0;border-width:.5rem .5rem 0;border-top-color:rgba(34,34,34,.25)}.bs-popover-auto[x-placement^=top]>.arrow::after,.bs-popover-top>.arrow::after{bottom:1px;border-width:.5rem .5rem 0;border-top-color:#fff}.bs-popover-auto[x-placement^=right],.bs-popover-right{margin-left:.5rem}.bs-popover-auto[x-placement^=right]>.arrow,.bs-popover-right>.arrow{left:calc(-.5rem - 1px);width:.5rem;height:1rem;margin:.5rem 0}.bs-popover-auto[x-placement^=right]>.arrow::before,.bs-popover-right>.arrow::before{left:0;border-width:.5rem .5rem .5rem 0;border-right-color:rgba(34,34,34,.25)}.bs-popover-auto[x-placement^=right]>.arrow::after,.bs-popover-right>.arrow::after{left:1px;border-width:.5rem .5rem .5rem 0;border-right-color:#fff}.bs-popover-auto[x-placement^=bottom],.bs-popover-bottom{margin-top:.5rem}.bs-popover-auto[x-placement^=bottom]>.arrow,.bs-popover-bottom>.arrow{top:calc(-.5rem - 1px)}.bs-popover-auto[x-placement^=bottom]>.arrow::before,.bs-popover-bottom>.arrow::before{top:0;border-width:0 .5rem .5rem .5rem;border-bottom-color:rgba(34,34,34,.25)}.bs-popover-auto[x-placement^=bottom]>.arrow::after,.bs-popover-bottom>.arrow::after{top:1px;border-width:0 .5rem .5rem .5rem;border-bottom-color:#fff}.bs-popover-auto[x-placement^=bottom] .popover-header::before,.bs-popover-bottom .popover-header::before{position:absolute;top:0;left:50%;display:block;width:1rem;margin-left:-.5rem;content:"";border-bottom:1px solid #f7f7f7}.bs-popover-auto[x-placement^=left],.bs-popover-left{margin-right:.5rem}.bs-popover-auto[x-placement^=left]>.arrow,.bs-popover-left>.arrow{right:calc(-.5rem - 1px);width:.5rem;height:1rem;margin:.5rem 0}.bs-popover-auto[x-placement^=left]>.arrow::before,.bs-popover-left>.arrow::before{right:0;border-width:.5rem 0 .5rem .5rem;border-left-color:rgba(34,34,34,.25)}.bs-popover-auto[x-placement^=left]>.arrow::after,.bs-popover-left>.arrow::after{right:1px;border-width:.5rem 0 .5rem .5rem;border-left-color:#fff}.popover-header{padding:.5rem .75rem;margin-bottom:0;font-size:1rem;color:#495057;background-color:#f7f7f7;border-bottom:1px solid #ebebeb;border-top-left-radius:calc(.5rem - 1px);border-top-right-radius:calc(.5rem - 1px)}.popover-header:empty{display:none}.popover-body{padding:.5rem .75rem;color:#495057}.carousel{position:relative}.carousel.pointer-event{touch-action:pan-y}.carousel-inner{position:relative;width:100%;overflow:hidden}.carousel-inner::after{display:block;clear:both;content:""}.carousel-item{position:relative;display:none;float:left;width:100%;margin-right:-100%;backface-visibility:hidden;transition:transform .6s ease-in-out}@media (prefers-reduced-motion:reduce){.carousel-item{transition:none}}.carousel-item-next,.carousel-item-prev,.carousel-item.active{display:block}.active.carousel-item-right,.carousel-item-next:not(.carousel-item-left){transform:translateX(100%)}.active.carousel-item-left,.carousel-item-prev:not(.carousel-item-right){transform:translateX(-100%)}.carousel-fade .carousel-item{opacity:0;transition-property:opacity;transform:none}.carousel-fade .carousel-item-next.carousel-item-left,.carousel-fade .carousel-item-prev.carousel-item-right,.carousel-fade .carousel-item.active{z-index:1;opacity:1}.carousel-fade .active.carousel-item-left,.carousel-fade .active.carousel-item-right{z-index:0;opacity:0;transition:opacity 0s .6s}@media (prefers-reduced-motion:reduce){.carousel-fade .active.carousel-item-left,.carousel-fade .active.carousel-item-right{transition:none}}.carousel-control-next,.carousel-control-prev{position:absolute;top:0;bottom:0;z-index:1;display:flex;align-items:center;justify-content:center;width:15%;color:#fff;text-align:center;opacity:.5;transition:opacity .15s ease}@media (prefers-reduced-motion:reduce){.carousel-control-next,.carousel-control-prev{transition:none}}.carousel-control-next:focus,.carousel-control-next:hover,.carousel-control-prev:focus,.carousel-control-prev:hover{color:#fff;text-decoration:none;outline:0;opacity:.9}.carousel-control-prev{left:0}.carousel-control-next{right:0}.carousel-control-next-icon,.carousel-control-prev-icon{display:inline-block;width:20px;height:20px;background:no-repeat 50%/100% 100%}.carousel-control-prev-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='%23ffffff' width='8' height='8' viewBox='0 0 8 8'%3e%3cpath d='M5.25 0l-4 4 4 4 1.5-1.5L4.25 4l2.5-2.5L5.25 0z'/%3e%3c/svg%3e")}.carousel-control-next-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='%23ffffff' width='8' height='8' viewBox='0 0 8 8'%3e%3cpath d='M2.75 0l-1.5 1.5L3.75 4l-2.5 2.5L2.75 8l4-4-4-4z'/%3e%3c/svg%3e")}.carousel-indicators{position:absolute;right:0;bottom:0;left:0;z-index:15;display:flex;justify-content:center;padding-left:0;margin-right:15%;margin-left:15%;list-style:none}.carousel-indicators li{box-sizing:content-box;flex:0 1 auto;width:30px;height:3px;margin-right:3px;margin-left:3px;text-indent:-999px;cursor:pointer;background-color:#fff;background-clip:padding-box;border-top:10px solid transparent;border-bottom:10px solid transparent;opacity:.5;transition:opacity .6s ease}@media (prefers-reduced-motion:reduce){.carousel-indicators li{transition:none}}.carousel-indicators .active{opacity:1}.carousel-caption{position:absolute;right:15%;bottom:20px;left:15%;z-index:10;padding-top:20px;padding-bottom:20px;color:#fff;text-align:center}@keyframes spinner-border{to{transform:rotate(360deg)}}.spinner-border{display:inline-block;width:2rem;height:2rem;vertical-align:text-bottom;border:.25em solid currentColor;border-right-color:transparent;border-radius:50%;animation:spinner-border .75s linear infinite}.spinner-border-sm{width:1rem;height:1rem;border-width:.2em}@keyframes spinner-grow{0%{transform:scale(0)}50%{opacity:1;transform:none}}.spinner-grow{display:inline-block;width:2rem;height:2rem;vertical-align:text-bottom;background-color:currentColor;border-radius:50%;opacity:0;animation:spinner-grow .75s linear infinite}.spinner-grow-sm{width:1rem;height:1rem}.align-baseline{vertical-align:baseline!important}.align-top{vertical-align:top!important}.align-middle{vertical-align:middle!important}.align-bottom{vertical-align:bottom!important}.align-text-bottom{vertical-align:text-bottom!important}.align-text-top{vertical-align:text-top!important}.bg-primary{background-color:#f1641e!important}a.bg-primary:focus,a.bg-primary:hover,button.bg-primary:focus,button.bg-primary:hover{background-color:#cf4d0d!important}.bg-secondary{background-color:#00c853!important}a.bg-secondary:focus,a.bg-secondary:hover,button.bg-secondary:focus,button.bg-secondary:hover{background-color:#00953e!important}.bg-success{background-color:#6610f2!important}a.bg-success:focus,a.bg-success:hover,button.bg-success:focus,button.bg-success:hover{background-color:#510bc4!important}.bg-info{background-color:#007bff!important}a.bg-info:focus,a.bg-info:hover,button.bg-info:focus,button.bg-info:hover{background-color:#0062cc!important}.bg-warning{background-color:#ffc107!important}a.bg-warning:focus,a.bg-warning:hover,button.bg-warning:focus,button.bg-warning:hover{background-color:#d39e00!important}.bg-danger{background-color:#873208!important}a.bg-danger:focus,a.bg-danger:hover,button.bg-danger:focus,button.bg-danger:hover{background-color:#572105!important}.bg-light{background-color:#f8f9fa!important}a.bg-light:focus,a.bg-light:hover,button.bg-light:focus,button.bg-light:hover{background-color:#dae0e5!important}.bg-dark{background-color:#343a40!important}a.bg-dark:focus,a.bg-dark:hover,button.bg-dark:focus,button.bg-dark:hover{background-color:#1d2124!important}.bg-white{background-color:#fff!important}.bg-transparent{background-color:transparent!important}.border{border:1px solid #495057!important}.border-top{border-top:1px solid #495057!important}.border-right{border-right:1px solid #495057!important}.border-bottom{border-bottom:1px solid #495057!important}.border-left{border-left:1px solid #495057!important}.border-0{border:0!important}.border-top-0{border-top:0!important}.border-right-0{border-right:0!important}.border-bottom-0{border-bottom:0!important}.border-left-0{border-left:0!important}.border-primary{border-color:#f1641e!important}.border-secondary{border-color:#00c853!important}.border-success{border-color:#6610f2!important}.border-info{border-color:#007bff!important}.border-warning{border-color:#ffc107!important}.border-danger{border-color:#873208!important}.border-light{border-color:#f8f9fa!important}.border-dark{border-color:#343a40!important}.border-white{border-color:#fff!important}.rounded-sm{border-radius:1rem!important}.rounded{border-radius:.5rem!important}.rounded-top{border-top-left-radius:.5rem!important;border-top-right-radius:.5rem!important}.rounded-right{border-top-right-radius:.5rem!important;border-bottom-right-radius:.5rem!important}.rounded-bottom{border-bottom-right-radius:.5rem!important;border-bottom-left-radius:.5rem!important}.rounded-left{border-top-left-radius:.5rem!important;border-bottom-left-radius:.5rem!important}.rounded-lg{border-radius:.5rem!important}.rounded-circle{border-radius:50%!important}.rounded-pill{border-radius:.25rem!important}.rounded-0{border-radius:0!important}.clearfix::after{display:block;clear:both;content:""}.d-none{display:none!important}.d-inline{display:inline!important}.d-inline-block{display:inline-block!important}.d-block{display:block!important}.d-table{display:table!important}.d-table-row{display:table-row!important}.d-table-cell{display:table-cell!important}.d-flex{display:flex!important}.d-inline-flex{display:inline-flex!important}@media (min-width:576px){.d-sm-none{display:none!important}.d-sm-inline{display:inline!important}.d-sm-inline-block{display:inline-block!important}.d-sm-block{display:block!important}.d-sm-table{display:table!important}.d-sm-table-row{display:table-row!important}.d-sm-table-cell{display:table-cell!important}.d-sm-flex{display:flex!important}.d-sm-inline-flex{display:inline-flex!important}}@media (min-width:768px){.d-md-none{display:none!important}.d-md-inline{display:inline!important}.d-md-inline-block{display:inline-block!important}.d-md-block{display:block!important}.d-md-table{display:table!important}.d-md-table-row{display:table-row!important}.d-md-table-cell{display:table-cell!important}.d-md-flex{display:flex!important}.d-md-inline-flex{display:inline-flex!important}}@media (min-width:992px){.d-lg-none{display:none!important}.d-lg-inline{display:inline!important}.d-lg-inline-block{display:inline-block!important}.d-lg-block{display:block!important}.d-lg-table{display:table!important}.d-lg-table-row{display:table-row!important}.d-lg-table-cell{display:table-cell!important}.d-lg-flex{display:flex!important}.d-lg-inline-flex{display:inline-flex!important}}@media (min-width:1200px){.d-xl-none{display:none!important}.d-xl-inline{display:inline!important}.d-xl-inline-block{display:inline-block!important}.d-xl-block{display:block!important}.d-xl-table{display:table!important}.d-xl-table-row{display:table-row!important}.d-xl-table-cell{display:table-cell!important}.d-xl-flex{display:flex!important}.d-xl-inline-flex{display:inline-flex!important}}@media print{.d-print-none{display:none!important}.d-print-inline{display:inline!important}.d-print-inline-block{display:inline-block!important}.d-print-block{display:block!important}.d-print-table{display:table!important}.d-print-table-row{display:table-row!important}.d-print-table-cell{display:table-cell!important}.d-print-flex{display:flex!important}.d-print-inline-flex{display:inline-flex!important}}.embed-responsive{position:relative;display:block;width:100%;padding:0;overflow:hidden}.embed-responsive::before{display:block;content:""}.embed-responsive .embed-responsive-item,.embed-responsive embed,.embed-responsive iframe,.embed-responsive object,.embed-responsive video{position:absolute;top:0;bottom:0;left:0;width:100%;height:100%;border:0}.embed-responsive-21by9::before{padding-top:42.85714%}.embed-responsive-16by9::before{padding-top:56.25%}.embed-responsive-4by3::before{padding-top:75%}.embed-responsive-1by1::before{padding-top:100%}.flex-row{flex-direction:row!important}.flex-column{flex-direction:column!important}.flex-row-reverse{flex-direction:row-reverse!important}.flex-column-reverse{flex-direction:column-reverse!important}.flex-wrap{flex-wrap:wrap!important}.flex-nowrap{flex-wrap:nowrap!important}.flex-wrap-reverse{flex-wrap:wrap-reverse!important}.flex-fill{flex:1 1 auto!important}.flex-grow-0{flex-grow:0!important}.flex-grow-1{flex-grow:1!important}.flex-shrink-0{flex-shrink:0!important}.flex-shrink-1{flex-shrink:1!important}.justify-content-start{justify-content:flex-start!important}.justify-content-end{justify-content:flex-end!important}.justify-content-center{justify-content:center!important}.justify-content-between{justify-content:space-between!important}.justify-content-around{justify-content:space-around!important}.align-items-start{align-items:flex-start!important}.align-items-end{align-items:flex-end!important}.align-items-center{align-items:center!important}.align-items-baseline{align-items:baseline!important}.align-items-stretch{align-items:stretch!important}.align-content-start{align-content:flex-start!important}.align-content-end{align-content:flex-end!important}.align-content-center{align-content:center!important}.align-content-between{align-content:space-between!important}.align-content-around{align-content:space-around!important}.align-content-stretch{align-content:stretch!important}.align-self-auto{align-self:auto!important}.align-self-start{align-self:flex-start!important}.align-self-end{align-self:flex-end!important}.align-self-center{align-self:center!important}.align-self-baseline{align-self:baseline!important}.align-self-stretch{align-self:stretch!important}@media (min-width:576px){.flex-sm-row{flex-direction:row!important}.flex-sm-column{flex-direction:column!important}.flex-sm-row-reverse{flex-direction:row-reverse!important}.flex-sm-column-reverse{flex-direction:column-reverse!important}.flex-sm-wrap{flex-wrap:wrap!important}.flex-sm-nowrap{flex-wrap:nowrap!important}.flex-sm-wrap-reverse{flex-wrap:wrap-reverse!important}.flex-sm-fill{flex:1 1 auto!important}.flex-sm-grow-0{flex-grow:0!important}.flex-sm-grow-1{flex-grow:1!important}.flex-sm-shrink-0{flex-shrink:0!important}.flex-sm-shrink-1{flex-shrink:1!important}.justify-content-sm-start{justify-content:flex-start!important}.justify-content-sm-end{justify-content:flex-end!important}.justify-content-sm-center{justify-content:center!important}.justify-content-sm-between{justify-content:space-between!important}.justify-content-sm-around{justify-content:space-around!important}.align-items-sm-start{align-items:flex-start!important}.align-items-sm-end{align-items:flex-end!important}.align-items-sm-center{align-items:center!important}.align-items-sm-baseline{align-items:baseline!important}.align-items-sm-stretch{align-items:stretch!important}.align-content-sm-start{align-content:flex-start!important}.align-content-sm-end{align-content:flex-end!important}.align-content-sm-center{align-content:center!important}.align-content-sm-between{align-content:space-between!important}.align-content-sm-around{align-content:space-around!important}.align-content-sm-stretch{align-content:stretch!important}.align-self-sm-auto{align-self:auto!important}.align-self-sm-start{align-self:flex-start!important}.align-self-sm-end{align-self:flex-end!important}.align-self-sm-center{align-self:center!important}.align-self-sm-baseline{align-self:baseline!important}.align-self-sm-stretch{align-self:stretch!important}}@media (min-width:768px){.flex-md-row{flex-direction:row!important}.flex-md-column{flex-direction:column!important}.flex-md-row-reverse{flex-direction:row-reverse!important}.flex-md-column-reverse{flex-direction:column-reverse!important}.flex-md-wrap{flex-wrap:wrap!important}.flex-md-nowrap{flex-wrap:nowrap!important}.flex-md-wrap-reverse{flex-wrap:wrap-reverse!important}.flex-md-fill{flex:1 1 auto!important}.flex-md-grow-0{flex-grow:0!important}.flex-md-grow-1{flex-grow:1!important}.flex-md-shrink-0{flex-shrink:0!important}.flex-md-shrink-1{flex-shrink:1!important}.justify-content-md-start{justify-content:flex-start!important}.justify-content-md-end{justify-content:flex-end!important}.justify-content-md-center{justify-content:center!important}.justify-content-md-between{justify-content:space-between!important}.justify-content-md-around{justify-content:space-around!important}.align-items-md-start{align-items:flex-start!important}.align-items-md-end{align-items:flex-end!important}.align-items-md-center{align-items:center!important}.align-items-md-baseline{align-items:baseline!important}.align-items-md-stretch{align-items:stretch!important}.align-content-md-start{align-content:flex-start!important}.align-content-md-end{align-content:flex-end!important}.align-content-md-center{align-content:center!important}.align-content-md-between{align-content:space-between!important}.align-content-md-around{align-content:space-around!important}.align-content-md-stretch{align-content:stretch!important}.align-self-md-auto{align-self:auto!important}.align-self-md-start{align-self:flex-start!important}.align-self-md-end{align-self:flex-end!important}.align-self-md-center{align-self:center!important}.align-self-md-baseline{align-self:baseline!important}.align-self-md-stretch{align-self:stretch!important}}@media (min-width:992px){.flex-lg-row{flex-direction:row!important}.flex-lg-column{flex-direction:column!important}.flex-lg-row-reverse{flex-direction:row-reverse!important}.flex-lg-column-reverse{flex-direction:column-reverse!important}.flex-lg-wrap{flex-wrap:wrap!important}.flex-lg-nowrap{flex-wrap:nowrap!important}.flex-lg-wrap-reverse{flex-wrap:wrap-reverse!important}.flex-lg-fill{flex:1 1 auto!important}.flex-lg-grow-0{flex-grow:0!important}.flex-lg-grow-1{flex-grow:1!important}.flex-lg-shrink-0{flex-shrink:0!important}.flex-lg-shrink-1{flex-shrink:1!important}.justify-content-lg-start{justify-content:flex-start!important}.justify-content-lg-end{justify-content:flex-end!important}.justify-content-lg-center{justify-content:center!important}.justify-content-lg-between{justify-content:space-between!important}.justify-content-lg-around{justify-content:space-around!important}.align-items-lg-start{align-items:flex-start!important}.align-items-lg-end{align-items:flex-end!important}.align-items-lg-center{align-items:center!important}.align-items-lg-baseline{align-items:baseline!important}.align-items-lg-stretch{align-items:stretch!important}.align-content-lg-start{align-content:flex-start!important}.align-content-lg-end{align-content:flex-end!important}.align-content-lg-center{align-content:center!important}.align-content-lg-between{align-content:space-between!important}.align-content-lg-around{align-content:space-around!important}.align-content-lg-stretch{align-content:stretch!important}.align-self-lg-auto{align-self:auto!important}.align-self-lg-start{align-self:flex-start!important}.align-self-lg-end{align-self:flex-end!important}.align-self-lg-center{align-self:center!important}.align-self-lg-baseline{align-self:baseline!important}.align-self-lg-stretch{align-self:stretch!important}}@media (min-width:1200px){.flex-xl-row{flex-direction:row!important}.flex-xl-column{flex-direction:column!important}.flex-xl-row-reverse{flex-direction:row-reverse!important}.flex-xl-column-reverse{flex-direction:column-reverse!important}.flex-xl-wrap{flex-wrap:wrap!important}.flex-xl-nowrap{flex-wrap:nowrap!important}.flex-xl-wrap-reverse{flex-wrap:wrap-reverse!important}.flex-xl-fill{flex:1 1 auto!important}.flex-xl-grow-0{flex-grow:0!important}.flex-xl-grow-1{flex-grow:1!important}.flex-xl-shrink-0{flex-shrink:0!important}.flex-xl-shrink-1{flex-shrink:1!important}.justify-content-xl-start{justify-content:flex-start!important}.justify-content-xl-end{justify-content:flex-end!important}.justify-content-xl-center{justify-content:center!important}.justify-content-xl-between{justify-content:space-between!important}.justify-content-xl-around{justify-content:space-around!important}.align-items-xl-start{align-items:flex-start!important}.align-items-xl-end{align-items:flex-end!important}.align-items-xl-center{align-items:center!important}.align-items-xl-baseline{align-items:baseline!important}.align-items-xl-stretch{align-items:stretch!important}.align-content-xl-start{align-content:flex-start!important}.align-content-xl-end{align-content:flex-end!important}.align-content-xl-center{align-content:center!important}.align-content-xl-between{align-content:space-between!important}.align-content-xl-around{align-content:space-around!important}.align-content-xl-stretch{align-content:stretch!important}.align-self-xl-auto{align-self:auto!important}.align-self-xl-start{align-self:flex-start!important}.align-self-xl-end{align-self:flex-end!important}.align-self-xl-center{align-self:center!important}.align-self-xl-baseline{align-self:baseline!important}.align-self-xl-stretch{align-self:stretch!important}}.float-left{float:left!important}.float-right{float:right!important}.float-none{float:none!important}@media (min-width:576px){.float-sm-left{float:left!important}.float-sm-right{float:right!important}.float-sm-none{float:none!important}}@media (min-width:768px){.float-md-left{float:left!important}.float-md-right{float:right!important}.float-md-none{float:none!important}}@media (min-width:992px){.float-lg-left{float:left!important}.float-lg-right{float:right!important}.float-lg-none{float:none!important}}@media (min-width:1200px){.float-xl-left{float:left!important}.float-xl-right{float:right!important}.float-xl-none{float:none!important}}.user-select-all{user-select:all!important}.user-select-auto{user-select:auto!important}.user-select-none{user-select:none!important}.overflow-auto{overflow:auto!important}.overflow-hidden{overflow:hidden!important}.position-static{position:static!important}.position-relative{position:relative!important}.position-absolute{position:absolute!important}.position-fixed{position:fixed!important}.position-sticky{position:sticky!important}.fixed-top{position:fixed;top:0;right:0;left:0;z-index:1030}.fixed-bottom{position:fixed;right:0;bottom:0;left:0;z-index:1030}@supports (position:sticky){.sticky-top{position:sticky;top:0;z-index:1020}}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;overflow:visible;clip:auto;white-space:normal}.shadow-sm{box-shadow:0 .125rem .25rem rgba(34,34,34,.075)!important}.shadow{box-shadow:0 .5rem 1rem rgba(34,34,34,.15)!important}.shadow-lg{box-shadow:0 1rem 3rem rgba(34,34,34,.175)!important}.shadow-none{box-shadow:none!important}.w-25{width:25%!important}.w-50{width:50%!important}.w-75{width:75%!important}.w-100{width:100%!important}.w-auto{width:auto!important}.h-25{height:25%!important}.h-50{height:50%!important}.h-75{height:75%!important}.h-100{height:100%!important}.h-auto{height:auto!important}.mw-100{max-width:100%!important}.mh-100{max-height:100%!important}.min-vw-100{min-width:100vw!important}.min-vh-100{min-height:100vh!important}.vw-100{width:100vw!important}.vh-100{height:100vh!important}.m-0{margin:0!important}.mt-0,.my-0{margin-top:0!important}.mr-0,.mx-0{margin-right:0!important}.mb-0,.my-0{margin-bottom:0!important}.ml-0,.mx-0{margin-left:0!important}.m-1{margin:.25rem!important}.mt-1,.my-1{margin-top:.25rem!important}.mr-1,.mx-1{margin-right:.25rem!important}.mb-1,.my-1{margin-bottom:.25rem!important}.ml-1,.mx-1{margin-left:.25rem!important}.m-2{margin:.5rem!important}.mt-2,.my-2{margin-top:.5rem!important}.mr-2,.mx-2{margin-right:.5rem!important}.mb-2,.my-2{margin-bottom:.5rem!important}.ml-2,.mx-2{margin-left:.5rem!important}.m-3{margin:1rem!important}.mt-3,.my-3{margin-top:1rem!important}.mr-3,.mx-3{margin-right:1rem!important}.mb-3,.my-3{margin-bottom:1rem!important}.ml-3,.mx-3{margin-left:1rem!important}.m-4{margin:1.5rem!important}.mt-4,.my-4{margin-top:1.5rem!important}.mr-4,.mx-4{margin-right:1.5rem!important}.mb-4,.my-4{margin-bottom:1.5rem!important}.ml-4,.mx-4{margin-left:1.5rem!important}.m-5{margin:3rem!important}.mt-5,.my-5{margin-top:3rem!important}.mr-5,.mx-5{margin-right:3rem!important}.mb-5,.my-5{margin-bottom:3rem!important}.ml-5,.mx-5{margin-left:3rem!important}.p-0{padding:0!important}.pt-0,.py-0{padding-top:0!important}.pr-0,.px-0{padding-right:0!important}.pb-0,.py-0{padding-bottom:0!important}.pl-0,.px-0{padding-left:0!important}.p-1{padding:.25rem!important}.pt-1,.py-1{padding-top:.25rem!important}.pr-1,.px-1{padding-right:.25rem!important}.pb-1,.py-1{padding-bottom:.25rem!important}.pl-1,.px-1{padding-left:.25rem!important}.p-2{padding:.5rem!important}.pt-2,.py-2{padding-top:.5rem!important}.pr-2,.px-2{padding-right:.5rem!important}.pb-2,.py-2{padding-bottom:.5rem!important}.pl-2,.px-2{padding-left:.5rem!important}.p-3{padding:1rem!important}.pt-3,.py-3{padding-top:1rem!important}.pr-3,.px-3{padding-right:1rem!important}.pb-3,.py-3{padding-bottom:1rem!important}.pl-3,.px-3{padding-left:1rem!important}.p-4{padding:1.5rem!important}.pt-4,.py-4{padding-top:1.5rem!important}.pr-4,.px-4{padding-right:1.5rem!important}.pb-4,.py-4{padding-bottom:1.5rem!important}.pl-4,.px-4{padding-left:1.5rem!important}.p-5{padding:3rem!important}.pt-5,.py-5{padding-top:3rem!important}.pr-5,.px-5{padding-right:3rem!important}.pb-5,.py-5{padding-bottom:3rem!important}.pl-5,.px-5{padding-left:3rem!important}.m-n1{margin:-.25rem!important}.mt-n1,.my-n1{margin-top:-.25rem!important}.mr-n1,.mx-n1{margin-right:-.25rem!important}.mb-n1,.my-n1{margin-bottom:-.25rem!important}.ml-n1,.mx-n1{margin-left:-.25rem!important}.m-n2{margin:-.5rem!important}.mt-n2,.my-n2{margin-top:-.5rem!important}.mr-n2,.mx-n2{margin-right:-.5rem!important}.mb-n2,.my-n2{margin-bottom:-.5rem!important}.ml-n2,.mx-n2{margin-left:-.5rem!important}.m-n3{margin:-1rem!important}.mt-n3,.my-n3{margin-top:-1rem!important}.mr-n3,.mx-n3{margin-right:-1rem!important}.mb-n3,.my-n3{margin-bottom:-1rem!important}.ml-n3,.mx-n3{margin-left:-1rem!important}.m-n4{margin:-1.5rem!important}.mt-n4,.my-n4{margin-top:-1.5rem!important}.mr-n4,.mx-n4{margin-right:-1.5rem!important}.mb-n4,.my-n4{margin-bottom:-1.5rem!important}.ml-n4,.mx-n4{margin-left:-1.5rem!important}.m-n5{margin:-3rem!important}.mt-n5,.my-n5{margin-top:-3rem!important}.mr-n5,.mx-n5{margin-right:-3rem!important}.mb-n5,.my-n5{margin-bottom:-3rem!important}.ml-n5,.mx-n5{margin-left:-3rem!important}.m-auto{margin:auto!important}.mt-auto,.my-auto{margin-top:auto!important}.mr-auto,.mx-auto{margin-right:auto!important}.mb-auto,.my-auto{margin-bottom:auto!important}.ml-auto,.mx-auto{margin-left:auto!important}@media (min-width:576px){.m-sm-0{margin:0!important}.mt-sm-0,.my-sm-0{margin-top:0!important}.mr-sm-0,.mx-sm-0{margin-right:0!important}.mb-sm-0,.my-sm-0{margin-bottom:0!important}.ml-sm-0,.mx-sm-0{margin-left:0!important}.m-sm-1{margin:.25rem!important}.mt-sm-1,.my-sm-1{margin-top:.25rem!important}.mr-sm-1,.mx-sm-1{margin-right:.25rem!important}.mb-sm-1,.my-sm-1{margin-bottom:.25rem!important}.ml-sm-1,.mx-sm-1{margin-left:.25rem!important}.m-sm-2{margin:.5rem!important}.mt-sm-2,.my-sm-2{margin-top:.5rem!important}.mr-sm-2,.mx-sm-2{margin-right:.5rem!important}.mb-sm-2,.my-sm-2{margin-bottom:.5rem!important}.ml-sm-2,.mx-sm-2{margin-left:.5rem!important}.m-sm-3{margin:1rem!important}.mt-sm-3,.my-sm-3{margin-top:1rem!important}.mr-sm-3,.mx-sm-3{margin-right:1rem!important}.mb-sm-3,.my-sm-3{margin-bottom:1rem!important}.ml-sm-3,.mx-sm-3{margin-left:1rem!important}.m-sm-4{margin:1.5rem!important}.mt-sm-4,.my-sm-4{margin-top:1.5rem!important}.mr-sm-4,.mx-sm-4{margin-right:1.5rem!important}.mb-sm-4,.my-sm-4{margin-bottom:1.5rem!important}.ml-sm-4,.mx-sm-4{margin-left:1.5rem!important}.m-sm-5{margin:3rem!important}.mt-sm-5,.my-sm-5{margin-top:3rem!important}.mr-sm-5,.mx-sm-5{margin-right:3rem!important}.mb-sm-5,.my-sm-5{margin-bottom:3rem!important}.ml-sm-5,.mx-sm-5{margin-left:3rem!important}.p-sm-0{padding:0!important}.pt-sm-0,.py-sm-0{padding-top:0!important}.pr-sm-0,.px-sm-0{padding-right:0!important}.pb-sm-0,.py-sm-0{padding-bottom:0!important}.pl-sm-0,.px-sm-0{padding-left:0!important}.p-sm-1{padding:.25rem!important}.pt-sm-1,.py-sm-1{padding-top:.25rem!important}.pr-sm-1,.px-sm-1{padding-right:.25rem!important}.pb-sm-1,.py-sm-1{padding-bottom:.25rem!important}.pl-sm-1,.px-sm-1{padding-left:.25rem!important}.p-sm-2{padding:.5rem!important}.pt-sm-2,.py-sm-2{padding-top:.5rem!important}.pr-sm-2,.px-sm-2{padding-right:.5rem!important}.pb-sm-2,.py-sm-2{padding-bottom:.5rem!important}.pl-sm-2,.px-sm-2{padding-left:.5rem!important}.p-sm-3{padding:1rem!important}.pt-sm-3,.py-sm-3{padding-top:1rem!important}.pr-sm-3,.px-sm-3{padding-right:1rem!important}.pb-sm-3,.py-sm-3{padding-bottom:1rem!important}.pl-sm-3,.px-sm-3{padding-left:1rem!important}.p-sm-4{padding:1.5rem!important}.pt-sm-4,.py-sm-4{padding-top:1.5rem!important}.pr-sm-4,.px-sm-4{padding-right:1.5rem!important}.pb-sm-4,.py-sm-4{padding-bottom:1.5rem!important}.pl-sm-4,.px-sm-4{padding-left:1.5rem!important}.p-sm-5{padding:3rem!important}.pt-sm-5,.py-sm-5{padding-top:3rem!important}.pr-sm-5,.px-sm-5{padding-right:3rem!important}.pb-sm-5,.py-sm-5{padding-bottom:3rem!important}.pl-sm-5,.px-sm-5{padding-left:3rem!important}.m-sm-n1{margin:-.25rem!important}.mt-sm-n1,.my-sm-n1{margin-top:-.25rem!important}.mr-sm-n1,.mx-sm-n1{margin-right:-.25rem!important}.mb-sm-n1,.my-sm-n1{margin-bottom:-.25rem!important}.ml-sm-n1,.mx-sm-n1{margin-left:-.25rem!important}.m-sm-n2{margin:-.5rem!important}.mt-sm-n2,.my-sm-n2{margin-top:-.5rem!important}.mr-sm-n2,.mx-sm-n2{margin-right:-.5rem!important}.mb-sm-n2,.my-sm-n2{margin-bottom:-.5rem!important}.ml-sm-n2,.mx-sm-n2{margin-left:-.5rem!important}.m-sm-n3{margin:-1rem!important}.mt-sm-n3,.my-sm-n3{margin-top:-1rem!important}.mr-sm-n3,.mx-sm-n3{margin-right:-1rem!important}.mb-sm-n3,.my-sm-n3{margin-bottom:-1rem!important}.ml-sm-n3,.mx-sm-n3{margin-left:-1rem!important}.m-sm-n4{margin:-1.5rem!important}.mt-sm-n4,.my-sm-n4{margin-top:-1.5rem!important}.mr-sm-n4,.mx-sm-n4{margin-right:-1.5rem!important}.mb-sm-n4,.my-sm-n4{margin-bottom:-1.5rem!important}.ml-sm-n4,.mx-sm-n4{margin-left:-1.5rem!important}.m-sm-n5{margin:-3rem!important}.mt-sm-n5,.my-sm-n5{margin-top:-3rem!important}.mr-sm-n5,.mx-sm-n5{margin-right:-3rem!important}.mb-sm-n5,.my-sm-n5{margin-bottom:-3rem!important}.ml-sm-n5,.mx-sm-n5{margin-left:-3rem!important}.m-sm-auto{margin:auto!important}.mt-sm-auto,.my-sm-auto{margin-top:auto!important}.mr-sm-auto,.mx-sm-auto{margin-right:auto!important}.mb-sm-auto,.my-sm-auto{margin-bottom:auto!important}.ml-sm-auto,.mx-sm-auto{margin-left:auto!important}}@media (min-width:768px){.m-md-0{margin:0!important}.mt-md-0,.my-md-0{margin-top:0!important}.mr-md-0,.mx-md-0{margin-right:0!important}.mb-md-0,.my-md-0{margin-bottom:0!important}.ml-md-0,.mx-md-0{margin-left:0!important}.m-md-1{margin:.25rem!important}.mt-md-1,.my-md-1{margin-top:.25rem!important}.mr-md-1,.mx-md-1{margin-right:.25rem!important}.mb-md-1,.my-md-1{margin-bottom:.25rem!important}.ml-md-1,.mx-md-1{margin-left:.25rem!important}.m-md-2{margin:.5rem!important}.mt-md-2,.my-md-2{margin-top:.5rem!important}.mr-md-2,.mx-md-2{margin-right:.5rem!important}.mb-md-2,.my-md-2{margin-bottom:.5rem!important}.ml-md-2,.mx-md-2{margin-left:.5rem!important}.m-md-3{margin:1rem!important}.mt-md-3,.my-md-3{margin-top:1rem!important}.mr-md-3,.mx-md-3{margin-right:1rem!important}.mb-md-3,.my-md-3{margin-bottom:1rem!important}.ml-md-3,.mx-md-3{margin-left:1rem!important}.m-md-4{margin:1.5rem!important}.mt-md-4,.my-md-4{margin-top:1.5rem!important}.mr-md-4,.mx-md-4{margin-right:1.5rem!important}.mb-md-4,.my-md-4{margin-bottom:1.5rem!important}.ml-md-4,.mx-md-4{margin-left:1.5rem!important}.m-md-5{margin:3rem!important}.mt-md-5,.my-md-5{margin-top:3rem!important}.mr-md-5,.mx-md-5{margin-right:3rem!important}.mb-md-5,.my-md-5{margin-bottom:3rem!important}.ml-md-5,.mx-md-5{margin-left:3rem!important}.p-md-0{padding:0!important}.pt-md-0,.py-md-0{padding-top:0!important}.pr-md-0,.px-md-0{padding-right:0!important}.pb-md-0,.py-md-0{padding-bottom:0!important}.pl-md-0,.px-md-0{padding-left:0!important}.p-md-1{padding:.25rem!important}.pt-md-1,.py-md-1{padding-top:.25rem!important}.pr-md-1,.px-md-1{padding-right:.25rem!important}.pb-md-1,.py-md-1{padding-bottom:.25rem!important}.pl-md-1,.px-md-1{padding-left:.25rem!important}.p-md-2{padding:.5rem!important}.pt-md-2,.py-md-2{padding-top:.5rem!important}.pr-md-2,.px-md-2{padding-right:.5rem!important}.pb-md-2,.py-md-2{padding-bottom:.5rem!important}.pl-md-2,.px-md-2{padding-left:.5rem!important}.p-md-3{padding:1rem!important}.pt-md-3,.py-md-3{padding-top:1rem!important}.pr-md-3,.px-md-3{padding-right:1rem!important}.pb-md-3,.py-md-3{padding-bottom:1rem!important}.pl-md-3,.px-md-3{padding-left:1rem!important}.p-md-4{padding:1.5rem!important}.pt-md-4,.py-md-4{padding-top:1.5rem!important}.pr-md-4,.px-md-4{padding-right:1.5rem!important}.pb-md-4,.py-md-4{padding-bottom:1.5rem!important}.pl-md-4,.px-md-4{padding-left:1.5rem!important}.p-md-5{padding:3rem!important}.pt-md-5,.py-md-5{padding-top:3rem!important}.pr-md-5,.px-md-5{padding-right:3rem!important}.pb-md-5,.py-md-5{padding-bottom:3rem!important}.pl-md-5,.px-md-5{padding-left:3rem!important}.m-md-n1{margin:-.25rem!important}.mt-md-n1,.my-md-n1{margin-top:-.25rem!important}.mr-md-n1,.mx-md-n1{margin-right:-.25rem!important}.mb-md-n1,.my-md-n1{margin-bottom:-.25rem!important}.ml-md-n1,.mx-md-n1{margin-left:-.25rem!important}.m-md-n2{margin:-.5rem!important}.mt-md-n2,.my-md-n2{margin-top:-.5rem!important}.mr-md-n2,.mx-md-n2{margin-right:-.5rem!important}.mb-md-n2,.my-md-n2{margin-bottom:-.5rem!important}.ml-md-n2,.mx-md-n2{margin-left:-.5rem!important}.m-md-n3{margin:-1rem!important}.mt-md-n3,.my-md-n3{margin-top:-1rem!important}.mr-md-n3,.mx-md-n3{margin-right:-1rem!important}.mb-md-n3,.my-md-n3{margin-bottom:-1rem!important}.ml-md-n3,.mx-md-n3{margin-left:-1rem!important}.m-md-n4{margin:-1.5rem!important}.mt-md-n4,.my-md-n4{margin-top:-1.5rem!important}.mr-md-n4,.mx-md-n4{margin-right:-1.5rem!important}.mb-md-n4,.my-md-n4{margin-bottom:-1.5rem!important}.ml-md-n4,.mx-md-n4{margin-left:-1.5rem!important}.m-md-n5{margin:-3rem!important}.mt-md-n5,.my-md-n5{margin-top:-3rem!important}.mr-md-n5,.mx-md-n5{margin-right:-3rem!important}.mb-md-n5,.my-md-n5{margin-bottom:-3rem!important}.ml-md-n5,.mx-md-n5{margin-left:-3rem!important}.m-md-auto{margin:auto!important}.mt-md-auto,.my-md-auto{margin-top:auto!important}.mr-md-auto,.mx-md-auto{margin-right:auto!important}.mb-md-auto,.my-md-auto{margin-bottom:auto!important}.ml-md-auto,.mx-md-auto{margin-left:auto!important}}@media (min-width:992px){.m-lg-0{margin:0!important}.mt-lg-0,.my-lg-0{margin-top:0!important}.mr-lg-0,.mx-lg-0{margin-right:0!important}.mb-lg-0,.my-lg-0{margin-bottom:0!important}.ml-lg-0,.mx-lg-0{margin-left:0!important}.m-lg-1{margin:.25rem!important}.mt-lg-1,.my-lg-1{margin-top:.25rem!important}.mr-lg-1,.mx-lg-1{margin-right:.25rem!important}.mb-lg-1,.my-lg-1{margin-bottom:.25rem!important}.ml-lg-1,.mx-lg-1{margin-left:.25rem!important}.m-lg-2{margin:.5rem!important}.mt-lg-2,.my-lg-2{margin-top:.5rem!important}.mr-lg-2,.mx-lg-2{margin-right:.5rem!important}.mb-lg-2,.my-lg-2{margin-bottom:.5rem!important}.ml-lg-2,.mx-lg-2{margin-left:.5rem!important}.m-lg-3{margin:1rem!important}.mt-lg-3,.my-lg-3{margin-top:1rem!important}.mr-lg-3,.mx-lg-3{margin-right:1rem!important}.mb-lg-3,.my-lg-3{margin-bottom:1rem!important}.ml-lg-3,.mx-lg-3{margin-left:1rem!important}.m-lg-4{margin:1.5rem!important}.mt-lg-4,.my-lg-4{margin-top:1.5rem!important}.mr-lg-4,.mx-lg-4{margin-right:1.5rem!important}.mb-lg-4,.my-lg-4{margin-bottom:1.5rem!important}.ml-lg-4,.mx-lg-4{margin-left:1.5rem!important}.m-lg-5{margin:3rem!important}.mt-lg-5,.my-lg-5{margin-top:3rem!important}.mr-lg-5,.mx-lg-5{margin-right:3rem!important}.mb-lg-5,.my-lg-5{margin-bottom:3rem!important}.ml-lg-5,.mx-lg-5{margin-left:3rem!important}.p-lg-0{padding:0!important}.pt-lg-0,.py-lg-0{padding-top:0!important}.pr-lg-0,.px-lg-0{padding-right:0!important}.pb-lg-0,.py-lg-0{padding-bottom:0!important}.pl-lg-0,.px-lg-0{padding-left:0!important}.p-lg-1{padding:.25rem!important}.pt-lg-1,.py-lg-1{padding-top:.25rem!important}.pr-lg-1,.px-lg-1{padding-right:.25rem!important}.pb-lg-1,.py-lg-1{padding-bottom:.25rem!important}.pl-lg-1,.px-lg-1{padding-left:.25rem!important}.p-lg-2{padding:.5rem!important}.pt-lg-2,.py-lg-2{padding-top:.5rem!important}.pr-lg-2,.px-lg-2{padding-right:.5rem!important}.pb-lg-2,.py-lg-2{padding-bottom:.5rem!important}.pl-lg-2,.px-lg-2{padding-left:.5rem!important}.p-lg-3{padding:1rem!important}.pt-lg-3,.py-lg-3{padding-top:1rem!important}.pr-lg-3,.px-lg-3{padding-right:1rem!important}.pb-lg-3,.py-lg-3{padding-bottom:1rem!important}.pl-lg-3,.px-lg-3{padding-left:1rem!important}.p-lg-4{padding:1.5rem!important}.pt-lg-4,.py-lg-4{padding-top:1.5rem!important}.pr-lg-4,.px-lg-4{padding-right:1.5rem!important}.pb-lg-4,.py-lg-4{padding-bottom:1.5rem!important}.pl-lg-4,.px-lg-4{padding-left:1.5rem!important}.p-lg-5{padding:3rem!important}.pt-lg-5,.py-lg-5{padding-top:3rem!important}.pr-lg-5,.px-lg-5{padding-right:3rem!important}.pb-lg-5,.py-lg-5{padding-bottom:3rem!important}.pl-lg-5,.px-lg-5{padding-left:3rem!important}.m-lg-n1{margin:-.25rem!important}.mt-lg-n1,.my-lg-n1{margin-top:-.25rem!important}.mr-lg-n1,.mx-lg-n1{margin-right:-.25rem!important}.mb-lg-n1,.my-lg-n1{margin-bottom:-.25rem!important}.ml-lg-n1,.mx-lg-n1{margin-left:-.25rem!important}.m-lg-n2{margin:-.5rem!important}.mt-lg-n2,.my-lg-n2{margin-top:-.5rem!important}.mr-lg-n2,.mx-lg-n2{margin-right:-.5rem!important}.mb-lg-n2,.my-lg-n2{margin-bottom:-.5rem!important}.ml-lg-n2,.mx-lg-n2{margin-left:-.5rem!important}.m-lg-n3{margin:-1rem!important}.mt-lg-n3,.my-lg-n3{margin-top:-1rem!important}.mr-lg-n3,.mx-lg-n3{margin-right:-1rem!important}.mb-lg-n3,.my-lg-n3{margin-bottom:-1rem!important}.ml-lg-n3,.mx-lg-n3{margin-left:-1rem!important}.m-lg-n4{margin:-1.5rem!important}.mt-lg-n4,.my-lg-n4{margin-top:-1.5rem!important}.mr-lg-n4,.mx-lg-n4{margin-right:-1.5rem!important}.mb-lg-n4,.my-lg-n4{margin-bottom:-1.5rem!important}.ml-lg-n4,.mx-lg-n4{margin-left:-1.5rem!important}.m-lg-n5{margin:-3rem!important}.mt-lg-n5,.my-lg-n5{margin-top:-3rem!important}.mr-lg-n5,.mx-lg-n5{margin-right:-3rem!important}.mb-lg-n5,.my-lg-n5{margin-bottom:-3rem!important}.ml-lg-n5,.mx-lg-n5{margin-left:-3rem!important}.m-lg-auto{margin:auto!important}.mt-lg-auto,.my-lg-auto{margin-top:auto!important}.mr-lg-auto,.mx-lg-auto{margin-right:auto!important}.mb-lg-auto,.my-lg-auto{margin-bottom:auto!important}.ml-lg-auto,.mx-lg-auto{margin-left:auto!important}}@media (min-width:1200px){.m-xl-0{margin:0!important}.mt-xl-0,.my-xl-0{margin-top:0!important}.mr-xl-0,.mx-xl-0{margin-right:0!important}.mb-xl-0,.my-xl-0{margin-bottom:0!important}.ml-xl-0,.mx-xl-0{margin-left:0!important}.m-xl-1{margin:.25rem!important}.mt-xl-1,.my-xl-1{margin-top:.25rem!important}.mr-xl-1,.mx-xl-1{margin-right:.25rem!important}.mb-xl-1,.my-xl-1{margin-bottom:.25rem!important}.ml-xl-1,.mx-xl-1{margin-left:.25rem!important}.m-xl-2{margin:.5rem!important}.mt-xl-2,.my-xl-2{margin-top:.5rem!important}.mr-xl-2,.mx-xl-2{margin-right:.5rem!important}.mb-xl-2,.my-xl-2{margin-bottom:.5rem!important}.ml-xl-2,.mx-xl-2{margin-left:.5rem!important}.m-xl-3{margin:1rem!important}.mt-xl-3,.my-xl-3{margin-top:1rem!important}.mr-xl-3,.mx-xl-3{margin-right:1rem!important}.mb-xl-3,.my-xl-3{margin-bottom:1rem!important}.ml-xl-3,.mx-xl-3{margin-left:1rem!important}.m-xl-4{margin:1.5rem!important}.mt-xl-4,.my-xl-4{margin-top:1.5rem!important}.mr-xl-4,.mx-xl-4{margin-right:1.5rem!important}.mb-xl-4,.my-xl-4{margin-bottom:1.5rem!important}.ml-xl-4,.mx-xl-4{margin-left:1.5rem!important}.m-xl-5{margin:3rem!important}.mt-xl-5,.my-xl-5{margin-top:3rem!important}.mr-xl-5,.mx-xl-5{margin-right:3rem!important}.mb-xl-5,.my-xl-5{margin-bottom:3rem!important}.ml-xl-5,.mx-xl-5{margin-left:3rem!important}.p-xl-0{padding:0!important}.pt-xl-0,.py-xl-0{padding-top:0!important}.pr-xl-0,.px-xl-0{padding-right:0!important}.pb-xl-0,.py-xl-0{padding-bottom:0!important}.pl-xl-0,.px-xl-0{padding-left:0!important}.p-xl-1{padding:.25rem!important}.pt-xl-1,.py-xl-1{padding-top:.25rem!important}.pr-xl-1,.px-xl-1{padding-right:.25rem!important}.pb-xl-1,.py-xl-1{padding-bottom:.25rem!important}.pl-xl-1,.px-xl-1{padding-left:.25rem!important}.p-xl-2{padding:.5rem!important}.pt-xl-2,.py-xl-2{padding-top:.5rem!important}.pr-xl-2,.px-xl-2{padding-right:.5rem!important}.pb-xl-2,.py-xl-2{padding-bottom:.5rem!important}.pl-xl-2,.px-xl-2{padding-left:.5rem!important}.p-xl-3{padding:1rem!important}.pt-xl-3,.py-xl-3{padding-top:1rem!important}.pr-xl-3,.px-xl-3{padding-right:1rem!important}.pb-xl-3,.py-xl-3{padding-bottom:1rem!important}.pl-xl-3,.px-xl-3{padding-left:1rem!important}.p-xl-4{padding:1.5rem!important}.pt-xl-4,.py-xl-4{padding-top:1.5rem!important}.pr-xl-4,.px-xl-4{padding-right:1.5rem!important}.pb-xl-4,.py-xl-4{padding-bottom:1.5rem!important}.pl-xl-4,.px-xl-4{padding-left:1.5rem!important}.p-xl-5{padding:3rem!important}.pt-xl-5,.py-xl-5{padding-top:3rem!important}.pr-xl-5,.px-xl-5{padding-right:3rem!important}.pb-xl-5,.py-xl-5{padding-bottom:3rem!important}.pl-xl-5,.px-xl-5{padding-left:3rem!important}.m-xl-n1{margin:-.25rem!important}.mt-xl-n1,.my-xl-n1{margin-top:-.25rem!important}.mr-xl-n1,.mx-xl-n1{margin-right:-.25rem!important}.mb-xl-n1,.my-xl-n1{margin-bottom:-.25rem!important}.ml-xl-n1,.mx-xl-n1{margin-left:-.25rem!important}.m-xl-n2{margin:-.5rem!important}.mt-xl-n2,.my-xl-n2{margin-top:-.5rem!important}.mr-xl-n2,.mx-xl-n2{margin-right:-.5rem!important}.mb-xl-n2,.my-xl-n2{margin-bottom:-.5rem!important}.ml-xl-n2,.mx-xl-n2{margin-left:-.5rem!important}.m-xl-n3{margin:-1rem!important}.mt-xl-n3,.my-xl-n3{margin-top:-1rem!important}.mr-xl-n3,.mx-xl-n3{margin-right:-1rem!important}.mb-xl-n3,.my-xl-n3{margin-bottom:-1rem!important}.ml-xl-n3,.mx-xl-n3{margin-left:-1rem!important}.m-xl-n4{margin:-1.5rem!important}.mt-xl-n4,.my-xl-n4{margin-top:-1.5rem!important}.mr-xl-n4,.mx-xl-n4{margin-right:-1.5rem!important}.mb-xl-n4,.my-xl-n4{margin-bottom:-1.5rem!important}.ml-xl-n4,.mx-xl-n4{margin-left:-1.5rem!important}.m-xl-n5{margin:-3rem!important}.mt-xl-n5,.my-xl-n5{margin-top:-3rem!important}.mr-xl-n5,.mx-xl-n5{margin-right:-3rem!important}.mb-xl-n5,.my-xl-n5{margin-bottom:-3rem!important}.ml-xl-n5,.mx-xl-n5{margin-left:-3rem!important}.m-xl-auto{margin:auto!important}.mt-xl-auto,.my-xl-auto{margin-top:auto!important}.mr-xl-auto,.mx-xl-auto{margin-right:auto!important}.mb-xl-auto,.my-xl-auto{margin-bottom:auto!important}.ml-xl-auto,.mx-xl-auto{margin-left:auto!important}}.stretched-link::after{position:absolute;top:0;right:0;bottom:0;left:0;z-index:1;pointer-events:auto;content:"";background-color:rgba(0,0,0,0)}.text-monospace{font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace!important}.text-justify{text-align:justify!important}.text-wrap{white-space:normal!important}.text-nowrap{white-space:nowrap!important}.text-truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.text-left{text-align:left!important}.text-right{text-align:right!important}.text-center{text-align:center!important}@media (min-width:576px){.text-sm-left{text-align:left!important}.text-sm-right{text-align:right!important}.text-sm-center{text-align:center!important}}@media (min-width:768px){.text-md-left{text-align:left!important}.text-md-right{text-align:right!important}.text-md-center{text-align:center!important}}@media (min-width:992px){.text-lg-left{text-align:left!important}.text-lg-right{text-align:right!important}.text-lg-center{text-align:center!important}}@media (min-width:1200px){.text-xl-left{text-align:left!important}.text-xl-right{text-align:right!important}.text-xl-center{text-align:center!important}}.text-lowercase{text-transform:lowercase!important}.text-uppercase{text-transform:uppercase!important}.text-capitalize{text-transform:capitalize!important}.font-weight-light{font-weight:300!important}.font-weight-lighter{font-weight:lighter!important}.font-weight-normal{font-weight:400!important}.font-weight-bold{font-weight:600!important}.font-weight-bolder{font-weight:bolder!important}.font-italic{font-style:italic!important}.text-white{color:#fff!important}.text-primary{color:#f1641e!important}a.text-primary:focus,a.text-primary:hover{color:#b7440b!important}.text-secondary{color:#00c853!important}a.text-secondary:focus,a.text-secondary:hover{color:#007c33!important}.text-success{color:#6610f2!important}a.text-success:focus,a.text-success:hover{color:#4709ac!important}.text-info{color:#007bff!important}a.text-info:focus,a.text-info:hover{color:#0056b3!important}.text-warning{color:#ffc107!important}a.text-warning:focus,a.text-warning:hover{color:#ba8b00!important}.text-danger{color:#873208!important}a.text-danger:focus,a.text-danger:hover{color:#3f1804!important}.text-light{color:#f8f9fa!important}a.text-light:focus,a.text-light:hover{color:#cbd3da!important}.text-dark{color:#343a40!important}a.text-dark:focus,a.text-dark:hover{color:#121416!important}.text-body{color:#495057!important}.text-muted{color:#6c757d!important}.text-black-50{color:rgba(34,34,34,.5)!important}.text-white-50{color:rgba(255,255,255,.5)!important}.text-hide{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.text-decoration-none{text-decoration:none!important}.text-break{word-wrap:break-word!important}.text-reset{color:inherit!important}.visible{visibility:visible!important}.invisible{visibility:hidden!important}@media print{*,::after,::before{text-shadow:none!important;box-shadow:none!important}a:not(.btn){text-decoration:underline}abbr[title]::after{content:" (" attr(title) ")"}pre{white-space:pre-wrap!important}blockquote,pre{border:1px solid #adb5bd;page-break-inside:avoid}thead{display:table-header-group}img,tr{page-break-inside:avoid}h2,h3,p{orphans:3;widows:3}h2,h3{page-break-after:avoid}@page{size:a3}body{min-width:992px!important}.container{min-width:992px!important}.navbar{display:none}.badge{border:1px solid #222}.table{border-collapse:collapse!important}.table td,.table th{background-color:#fff!important}.table-bordered td,.table-bordered th{border:1px solid #dee2e6!important}.table-dark{color:inherit}.table-dark tbody+tbody,.table-dark td,.table-dark th,.table-dark thead th{border-color:#495057}.table .thead-dark th{color:inherit;border-color:#495057}}
index 1bd07e86378a1eeb5d6ee54d36f500ded3d8c412..f50f061f1789d5145eae060886e2d1da393b76e4 100644 (file)
@@ -6,7 +6,7 @@
   "license": "AGPL-3.0-or-later",
   "main": "index.js",
   "scripts": {
-    "api-test": "jest src/api_tests/api.spec.ts",
+    "api-test": "jest src/api_tests/ -i --verbose",
     "build": "node fuse prod",
     "lint": "tsc --noEmit && eslint --report-unused-disable-directives --ext .js,.ts,.tsx src",
     "prebuild": "node generate_translations.js",
   "keywords": [],
   "dependencies": {
     "@types/autosize": "^3.0.6",
+    "@types/jest": "^26.0.7",
     "@types/js-cookie": "^2.2.6",
     "@types/jwt-decode": "^2.2.1",
-    "@types/markdown-it": "^0.0.9",
+    "@types/markdown-it": "^10.0.1",
     "@types/markdown-it-container": "^2.0.2",
-    "@types/node": "^13.11.1",
+    "@types/node": "^14.0.26",
+    "@types/node-fetch": "^2.5.6",
     "autosize": "^4.0.2",
     "bootswatch": "^4.3.1",
     "choices.js": "^9.0.1",
     "husky": "^4.2.5",
     "i18next": "^19.4.1",
     "inferno": "^7.4.2",
-    "inferno-i18next": "nimbusec-oss/inferno-i18next",
+    "inferno-helmet": "^5.2.1",
+    "inferno-i18next": "github:nimbusec-oss/inferno-i18next#semver:^7.4.2",
     "inferno-router": "^7.4.2",
     "js-cookie": "^2.2.0",
     "jwt-decode": "^2.2.0",
-    "markdown-it": "^10.0.0",
-    "markdown-it-container": "^2.0.0",
+    "lemmy-js-client": "^1.0.8",
+    "markdown-it": "^11.0.0",
+    "markdown-it-container": "^3.0.0",
     "markdown-it-emoji": "^1.4.0",
+    "markdown-it-sub": "^1.0.0",
+    "markdown-it-sup": "^1.0.0",
     "moment": "^2.24.0",
     "node-fetch": "^2.6.0",
     "prettier": "^2.0.4",
     "reconnecting-websocket": "^4.4.0",
+    "register-service-worker": "^1.7.1",
     "rxjs": "^6.5.5",
     "terser": "^4.6.11",
     "tippy.js": "^6.1.1",
     "ws": "^7.2.3"
   },
   "devDependencies": {
-    "@types/jest": "^25.2.1",
-    "@types/node-fetch": "^2.5.6",
-    "eslint": "^6.5.1",
+    "eslint": "^7.5.0",
     "eslint-plugin-inferno": "^7.14.3",
-    "eslint-plugin-jane": "^7.2.1",
+    "eslint-plugin-jane": "^8.0.4",
     "fuse-box": "^3.1.3",
-    "jest": "^25.4.0",
+    "jest": "^26.0.7",
     "lint-staged": "^10.1.3",
     "sortpack": "^2.1.4",
-    "ts-jest": "^25.4.0",
+    "ts-jest": "^26.1.3",
     "ts-node": "^8.8.2",
     "ts-transform-classcat": "^1.0.0",
     "ts-transform-inferno": "^4.0.3",
@@ -70,7 +75,7 @@
   "engineStrict": true,
   "husky": {
     "hooks": {
-      "pre-commit": "cargo clippy --manifest-path ../server/Cargo.toml --all-targets --workspace -- -D warnings && lint-staged"
+      "pre-commit": "cargo +nightly clippy --manifest-path ../server/Cargo.toml --all-targets --workspace -- -D warnings && lint-staged"
     }
   },
   "lint-staged": {
@@ -79,7 +84,7 @@
       "eslint --fix"
     ],
     "../server/src/**/*.rs": [
-      "rustfmt --config-path ../server/.rustfmt.toml"
+      "rustfmt +nightly --config-path ../server/.rustfmt.toml"
     ],
     "package.json": [
       "sortpack"
diff --git a/ui/src/api_tests/api.spec.ts b/ui/src/api_tests/api.spec.ts
deleted file mode 100644 (file)
index 41710e1..0000000
+++ /dev/null
@@ -1,1487 +0,0 @@
-import fetch from 'node-fetch';
-
-import {
-  LoginForm,
-  LoginResponse,
-  PostForm,
-  PostResponse,
-  SearchResponse,
-  FollowCommunityForm,
-  CommunityResponse,
-  GetFollowedCommunitiesResponse,
-  GetPostForm,
-  GetPostResponse,
-  CommentForm,
-  CommentResponse,
-  CommunityForm,
-  GetCommunityForm,
-  GetCommunityResponse,
-  CommentLikeForm,
-  CreatePostLikeForm,
-  PrivateMessageForm,
-  EditPrivateMessageForm,
-  PrivateMessageResponse,
-  PrivateMessagesResponse,
-  GetUserMentionsResponse,
-} from '../interfaces';
-
-let lemmyAlphaUrl = 'http://localhost:8540';
-let lemmyAlphaApiUrl = `${lemmyAlphaUrl}/api/v1`;
-let lemmyAlphaAuth: string;
-
-let lemmyBetaUrl = 'http://localhost:8550';
-let lemmyBetaApiUrl = `${lemmyBetaUrl}/api/v1`;
-let lemmyBetaAuth: string;
-
-let lemmyGammaUrl = 'http://localhost:8560';
-let lemmyGammaApiUrl = `${lemmyGammaUrl}/api/v1`;
-let lemmyGammaAuth: string;
-
-// Workaround for tests being run before beforeAll() is finished
-// https://github.com/facebook/jest/issues/9527#issuecomment-592406108
-describe('main', () => {
-  beforeAll(async () => {
-    console.log('Logging in as lemmy_alpha');
-    let form: LoginForm = {
-      username_or_email: 'lemmy_alpha',
-      password: 'lemmy',
-    };
-
-    let res: LoginResponse = await fetch(`${lemmyAlphaApiUrl}/user/login`, {
-      method: 'POST',
-      headers: {
-        'Content-Type': 'application/json',
-      },
-      body: wrapper(form),
-    }).then(d => d.json());
-
-    lemmyAlphaAuth = res.jwt;
-
-    console.log('Logging in as lemmy_beta');
-    let formB = {
-      username_or_email: 'lemmy_beta',
-      password: 'lemmy',
-    };
-
-    let resB: LoginResponse = await fetch(`${lemmyBetaApiUrl}/user/login`, {
-      method: 'POST',
-      headers: {
-        'Content-Type': 'application/json',
-      },
-      body: wrapper(formB),
-    }).then(d => d.json());
-
-    lemmyBetaAuth = resB.jwt;
-
-    console.log('Logging in as lemmy_gamma');
-    let formC = {
-      username_or_email: 'lemmy_gamma',
-      password: 'lemmy',
-    };
-
-    let resG: LoginResponse = await fetch(`${lemmyGammaApiUrl}/user/login`, {
-      method: 'POST',
-      headers: {
-        'Content-Type': 'application/json',
-      },
-      body: wrapper(formC),
-    }).then(d => d.json());
-
-    lemmyGammaAuth = resG.jwt;
-  });
-
-  describe('post_search', () => {
-    test('Create test post on alpha and fetch it on beta', async () => {
-      let name = 'A jest test post';
-      let postForm: PostForm = {
-        name,
-        auth: lemmyAlphaAuth,
-        community_id: 2,
-        creator_id: 2,
-        nsfw: false,
-      };
-
-      let createPostRes: PostResponse = await fetch(
-        `${lemmyAlphaApiUrl}/post`,
-        {
-          method: 'POST',
-          headers: {
-            'Content-Type': 'application/json',
-          },
-          body: wrapper(postForm),
-        }
-      ).then(d => d.json());
-      expect(createPostRes.post.name).toBe(name);
-
-      let searchUrl = `${lemmyBetaApiUrl}/search?q=${createPostRes.post.ap_id}&type_=All&sort=TopAll`;
-      let searchResponse: SearchResponse = await fetch(searchUrl, {
-        method: 'GET',
-      }).then(d => d.json());
-
-      // TODO: check more fields
-      expect(searchResponse.posts[0].name).toBe(name);
-    });
-  });
-
-  describe('follow_accept', () => {
-    test('/u/lemmy_alpha follows and accepts lemmy-beta/c/main', async () => {
-      // Make sure lemmy-beta/c/main is cached on lemmy_alpha
-      // Use short-hand search url
-      let searchUrl = `${lemmyAlphaApiUrl}/search?q=!main@lemmy-beta:8550&type_=All&sort=TopAll`;
-
-      let searchResponse: SearchResponse = await fetch(searchUrl, {
-        method: 'GET',
-      }).then(d => d.json());
-
-      expect(searchResponse.communities[0].name).toBe('main');
-
-      let followForm: FollowCommunityForm = {
-        community_id: searchResponse.communities[0].id,
-        follow: true,
-        auth: lemmyAlphaAuth,
-      };
-
-      let followRes: CommunityResponse = await fetch(
-        `${lemmyAlphaApiUrl}/community/follow`,
-        {
-          method: 'POST',
-          headers: {
-            'Content-Type': 'application/json',
-          },
-          body: wrapper(followForm),
-        }
-      ).then(d => d.json());
-
-      // Make sure the follow response went through
-      expect(followRes.community.local).toBe(false);
-      expect(followRes.community.name).toBe('main');
-
-      // Check that you are subscribed to it locally
-      let followedCommunitiesUrl = `${lemmyAlphaApiUrl}/user/followed_communities?&auth=${lemmyAlphaAuth}`;
-      let followedCommunitiesRes: GetFollowedCommunitiesResponse = await fetch(
-        followedCommunitiesUrl,
-        {
-          method: 'GET',
-        }
-      ).then(d => d.json());
-
-      expect(followedCommunitiesRes.communities[1].community_local).toBe(false);
-
-      // Test out unfollowing
-      let unfollowForm: FollowCommunityForm = {
-        community_id: searchResponse.communities[0].id,
-        follow: false,
-        auth: lemmyAlphaAuth,
-      };
-
-      let unfollowRes: CommunityResponse = await fetch(
-        `${lemmyAlphaApiUrl}/community/follow`,
-        {
-          method: 'POST',
-          headers: {
-            'Content-Type': 'application/json',
-          },
-          body: wrapper(unfollowForm),
-        }
-      ).then(d => d.json());
-      expect(unfollowRes.community.local).toBe(false);
-
-      // Check that you are unsubscribed to it locally
-      let followedCommunitiesResAgain: GetFollowedCommunitiesResponse = await fetch(
-        followedCommunitiesUrl,
-        {
-          method: 'GET',
-        }
-      ).then(d => d.json());
-
-      expect(followedCommunitiesResAgain.communities.length).toBe(1);
-
-      // Follow again, for other tests
-      let followResAgain: CommunityResponse = await fetch(
-        `${lemmyAlphaApiUrl}/community/follow`,
-        {
-          method: 'POST',
-          headers: {
-            'Content-Type': 'application/json',
-          },
-          body: wrapper(followForm),
-        }
-      ).then(d => d.json());
-
-      // Make sure the follow response went through
-      expect(followResAgain.community.local).toBe(false);
-      expect(followResAgain.community.name).toBe('main');
-
-      // Also make G follow B
-
-      // Use short-hand search url
-      let searchUrlG = `${lemmyGammaApiUrl}/search?q=!main@lemmy-beta:8550&type_=All&sort=TopAll`;
-
-      let searchResponseG: SearchResponse = await fetch(searchUrlG, {
-        method: 'GET',
-      }).then(d => d.json());
-
-      expect(searchResponseG.communities[0].name).toBe('main');
-
-      let followFormG: FollowCommunityForm = {
-        community_id: searchResponseG.communities[0].id,
-        follow: true,
-        auth: lemmyGammaAuth,
-      };
-
-      let followResG: CommunityResponse = await fetch(
-        `${lemmyGammaApiUrl}/community/follow`,
-        {
-          method: 'POST',
-          headers: {
-            'Content-Type': 'application/json',
-          },
-          body: wrapper(followFormG),
-        }
-      ).then(d => d.json());
-
-      // Make sure the follow response went through
-      expect(followResG.community.local).toBe(false);
-      expect(followResG.community.name).toBe('main');
-
-      // Check that you are subscribed to it locally
-      let followedCommunitiesUrlG = `${lemmyGammaApiUrl}/user/followed_communities?&auth=${lemmyGammaAuth}`;
-      let followedCommunitiesResG: GetFollowedCommunitiesResponse = await fetch(
-        followedCommunitiesUrlG,
-        {
-          method: 'GET',
-        }
-      ).then(d => d.json());
-
-      expect(followedCommunitiesResG.communities[1].community_local).toBe(
-        false
-      );
-    });
-  });
-
-  describe('create test post', () => {
-    test('/u/lemmy_alpha creates a post on /c/lemmy_beta/main, its on both instances', async () => {
-      let name = 'A jest test federated post';
-      let postForm: PostForm = {
-        name,
-        auth: lemmyAlphaAuth,
-        community_id: 3,
-        creator_id: 2,
-        nsfw: false,
-      };
-
-      let createResponse: PostResponse = await fetch(
-        `${lemmyAlphaApiUrl}/post`,
-        {
-          method: 'POST',
-          headers: {
-            'Content-Type': 'application/json',
-          },
-          body: wrapper(postForm),
-        }
-      ).then(d => d.json());
-
-      let unlikePostForm: CreatePostLikeForm = {
-        post_id: createResponse.post.id,
-        score: 0,
-        auth: lemmyAlphaAuth,
-      };
-      expect(createResponse.post.name).toBe(name);
-      expect(createResponse.post.community_local).toBe(false);
-      expect(createResponse.post.creator_local).toBe(true);
-      expect(createResponse.post.score).toBe(1);
-
-      let unlikePostRes: PostResponse = await fetch(
-        `${lemmyAlphaApiUrl}/post/like`,
-        {
-          method: 'POST',
-          headers: {
-            'Content-Type': 'application/json',
-          },
-          body: wrapper(unlikePostForm),
-        }
-      ).then(d => d.json());
-      expect(unlikePostRes.post.score).toBe(0);
-
-      let getPostUrl = `${lemmyBetaApiUrl}/post?id=2`;
-      let getPostRes: GetPostResponse = await fetch(getPostUrl, {
-        method: 'GET',
-      }).then(d => d.json());
-
-      expect(getPostRes.post.name).toBe(name);
-      expect(getPostRes.post.community_local).toBe(true);
-      expect(getPostRes.post.creator_local).toBe(false);
-      expect(getPostRes.post.score).toBe(0);
-    });
-  });
-
-  describe('update test post', () => {
-    test('/u/lemmy_alpha updates a post on /c/lemmy_beta/main, the update is on both', async () => {
-      let name = 'A jest test federated post, updated';
-      let postForm: PostForm = {
-        name,
-        edit_id: 2,
-        auth: lemmyAlphaAuth,
-        community_id: 3,
-        creator_id: 2,
-        nsfw: false,
-      };
-
-      let updateResponse: PostResponse = await fetch(
-        `${lemmyAlphaApiUrl}/post`,
-        {
-          method: 'PUT',
-          headers: {
-            'Content-Type': 'application/json',
-          },
-          body: wrapper(postForm),
-        }
-      ).then(d => d.json());
-
-      expect(updateResponse.post.name).toBe(name);
-      expect(updateResponse.post.community_local).toBe(false);
-      expect(updateResponse.post.creator_local).toBe(true);
-
-      let getPostUrl = `${lemmyBetaApiUrl}/post?id=2`;
-      let getPostRes: GetPostResponse = await fetch(getPostUrl, {
-        method: 'GET',
-      }).then(d => d.json());
-
-      expect(getPostRes.post.name).toBe(name);
-      expect(getPostRes.post.community_local).toBe(true);
-      expect(getPostRes.post.creator_local).toBe(false);
-    });
-  });
-
-  describe('create test comment', () => {
-    test('/u/lemmy_alpha creates a comment on /c/lemmy_beta/main, its on both instances', async () => {
-      let content = 'A jest test federated comment';
-      let commentForm: CommentForm = {
-        content,
-        post_id: 2,
-        auth: lemmyAlphaAuth,
-      };
-
-      let createResponse: CommentResponse = await fetch(
-        `${lemmyAlphaApiUrl}/comment`,
-        {
-          method: 'POST',
-          headers: {
-            'Content-Type': 'application/json',
-          },
-          body: wrapper(commentForm),
-        }
-      ).then(d => d.json());
-
-      expect(createResponse.comment.content).toBe(content);
-      expect(createResponse.comment.community_local).toBe(false);
-      expect(createResponse.comment.creator_local).toBe(true);
-      expect(createResponse.comment.score).toBe(1);
-
-      // Do an unlike, to test it
-      let unlikeCommentForm: CommentLikeForm = {
-        comment_id: createResponse.comment.id,
-        score: 0,
-        post_id: 2,
-        auth: lemmyAlphaAuth,
-      };
-
-      let unlikeCommentRes: CommentResponse = await fetch(
-        `${lemmyAlphaApiUrl}/comment/like`,
-        {
-          method: 'POST',
-          headers: {
-            'Content-Type': 'application/json',
-          },
-          body: wrapper(unlikeCommentForm),
-        }
-      ).then(d => d.json());
-
-      expect(unlikeCommentRes.comment.score).toBe(0);
-
-      let getPostUrl = `${lemmyBetaApiUrl}/post?id=2`;
-      let getPostRes: GetPostResponse = await fetch(getPostUrl, {
-        method: 'GET',
-      }).then(d => d.json());
-
-      expect(getPostRes.comments[0].content).toBe(content);
-      expect(getPostRes.comments[0].community_local).toBe(true);
-      expect(getPostRes.comments[0].creator_local).toBe(false);
-      expect(getPostRes.comments[0].score).toBe(0);
-
-      // Now do beta replying to that comment, as a child comment
-      let contentBeta = 'A child federated comment from beta';
-      let commentFormBeta: CommentForm = {
-        content: contentBeta,
-        post_id: getPostRes.post.id,
-        parent_id: getPostRes.comments[0].id,
-        auth: lemmyBetaAuth,
-      };
-
-      let createResponseBeta: CommentResponse = await fetch(
-        `${lemmyBetaApiUrl}/comment`,
-        {
-          method: 'POST',
-          headers: {
-            'Content-Type': 'application/json',
-          },
-          body: wrapper(commentFormBeta),
-        }
-      ).then(d => d.json());
-
-      expect(createResponseBeta.comment.content).toBe(contentBeta);
-      expect(createResponseBeta.comment.community_local).toBe(true);
-      expect(createResponseBeta.comment.creator_local).toBe(true);
-      expect(createResponseBeta.comment.parent_id).toBe(1);
-      expect(createResponseBeta.comment.score).toBe(1);
-
-      // Make sure lemmy alpha sees that new child comment from beta
-      let getPostUrlAlpha = `${lemmyAlphaApiUrl}/post?id=2`;
-      let getPostResAlpha: GetPostResponse = await fetch(getPostUrlAlpha, {
-        method: 'GET',
-      }).then(d => d.json());
-
-      // The newest show up first
-      expect(getPostResAlpha.comments[0].content).toBe(contentBeta);
-      expect(getPostResAlpha.comments[0].community_local).toBe(false);
-      expect(getPostResAlpha.comments[0].creator_local).toBe(false);
-      expect(getPostResAlpha.comments[0].score).toBe(1);
-
-      // Lemmy alpha responds to their own comment, but mentions lemmy beta.
-      // Make sure lemmy beta gets that in their inbox.
-      let mentionContent = 'A test mention of @lemmy_beta@lemmy-beta:8550';
-      let mentionCommentForm: CommentForm = {
-        content: mentionContent,
-        post_id: 2,
-        parent_id: createResponse.comment.id,
-        auth: lemmyAlphaAuth,
-      };
-
-      let createMentionRes: CommentResponse = await fetch(
-        `${lemmyAlphaApiUrl}/comment`,
-        {
-          method: 'POST',
-          headers: {
-            'Content-Type': 'application/json',
-          },
-          body: wrapper(mentionCommentForm),
-        }
-      ).then(d => d.json());
-
-      expect(createMentionRes.comment.content).toBe(mentionContent);
-      expect(createMentionRes.comment.community_local).toBe(false);
-      expect(createMentionRes.comment.creator_local).toBe(true);
-      expect(createMentionRes.comment.score).toBe(1);
-
-      // Make sure lemmy beta sees that new mention
-      let getMentionUrl = `${lemmyBetaApiUrl}/user/mention?sort=New&unread_only=false&auth=${lemmyBetaAuth}`;
-      let getMentionsRes: GetUserMentionsResponse = await fetch(getMentionUrl, {
-        method: 'GET',
-      }).then(d => d.json());
-
-      // The newest show up first
-      expect(getMentionsRes.mentions[0].content).toBe(mentionContent);
-      expect(getMentionsRes.mentions[0].community_local).toBe(true);
-      expect(getMentionsRes.mentions[0].creator_local).toBe(false);
-      expect(getMentionsRes.mentions[0].score).toBe(1);
-    });
-  });
-
-  describe('update test comment', () => {
-    test('/u/lemmy_alpha updates a comment on /c/lemmy_beta/main, its on both instances', async () => {
-      let content = 'A jest test federated comment update';
-      let commentForm: CommentForm = {
-        content,
-        post_id: 2,
-        edit_id: 1,
-        auth: lemmyAlphaAuth,
-        creator_id: 2,
-      };
-
-      let updateResponse: CommentResponse = await fetch(
-        `${lemmyAlphaApiUrl}/comment`,
-        {
-          method: 'PUT',
-          headers: {
-            'Content-Type': 'application/json',
-          },
-          body: wrapper(commentForm),
-        }
-      ).then(d => d.json());
-
-      expect(updateResponse.comment.content).toBe(content);
-      expect(updateResponse.comment.community_local).toBe(false);
-      expect(updateResponse.comment.creator_local).toBe(true);
-
-      let getPostUrl = `${lemmyBetaApiUrl}/post?id=2`;
-      let getPostRes: GetPostResponse = await fetch(getPostUrl, {
-        method: 'GET',
-      }).then(d => d.json());
-
-      expect(getPostRes.comments[2].content).toBe(content);
-      expect(getPostRes.comments[2].community_local).toBe(true);
-      expect(getPostRes.comments[2].creator_local).toBe(false);
-    });
-  });
-
-  describe('delete things', () => {
-    test('/u/lemmy_beta deletes and undeletes a federated comment, post, and community, lemmy_alpha sees its deleted.', async () => {
-      // Create a test community
-      let communityName = 'test_community';
-      let communityForm: CommunityForm = {
-        name: communityName,
-        title: communityName,
-        category_id: 1,
-        nsfw: false,
-        auth: lemmyBetaAuth,
-      };
-
-      let createCommunityRes: CommunityResponse = await fetch(
-        `${lemmyBetaApiUrl}/community`,
-        {
-          method: 'POST',
-          headers: {
-            'Content-Type': 'application/json',
-          },
-          body: wrapper(communityForm),
-        }
-      ).then(d => d.json());
-
-      expect(createCommunityRes.community.name).toBe(communityName);
-
-      // Cache it on lemmy_alpha
-      let searchUrl = `${lemmyAlphaApiUrl}/search?q=http://lemmy-beta:8550/c/${communityName}&type_=All&sort=TopAll`;
-      let searchResponse: SearchResponse = await fetch(searchUrl, {
-        method: 'GET',
-      }).then(d => d.json());
-
-      let communityOnAlphaId = searchResponse.communities[0].id;
-
-      // Follow it
-      let followForm: FollowCommunityForm = {
-        community_id: communityOnAlphaId,
-        follow: true,
-        auth: lemmyAlphaAuth,
-      };
-
-      let followRes: CommunityResponse = await fetch(
-        `${lemmyAlphaApiUrl}/community/follow`,
-        {
-          method: 'POST',
-          headers: {
-            'Content-Type': 'application/json',
-          },
-          body: wrapper(followForm),
-        }
-      ).then(d => d.json());
-
-      // Make sure the follow response went through
-      expect(followRes.community.local).toBe(false);
-      expect(followRes.community.name).toBe(communityName);
-
-      // Lemmy beta creates a test post
-      let postName = 'A jest test post with delete';
-      let createPostForm: PostForm = {
-        name: postName,
-        auth: lemmyBetaAuth,
-        community_id: createCommunityRes.community.id,
-        creator_id: 2,
-        nsfw: false,
-      };
-
-      let createPostRes: PostResponse = await fetch(`${lemmyBetaApiUrl}/post`, {
-        method: 'POST',
-        headers: {
-          'Content-Type': 'application/json',
-        },
-        body: wrapper(createPostForm),
-      }).then(d => d.json());
-      expect(createPostRes.post.name).toBe(postName);
-
-      // Lemmy beta creates a test comment
-      let commentContent = 'A jest test federated comment with delete';
-      let createCommentForm: CommentForm = {
-        content: commentContent,
-        post_id: createPostRes.post.id,
-        auth: lemmyBetaAuth,
-      };
-
-      let createCommentRes: CommentResponse = await fetch(
-        `${lemmyBetaApiUrl}/comment`,
-        {
-          method: 'POST',
-          headers: {
-            'Content-Type': 'application/json',
-          },
-          body: wrapper(createCommentForm),
-        }
-      ).then(d => d.json());
-
-      expect(createCommentRes.comment.content).toBe(commentContent);
-
-      // lemmy_beta deletes the comment
-      let deleteCommentForm: CommentForm = {
-        content: commentContent,
-        edit_id: createCommentRes.comment.id,
-        post_id: createPostRes.post.id,
-        deleted: true,
-        auth: lemmyBetaAuth,
-        creator_id: createCommentRes.comment.creator_id,
-      };
-
-      let deleteCommentRes: CommentResponse = await fetch(
-        `${lemmyBetaApiUrl}/comment`,
-        {
-          method: 'PUT',
-          headers: {
-            'Content-Type': 'application/json',
-          },
-          body: wrapper(deleteCommentForm),
-        }
-      ).then(d => d.json());
-      expect(deleteCommentRes.comment.deleted).toBe(true);
-
-      // lemmy_alpha sees that the comment is deleted
-      let getPostUrl = `${lemmyAlphaApiUrl}/post?id=3`;
-      let getPostRes: GetPostResponse = await fetch(getPostUrl, {
-        method: 'GET',
-      }).then(d => d.json());
-      expect(getPostRes.comments[0].deleted).toBe(true);
-
-      // lemmy_beta undeletes the comment
-      let undeleteCommentForm: CommentForm = {
-        content: commentContent,
-        edit_id: createCommentRes.comment.id,
-        post_id: createPostRes.post.id,
-        deleted: false,
-        auth: lemmyBetaAuth,
-        creator_id: createCommentRes.comment.creator_id,
-      };
-
-      let undeleteCommentRes: CommentResponse = await fetch(
-        `${lemmyBetaApiUrl}/comment`,
-        {
-          method: 'PUT',
-          headers: {
-            'Content-Type': 'application/json',
-          },
-          body: wrapper(undeleteCommentForm),
-        }
-      ).then(d => d.json());
-      expect(undeleteCommentRes.comment.deleted).toBe(false);
-
-      // lemmy_alpha sees that the comment is undeleted
-      let getPostUndeleteRes: GetPostResponse = await fetch(getPostUrl, {
-        method: 'GET',
-      }).then(d => d.json());
-      expect(getPostUndeleteRes.comments[0].deleted).toBe(false);
-
-      // lemmy_beta deletes the post
-      let deletePostForm: PostForm = {
-        name: postName,
-        edit_id: createPostRes.post.id,
-        auth: lemmyBetaAuth,
-        community_id: createPostRes.post.community_id,
-        creator_id: createPostRes.post.creator_id,
-        nsfw: false,
-        deleted: true,
-      };
-
-      let deletePostRes: PostResponse = await fetch(`${lemmyBetaApiUrl}/post`, {
-        method: 'PUT',
-        headers: {
-          'Content-Type': 'application/json',
-        },
-        body: wrapper(deletePostForm),
-      }).then(d => d.json());
-      expect(deletePostRes.post.deleted).toBe(true);
-
-      // Make sure lemmy_alpha sees the post is deleted
-      let getPostResAgain: GetPostResponse = await fetch(getPostUrl, {
-        method: 'GET',
-      }).then(d => d.json());
-      expect(getPostResAgain.post.deleted).toBe(true);
-
-      // lemmy_beta undeletes the post
-      let undeletePostForm: PostForm = {
-        name: postName,
-        edit_id: createPostRes.post.id,
-        auth: lemmyBetaAuth,
-        community_id: createPostRes.post.community_id,
-        creator_id: createPostRes.post.creator_id,
-        nsfw: false,
-        deleted: false,
-      };
-
-      let undeletePostRes: PostResponse = await fetch(
-        `${lemmyBetaApiUrl}/post`,
-        {
-          method: 'PUT',
-          headers: {
-            'Content-Type': 'application/json',
-          },
-          body: wrapper(undeletePostForm),
-        }
-      ).then(d => d.json());
-      expect(undeletePostRes.post.deleted).toBe(false);
-
-      // Make sure lemmy_alpha sees the post is undeleted
-      let getPostResAgainTwo: GetPostResponse = await fetch(getPostUrl, {
-        method: 'GET',
-      }).then(d => d.json());
-      expect(getPostResAgainTwo.post.deleted).toBe(false);
-
-      // lemmy_beta deletes the community
-      let deleteCommunityForm: CommunityForm = {
-        name: communityName,
-        title: communityName,
-        category_id: 1,
-        edit_id: createCommunityRes.community.id,
-        nsfw: false,
-        deleted: true,
-        auth: lemmyBetaAuth,
-      };
-
-      let deleteResponse: CommunityResponse = await fetch(
-        `${lemmyBetaApiUrl}/community`,
-        {
-          method: 'PUT',
-          headers: {
-            'Content-Type': 'application/json',
-          },
-          body: wrapper(deleteCommunityForm),
-        }
-      ).then(d => d.json());
-
-      // Make sure the delete went through
-      expect(deleteResponse.community.deleted).toBe(true);
-
-      // Re-get it from alpha, make sure its deleted there too
-      let getCommunityUrl = `${lemmyAlphaApiUrl}/community?id=${communityOnAlphaId}&auth=${lemmyAlphaAuth}`;
-      let getCommunityRes: GetCommunityResponse = await fetch(getCommunityUrl, {
-        method: 'GET',
-      }).then(d => d.json());
-
-      expect(getCommunityRes.community.deleted).toBe(true);
-
-      // lemmy_beta undeletes the community
-      let undeleteCommunityForm: CommunityForm = {
-        name: communityName,
-        title: communityName,
-        category_id: 1,
-        edit_id: createCommunityRes.community.id,
-        nsfw: false,
-        deleted: false,
-        auth: lemmyBetaAuth,
-      };
-
-      let undeleteCommunityRes: CommunityResponse = await fetch(
-        `${lemmyBetaApiUrl}/community`,
-        {
-          method: 'PUT',
-          headers: {
-            'Content-Type': 'application/json',
-          },
-          body: wrapper(undeleteCommunityForm),
-        }
-      ).then(d => d.json());
-
-      // Make sure the delete went through
-      expect(undeleteCommunityRes.community.deleted).toBe(false);
-
-      // Re-get it from alpha, make sure its deleted there too
-      let getCommunityResAgain: GetCommunityResponse = await fetch(
-        getCommunityUrl,
-        {
-          method: 'GET',
-        }
-      ).then(d => d.json());
-      expect(getCommunityResAgain.community.deleted).toBe(false);
-    });
-  });
-
-  describe('remove things', () => {
-    test('/u/lemmy_beta removes and unremoves a federated comment, post, and community, lemmy_alpha sees its removed.', async () => {
-      // Create a test community
-      let communityName = 'test_community_rem';
-      let communityForm: CommunityForm = {
-        name: communityName,
-        title: communityName,
-        category_id: 1,
-        nsfw: false,
-        auth: lemmyBetaAuth,
-      };
-
-      let createCommunityRes: CommunityResponse = await fetch(
-        `${lemmyBetaApiUrl}/community`,
-        {
-          method: 'POST',
-          headers: {
-            'Content-Type': 'application/json',
-          },
-          body: wrapper(communityForm),
-        }
-      ).then(d => d.json());
-
-      expect(createCommunityRes.community.name).toBe(communityName);
-
-      // Cache it on lemmy_alpha
-      let searchUrl = `${lemmyAlphaApiUrl}/search?q=http://lemmy-beta:8550/c/${communityName}&type_=All&sort=TopAll`;
-      let searchResponse: SearchResponse = await fetch(searchUrl, {
-        method: 'GET',
-      }).then(d => d.json());
-
-      let communityOnAlphaId = searchResponse.communities[0].id;
-
-      // Follow it
-      let followForm: FollowCommunityForm = {
-        community_id: communityOnAlphaId,
-        follow: true,
-        auth: lemmyAlphaAuth,
-      };
-
-      let followRes: CommunityResponse = await fetch(
-        `${lemmyAlphaApiUrl}/community/follow`,
-        {
-          method: 'POST',
-          headers: {
-            'Content-Type': 'application/json',
-          },
-          body: wrapper(followForm),
-        }
-      ).then(d => d.json());
-
-      // Make sure the follow response went through
-      expect(followRes.community.local).toBe(false);
-      expect(followRes.community.name).toBe(communityName);
-
-      // Lemmy beta creates a test post
-      let postName = 'A jest test post with remove';
-      let createPostForm: PostForm = {
-        name: postName,
-        auth: lemmyBetaAuth,
-        community_id: createCommunityRes.community.id,
-        creator_id: 2,
-        nsfw: false,
-      };
-
-      let createPostRes: PostResponse = await fetch(`${lemmyBetaApiUrl}/post`, {
-        method: 'POST',
-        headers: {
-          'Content-Type': 'application/json',
-        },
-        body: wrapper(createPostForm),
-      }).then(d => d.json());
-      expect(createPostRes.post.name).toBe(postName);
-
-      // Lemmy beta creates a test comment
-      let commentContent = 'A jest test federated comment with remove';
-      let createCommentForm: CommentForm = {
-        content: commentContent,
-        post_id: createPostRes.post.id,
-        auth: lemmyBetaAuth,
-      };
-
-      let createCommentRes: CommentResponse = await fetch(
-        `${lemmyBetaApiUrl}/comment`,
-        {
-          method: 'POST',
-          headers: {
-            'Content-Type': 'application/json',
-          },
-          body: wrapper(createCommentForm),
-        }
-      ).then(d => d.json());
-
-      expect(createCommentRes.comment.content).toBe(commentContent);
-
-      // lemmy_beta removes the comment
-      let removeCommentForm: CommentForm = {
-        content: commentContent,
-        edit_id: createCommentRes.comment.id,
-        post_id: createPostRes.post.id,
-        removed: true,
-        auth: lemmyBetaAuth,
-        creator_id: createCommentRes.comment.creator_id,
-      };
-
-      let removeCommentRes: CommentResponse = await fetch(
-        `${lemmyBetaApiUrl}/comment`,
-        {
-          method: 'PUT',
-          headers: {
-            'Content-Type': 'application/json',
-          },
-          body: wrapper(removeCommentForm),
-        }
-      ).then(d => d.json());
-      expect(removeCommentRes.comment.removed).toBe(true);
-
-      // lemmy_alpha sees that the comment is removed
-      let getPostUrl = `${lemmyAlphaApiUrl}/post?id=4`;
-      let getPostRes: GetPostResponse = await fetch(getPostUrl, {
-        method: 'GET',
-      }).then(d => d.json());
-      expect(getPostRes.comments[0].removed).toBe(true);
-
-      // lemmy_beta undeletes the comment
-      let unremoveCommentForm: CommentForm = {
-        content: commentContent,
-        edit_id: createCommentRes.comment.id,
-        post_id: createPostRes.post.id,
-        removed: false,
-        auth: lemmyBetaAuth,
-        creator_id: createCommentRes.comment.creator_id,
-      };
-
-      let unremoveCommentRes: CommentResponse = await fetch(
-        `${lemmyBetaApiUrl}/comment`,
-        {
-          method: 'PUT',
-          headers: {
-            'Content-Type': 'application/json',
-          },
-          body: wrapper(unremoveCommentForm),
-        }
-      ).then(d => d.json());
-      expect(unremoveCommentRes.comment.removed).toBe(false);
-
-      // lemmy_alpha sees that the comment is undeleted
-      let getPostUnremoveRes: GetPostResponse = await fetch(getPostUrl, {
-        method: 'GET',
-      }).then(d => d.json());
-      expect(getPostUnremoveRes.comments[0].removed).toBe(false);
-
-      // lemmy_beta deletes the post
-      let removePostForm: PostForm = {
-        name: postName,
-        edit_id: createPostRes.post.id,
-        auth: lemmyBetaAuth,
-        community_id: createPostRes.post.community_id,
-        creator_id: createPostRes.post.creator_id,
-        nsfw: false,
-        removed: true,
-      };
-
-      let removePostRes: PostResponse = await fetch(`${lemmyBetaApiUrl}/post`, {
-        method: 'PUT',
-        headers: {
-          'Content-Type': 'application/json',
-        },
-        body: wrapper(removePostForm),
-      }).then(d => d.json());
-      expect(removePostRes.post.removed).toBe(true);
-
-      // Make sure lemmy_alpha sees the post is deleted
-      let getPostResAgain: GetPostResponse = await fetch(getPostUrl, {
-        method: 'GET',
-      }).then(d => d.json());
-      expect(getPostResAgain.post.removed).toBe(true);
-
-      // lemmy_beta unremoves the post
-      let unremovePostForm: PostForm = {
-        name: postName,
-        edit_id: createPostRes.post.id,
-        auth: lemmyBetaAuth,
-        community_id: createPostRes.post.community_id,
-        creator_id: createPostRes.post.creator_id,
-        nsfw: false,
-        removed: false,
-      };
-
-      let unremovePostRes: PostResponse = await fetch(
-        `${lemmyBetaApiUrl}/post`,
-        {
-          method: 'PUT',
-          headers: {
-            'Content-Type': 'application/json',
-          },
-          body: wrapper(unremovePostForm),
-        }
-      ).then(d => d.json());
-      expect(unremovePostRes.post.removed).toBe(false);
-
-      // Make sure lemmy_alpha sees the post is unremoved
-      let getPostResAgainTwo: GetPostResponse = await fetch(getPostUrl, {
-        method: 'GET',
-      }).then(d => d.json());
-      expect(getPostResAgainTwo.post.removed).toBe(false);
-
-      // lemmy_beta deletes the community
-      let removeCommunityForm: CommunityForm = {
-        name: communityName,
-        title: communityName,
-        category_id: 1,
-        edit_id: createCommunityRes.community.id,
-        nsfw: false,
-        removed: true,
-        auth: lemmyBetaAuth,
-      };
-
-      let removeCommunityRes: CommunityResponse = await fetch(
-        `${lemmyBetaApiUrl}/community`,
-        {
-          method: 'PUT',
-          headers: {
-            'Content-Type': 'application/json',
-          },
-          body: wrapper(removeCommunityForm),
-        }
-      ).then(d => d.json());
-
-      // Make sure the delete went through
-      expect(removeCommunityRes.community.removed).toBe(true);
-
-      // Re-get it from alpha, make sure its removed there too
-      let getCommunityUrl = `${lemmyAlphaApiUrl}/community?id=${communityOnAlphaId}&auth=${lemmyAlphaAuth}`;
-      let getCommunityRes: GetCommunityResponse = await fetch(getCommunityUrl, {
-        method: 'GET',
-      }).then(d => d.json());
-
-      expect(getCommunityRes.community.removed).toBe(true);
-
-      // lemmy_beta unremoves the community
-      let unremoveCommunityForm: CommunityForm = {
-        name: communityName,
-        title: communityName,
-        category_id: 1,
-        edit_id: createCommunityRes.community.id,
-        nsfw: false,
-        removed: false,
-        auth: lemmyBetaAuth,
-      };
-
-      let unremoveCommunityRes: CommunityResponse = await fetch(
-        `${lemmyBetaApiUrl}/community`,
-        {
-          method: 'PUT',
-          headers: {
-            'Content-Type': 'application/json',
-          },
-          body: wrapper(unremoveCommunityForm),
-        }
-      ).then(d => d.json());
-
-      // Make sure the delete went through
-      expect(unremoveCommunityRes.community.removed).toBe(false);
-
-      // Re-get it from alpha, make sure its deleted there too
-      let getCommunityResAgain: GetCommunityResponse = await fetch(
-        getCommunityUrl,
-        {
-          method: 'GET',
-        }
-      ).then(d => d.json());
-      expect(getCommunityResAgain.community.removed).toBe(false);
-    });
-  });
-
-  describe('private message', () => {
-    test('/u/lemmy_alpha creates/updates/deletes/undeletes a private_message to /u/lemmy_beta, its on both instances', async () => {
-      let content = 'A jest test federated private message';
-      let privateMessageForm: PrivateMessageForm = {
-        content,
-        recipient_id: 3,
-        auth: lemmyAlphaAuth,
-      };
-
-      let createRes: PrivateMessageResponse = await fetch(
-        `${lemmyAlphaApiUrl}/private_message`,
-        {
-          method: 'POST',
-          headers: {
-            'Content-Type': 'application/json',
-          },
-          body: wrapper(privateMessageForm),
-        }
-      ).then(d => d.json());
-      expect(createRes.message.content).toBe(content);
-      expect(createRes.message.local).toBe(true);
-      expect(createRes.message.creator_local).toBe(true);
-      expect(createRes.message.recipient_local).toBe(false);
-
-      // Get it from beta
-      let getPrivateMessagesUrl = `${lemmyBetaApiUrl}/private_message/list?auth=${lemmyBetaAuth}&unread_only=false`;
-
-      let getPrivateMessagesRes: PrivateMessagesResponse = await fetch(
-        getPrivateMessagesUrl,
-        {
-          method: 'GET',
-        }
-      ).then(d => d.json());
-
-      expect(getPrivateMessagesRes.messages[0].content).toBe(content);
-      expect(getPrivateMessagesRes.messages[0].local).toBe(false);
-      expect(getPrivateMessagesRes.messages[0].creator_local).toBe(false);
-      expect(getPrivateMessagesRes.messages[0].recipient_local).toBe(true);
-
-      // lemmy alpha updates the private message
-      let updatedContent = 'A jest test federated private message edited';
-      let updatePrivateMessageForm: EditPrivateMessageForm = {
-        content: updatedContent,
-        edit_id: createRes.message.id,
-        auth: lemmyAlphaAuth,
-      };
-
-      let updateRes: PrivateMessageResponse = await fetch(
-        `${lemmyAlphaApiUrl}/private_message`,
-        {
-          method: 'PUT',
-          headers: {
-            'Content-Type': 'application/json',
-          },
-          body: wrapper(updatePrivateMessageForm),
-        }
-      ).then(d => d.json());
-
-      expect(updateRes.message.content).toBe(updatedContent);
-
-      // Fetch from beta again
-      let getPrivateMessagesUpdatedRes: PrivateMessagesResponse = await fetch(
-        getPrivateMessagesUrl,
-        {
-          method: 'GET',
-        }
-      ).then(d => d.json());
-
-      expect(getPrivateMessagesUpdatedRes.messages[0].content).toBe(
-        updatedContent
-      );
-
-      // lemmy alpha deletes the private message
-      let deletePrivateMessageForm: EditPrivateMessageForm = {
-        deleted: true,
-        edit_id: createRes.message.id,
-        auth: lemmyAlphaAuth,
-      };
-
-      let deleteRes: PrivateMessageResponse = await fetch(
-        `${lemmyAlphaApiUrl}/private_message`,
-        {
-          method: 'PUT',
-          headers: {
-            'Content-Type': 'application/json',
-          },
-          body: wrapper(deletePrivateMessageForm),
-        }
-      ).then(d => d.json());
-
-      expect(deleteRes.message.deleted).toBe(true);
-
-      // Fetch from beta again
-      let getPrivateMessagesDeletedRes: PrivateMessagesResponse = await fetch(
-        getPrivateMessagesUrl,
-        {
-          method: 'GET',
-        }
-      ).then(d => d.json());
-
-      // The GetPrivateMessages filters out deleted,
-      // even though they are in the actual database.
-      // no reason to show them
-      expect(getPrivateMessagesDeletedRes.messages.length).toBe(0);
-
-      // lemmy alpha undeletes the private message
-      let undeletePrivateMessageForm: EditPrivateMessageForm = {
-        deleted: false,
-        edit_id: createRes.message.id,
-        auth: lemmyAlphaAuth,
-      };
-
-      let undeleteRes: PrivateMessageResponse = await fetch(
-        `${lemmyAlphaApiUrl}/private_message`,
-        {
-          method: 'PUT',
-          headers: {
-            'Content-Type': 'application/json',
-          },
-          body: wrapper(undeletePrivateMessageForm),
-        }
-      ).then(d => d.json());
-
-      expect(undeleteRes.message.deleted).toBe(false);
-
-      // Fetch from beta again
-      let getPrivateMessagesUnDeletedRes: PrivateMessagesResponse = await fetch(
-        getPrivateMessagesUrl,
-        {
-          method: 'GET',
-        }
-      ).then(d => d.json());
-
-      expect(getPrivateMessagesUnDeletedRes.messages[0].deleted).toBe(false);
-    });
-  });
-
-  describe('comment_search', () => {
-    test('Create comment on alpha and search it', async () => {
-      let content = 'A jest test federated comment for search';
-      let commentForm: CommentForm = {
-        content,
-        post_id: 1,
-        auth: lemmyAlphaAuth,
-      };
-
-      let createResponse: CommentResponse = await fetch(
-        `${lemmyAlphaApiUrl}/comment`,
-        {
-          method: 'POST',
-          headers: {
-            'Content-Type': 'application/json',
-          },
-          body: wrapper(commentForm),
-        }
-      ).then(d => d.json());
-
-      let searchUrl = `${lemmyBetaApiUrl}/search?q=${createResponse.comment.ap_id}&type_=All&sort=TopAll`;
-      let searchResponse: SearchResponse = await fetch(searchUrl, {
-        method: 'GET',
-      }).then(d => d.json());
-
-      // TODO: check more fields
-      expect(searchResponse.comments[0].content).toBe(content);
-    });
-  });
-
-  describe('announce', () => {
-    test('A and G subscribe to B (center) A does action, it gets announced to G', async () => {
-      // A and G are already subscribed to B earlier.
-      //
-      let postName = 'A jest test post for announce';
-      let createPostForm: PostForm = {
-        name: postName,
-        auth: lemmyAlphaAuth,
-        community_id: 2,
-        creator_id: 2,
-        nsfw: false,
-      };
-
-      let createPostRes: PostResponse = await fetch(
-        `${lemmyAlphaApiUrl}/post`,
-        {
-          method: 'POST',
-          headers: {
-            'Content-Type': 'application/json',
-          },
-          body: wrapper(createPostForm),
-        }
-      ).then(d => d.json());
-      expect(createPostRes.post.name).toBe(postName);
-
-      // Make sure that post got announced to Gamma
-      let searchUrl = `${lemmyGammaApiUrl}/search?q=${createPostRes.post.ap_id}&type_=All&sort=TopAll`;
-      let searchResponse: SearchResponse = await fetch(searchUrl, {
-        method: 'GET',
-      }).then(d => d.json());
-      let postId = searchResponse.posts[0].id;
-      expect(searchResponse.posts[0].name).toBe(postName);
-
-      // Create a test comment on Gamma, make sure it gets announced to alpha
-      let commentContent =
-        'A jest test federated comment announce, lets mention @lemmy_beta@lemmy-beta:8550';
-
-      let commentForm: CommentForm = {
-        content: commentContent,
-        post_id: postId,
-        auth: lemmyGammaAuth,
-      };
-
-      let createCommentRes: CommentResponse = await fetch(
-        `${lemmyGammaApiUrl}/comment`,
-        {
-          method: 'POST',
-          headers: {
-            'Content-Type': 'application/json',
-          },
-          body: wrapper(commentForm),
-        }
-      ).then(d => d.json());
-
-      expect(createCommentRes.comment.content).toBe(commentContent);
-      expect(createCommentRes.comment.community_local).toBe(false);
-      expect(createCommentRes.comment.creator_local).toBe(true);
-      expect(createCommentRes.comment.score).toBe(1);
-
-      // Get the post from alpha, make sure it has gamma's comment
-      let getPostUrl = `${lemmyAlphaApiUrl}/post?id=5`;
-      let getPostRes: GetPostResponse = await fetch(getPostUrl, {
-        method: 'GET',
-      }).then(d => d.json());
-
-      expect(getPostRes.comments[0].content).toBe(commentContent);
-      expect(getPostRes.comments[0].community_local).toBe(true);
-      expect(getPostRes.comments[0].creator_local).toBe(false);
-      expect(getPostRes.comments[0].score).toBe(1);
-    });
-  });
-
-  describe('fetch inreplytos', () => {
-    test('A is unsubbed from B, B makes a post, and some embedded comments, A subs to B, B updates the lowest level comment, A fetches both the post and all the inreplyto comments for that post.', async () => {
-      // Check that A is subscribed to B
-      let followedCommunitiesUrl = `${lemmyAlphaApiUrl}/user/followed_communities?&auth=${lemmyAlphaAuth}`;
-      let followedCommunitiesRes: GetFollowedCommunitiesResponse = await fetch(
-        followedCommunitiesUrl,
-        {
-          method: 'GET',
-        }
-      ).then(d => d.json());
-      expect(followedCommunitiesRes.communities[1].community_local).toBe(false);
-
-      // A unsubs from B (communities ids 3-5)
-      for (let i = 3; i <= 5; i++) {
-        let unfollowForm: FollowCommunityForm = {
-          community_id: i,
-          follow: false,
-          auth: lemmyAlphaAuth,
-        };
-
-        let unfollowRes: CommunityResponse = await fetch(
-          `${lemmyAlphaApiUrl}/community/follow`,
-          {
-            method: 'POST',
-            headers: {
-              'Content-Type': 'application/json',
-            },
-            body: wrapper(unfollowForm),
-          }
-        ).then(d => d.json());
-        expect(unfollowRes.community.local).toBe(false);
-      }
-
-      // Check that you are unsubscribed from all of them locally
-      let followedCommunitiesResAgain: GetFollowedCommunitiesResponse = await fetch(
-        followedCommunitiesUrl,
-        {
-          method: 'GET',
-        }
-      ).then(d => d.json());
-      expect(followedCommunitiesResAgain.communities.length).toBe(1);
-
-      // B creates a post, and two comments, should be invisible to A
-      let betaPostName = 'Test post on B, invisible to A at first';
-      let postForm: PostForm = {
-        name: betaPostName,
-        auth: lemmyBetaAuth,
-        community_id: 2,
-        creator_id: 2,
-        nsfw: false,
-      };
-
-      let createPostRes: PostResponse = await fetch(`${lemmyBetaApiUrl}/post`, {
-        method: 'POST',
-        headers: {
-          'Content-Type': 'application/json',
-        },
-        body: wrapper(postForm),
-      }).then(d => d.json());
-      expect(createPostRes.post.name).toBe(betaPostName);
-
-      // B creates a comment, then a child one of that.
-      let parentCommentContent = 'An invisible top level comment from beta';
-      let createParentCommentForm: CommentForm = {
-        content: parentCommentContent,
-        post_id: createPostRes.post.id,
-        auth: lemmyBetaAuth,
-      };
-
-      let createParentCommentRes: CommentResponse = await fetch(
-        `${lemmyBetaApiUrl}/comment`,
-        {
-          method: 'POST',
-          headers: {
-            'Content-Type': 'application/json',
-          },
-          body: wrapper(createParentCommentForm),
-        }
-      ).then(d => d.json());
-      expect(createParentCommentRes.comment.content).toBe(parentCommentContent);
-
-      let childCommentContent = 'An invisible child comment from beta';
-      let createChildCommentForm: CommentForm = {
-        content: childCommentContent,
-        parent_id: createParentCommentRes.comment.id,
-        post_id: createPostRes.post.id,
-        auth: lemmyBetaAuth,
-      };
-
-      let createChildCommentRes: CommentResponse = await fetch(
-        `${lemmyBetaApiUrl}/comment`,
-        {
-          method: 'POST',
-          headers: {
-            'Content-Type': 'application/json',
-          },
-          body: wrapper(createChildCommentForm),
-        }
-      ).then(d => d.json());
-      expect(createChildCommentRes.comment.content).toBe(childCommentContent);
-
-      // Follow again, for other tests
-      let searchUrl = `${lemmyAlphaApiUrl}/search?q=!main@lemmy-beta:8550&type_=All&sort=TopAll`;
-
-      let searchResponse: SearchResponse = await fetch(searchUrl, {
-        method: 'GET',
-      }).then(d => d.json());
-
-      expect(searchResponse.communities[0].name).toBe('main');
-
-      let followForm: FollowCommunityForm = {
-        community_id: searchResponse.communities[0].id,
-        follow: true,
-        auth: lemmyAlphaAuth,
-      };
-
-      let followResAgain: CommunityResponse = await fetch(
-        `${lemmyAlphaApiUrl}/community/follow`,
-        {
-          method: 'POST',
-          headers: {
-            'Content-Type': 'application/json',
-          },
-          body: wrapper(followForm),
-        }
-      ).then(d => d.json());
-
-      // Make sure the follow response went through
-      expect(followResAgain.community.local).toBe(false);
-      expect(followResAgain.community.name).toBe('main');
-
-      let updatedCommentContent = 'An update child comment from beta';
-      let updatedCommentForm: CommentForm = {
-        content: updatedCommentContent,
-        post_id: createPostRes.post.id,
-        edit_id: createChildCommentRes.comment.id,
-        auth: lemmyBetaAuth,
-        creator_id: 2,
-      };
-
-      let updateResponse: CommentResponse = await fetch(
-        `${lemmyBetaApiUrl}/comment`,
-        {
-          method: 'PUT',
-          headers: {
-            'Content-Type': 'application/json',
-          },
-          body: wrapper(updatedCommentForm),
-        }
-      ).then(d => d.json());
-      expect(updateResponse.comment.content).toBe(updatedCommentContent);
-
-      // Make sure that A picked up the post, parent comment, and child comment
-      let getPostUrl = `${lemmyAlphaApiUrl}/post?id=6`;
-      let getPostRes: GetPostResponse = await fetch(getPostUrl, {
-        method: 'GET',
-      }).then(d => d.json());
-
-      expect(getPostRes.post.name).toBe(betaPostName);
-      expect(getPostRes.comments[1].content).toBe(parentCommentContent);
-      expect(getPostRes.comments[0].content).toBe(updatedCommentContent);
-      expect(getPostRes.post.community_local).toBe(false);
-      expect(getPostRes.post.creator_local).toBe(false);
-    });
-  });
-});
-
-function wrapper(form: any): string {
-  return JSON.stringify(form);
-}
diff --git a/ui/src/api_tests/comment.spec.ts b/ui/src/api_tests/comment.spec.ts
new file mode 100644 (file)
index 0000000..747ec91
--- /dev/null
@@ -0,0 +1,336 @@
+import {
+  alpha,
+  beta,
+  gamma,
+  setupLogins,
+  createPost,
+  getPost,
+  searchComment,
+  likeComment,
+  followBeta,
+  searchForBetaCommunity,
+  createComment,
+  updateComment,
+  deleteComment,
+  removeComment,
+  getMentions,
+  searchPost,
+  unfollowRemotes,
+  createCommunity,
+  registerUser,
+  API,
+} from './shared';
+
+import { PostResponse } from 'lemmy-js-client';
+
+let postRes: PostResponse;
+
+beforeAll(async () => {
+  await setupLogins();
+  await followBeta(alpha);
+  await followBeta(gamma);
+  let search = await searchForBetaCommunity(alpha);
+  postRes = await createPost(
+    alpha,
+    search.communities.filter(c => c.local == false)[0].id
+  );
+});
+
+afterAll(async () => {
+  await unfollowRemotes(alpha);
+  await unfollowRemotes(gamma);
+});
+
+test('Create a comment', async () => {
+  let commentRes = await createComment(alpha, postRes.post.id);
+  expect(commentRes.comment.content).toBeDefined();
+  expect(commentRes.comment.community_local).toBe(false);
+  expect(commentRes.comment.creator_local).toBe(true);
+  expect(commentRes.comment.score).toBe(1);
+
+  // Make sure that comment is liked on beta
+  let searchBeta = await searchComment(beta, commentRes.comment);
+  let betaComment = searchBeta.comments[0];
+  expect(betaComment).toBeDefined();
+  expect(betaComment.community_local).toBe(true);
+  expect(betaComment.creator_local).toBe(false);
+  expect(betaComment.score).toBe(1);
+});
+
+test('Create a comment in a non-existent post', async () => {
+  let commentRes = await createComment(alpha, -1);
+  expect(commentRes).toStrictEqual({ error: 'couldnt_find_post' });
+});
+
+test('Update a comment', async () => {
+  let commentRes = await createComment(alpha, postRes.post.id);
+  let updateCommentRes = await updateComment(alpha, commentRes.comment.id);
+  expect(updateCommentRes.comment.content).toBe(
+    'A jest test federated comment update'
+  );
+  expect(updateCommentRes.comment.community_local).toBe(false);
+  expect(updateCommentRes.comment.creator_local).toBe(true);
+
+  // Make sure that post is updated on beta
+  let searchBeta = await searchComment(beta, commentRes.comment);
+  let betaComment = searchBeta.comments[0];
+  expect(betaComment.content).toBe('A jest test federated comment update');
+});
+
+test('Delete a comment', async () => {
+  let commentRes = await createComment(alpha, postRes.post.id);
+  let deleteCommentRes = await deleteComment(
+    alpha,
+    true,
+    commentRes.comment.id
+  );
+  expect(deleteCommentRes.comment.deleted).toBe(true);
+
+  // Make sure that comment is deleted on beta
+  // The search doesnt work below, because it returns a tombstone / http::gone
+  // let searchBeta = await searchComment(beta, commentRes.comment);
+  // console.log(searchBeta);
+  // let betaComment = searchBeta.comments[0];
+  // Create a fake post, just to get the previous new post id
+  let createdBetaPostJustToGetId = await createPost(beta, 2);
+  let betaPost = await getPost(beta, createdBetaPostJustToGetId.post.id - 1);
+  let betaComment = betaPost.comments[0];
+  expect(betaComment.deleted).toBe(true);
+
+  let undeleteCommentRes = await deleteComment(
+    alpha,
+    false,
+    commentRes.comment.id
+  );
+  expect(undeleteCommentRes.comment.deleted).toBe(false);
+
+  // Make sure that comment is undeleted on beta
+  let searchBeta2 = await searchComment(beta, commentRes.comment);
+  let betaComment2 = searchBeta2.comments[0];
+  expect(betaComment2.deleted).toBe(false);
+});
+
+test('Remove a comment from admin and community on the same instance', async () => {
+  let commentRes = await createComment(alpha, postRes.post.id);
+
+  // Get the id for beta
+  let betaCommentId = (await searchComment(beta, commentRes.comment))
+    .comments[0].id;
+
+  // The beta admin removes it (the community lives on beta)
+  let removeCommentRes = await removeComment(beta, true, betaCommentId);
+  expect(removeCommentRes.comment.removed).toBe(true);
+
+  // Make sure that comment is removed on alpha (it gets pushed since an admin from beta removed it)
+  let refetchedPost = await getPost(alpha, postRes.post.id);
+  expect(refetchedPost.comments[0].removed).toBe(true);
+
+  let unremoveCommentRes = await removeComment(beta, false, betaCommentId);
+  expect(unremoveCommentRes.comment.removed).toBe(false);
+
+  // Make sure that comment is unremoved on beta
+  let refetchedPost2 = await getPost(alpha, postRes.post.id);
+  expect(refetchedPost2.comments[0].removed).toBe(false);
+});
+
+test('Remove a comment from admin and community on different instance', async () => {
+  let alphaUser = await registerUser(alpha);
+  let newAlphaApi: API = {
+    client: alpha.client,
+    auth: alphaUser.jwt,
+  };
+
+  // New alpha user creates a community, post, and comment.
+  let newCommunity = await createCommunity(newAlphaApi);
+  let newPost = await createPost(newAlphaApi, newCommunity.community.id);
+  let commentRes = await createComment(newAlphaApi, newPost.post.id);
+  expect(commentRes.comment.content).toBeDefined();
+
+  // Beta searches that to cache it, then removes it
+  let searchBeta = await searchComment(beta, commentRes.comment);
+  let betaComment = searchBeta.comments[0];
+  let removeCommentRes = await removeComment(beta, true, betaComment.id);
+  expect(removeCommentRes.comment.removed).toBe(true);
+
+  // Make sure its not removed on alpha
+  let refetchedPost = await getPost(newAlphaApi, newPost.post.id);
+  expect(refetchedPost.comments[0].removed).toBe(false);
+});
+
+test('Unlike a comment', async () => {
+  let commentRes = await createComment(alpha, postRes.post.id);
+  let unlike = await likeComment(alpha, 0, commentRes.comment);
+  expect(unlike.comment.score).toBe(0);
+
+  // Make sure that post is unliked on beta
+  let searchBeta = await searchComment(beta, commentRes.comment);
+  let betaComment = searchBeta.comments[0];
+  expect(betaComment).toBeDefined();
+  expect(betaComment.community_local).toBe(true);
+  expect(betaComment.creator_local).toBe(false);
+  expect(betaComment.score).toBe(0);
+});
+
+test('Federated comment like', async () => {
+  let commentRes = await createComment(alpha, postRes.post.id);
+
+  // Find the comment on beta
+  let searchBeta = await searchComment(beta, commentRes.comment);
+  let betaComment = searchBeta.comments[0];
+
+  let like = await likeComment(beta, 1, betaComment);
+  expect(like.comment.score).toBe(2);
+
+  // Get the post from alpha, check the likes
+  let post = await getPost(alpha, postRes.post.id);
+  expect(post.comments[0].score).toBe(2);
+});
+
+test('Reply to a comment', async () => {
+  // Create a comment on alpha, find it on beta
+  let commentRes = await createComment(alpha, postRes.post.id);
+  let searchBeta = await searchComment(beta, commentRes.comment);
+  let betaComment = searchBeta.comments[0];
+
+  // find that comment id on beta
+
+  // Reply from beta
+  let replyRes = await createComment(beta, betaComment.post_id, betaComment.id);
+  expect(replyRes.comment.content).toBeDefined();
+  expect(replyRes.comment.community_local).toBe(true);
+  expect(replyRes.comment.creator_local).toBe(true);
+  expect(replyRes.comment.parent_id).toBe(betaComment.id);
+  expect(replyRes.comment.score).toBe(1);
+
+  // Make sure that comment is seen on alpha
+  // TODO not sure why, but a searchComment back to alpha, for the ap_id of betas
+  // comment, isn't working.
+  // let searchAlpha = await searchComment(alpha, replyRes.comment);
+  let post = await getPost(alpha, postRes.post.id);
+  let alphaComment = post.comments[0];
+  expect(alphaComment.content).toBeDefined();
+  expect(alphaComment.parent_id).toBe(post.comments[1].id);
+  expect(alphaComment.community_local).toBe(false);
+  expect(alphaComment.creator_local).toBe(false);
+  expect(alphaComment.score).toBe(1);
+});
+
+test('Mention beta', async () => {
+  // Create a mention on alpha
+  let mentionContent = 'A test mention of @lemmy_beta@lemmy-beta:8550';
+  let commentRes = await createComment(alpha, postRes.post.id);
+  let mentionRes = await createComment(
+    alpha,
+    postRes.post.id,
+    commentRes.comment.id,
+    mentionContent
+  );
+  expect(mentionRes.comment.content).toBeDefined();
+  expect(mentionRes.comment.community_local).toBe(false);
+  expect(mentionRes.comment.creator_local).toBe(true);
+  expect(mentionRes.comment.score).toBe(1);
+
+  let mentionsRes = await getMentions(beta);
+  expect(mentionsRes.mentions[0].content).toBeDefined();
+  expect(mentionsRes.mentions[0].community_local).toBe(true);
+  expect(mentionsRes.mentions[0].creator_local).toBe(false);
+  expect(mentionsRes.mentions[0].score).toBe(1);
+});
+
+test('Comment Search', async () => {
+  let commentRes = await createComment(alpha, postRes.post.id);
+  let searchBeta = await searchComment(beta, commentRes.comment);
+  expect(searchBeta.comments[0].ap_id).toBe(commentRes.comment.ap_id);
+});
+
+test('A and G subscribe to B (center) A posts, G mentions B, it gets announced to A', async () => {
+  // Create a local post
+  let alphaPost = await createPost(alpha, 2);
+  expect(alphaPost.post.community_local).toBe(true);
+
+  // Make sure gamma sees it
+  let search = await searchPost(gamma, alphaPost.post);
+  let gammaPost = search.posts[0];
+
+  let commentContent =
+    'A jest test federated comment announce, lets mention @lemmy_beta@lemmy-beta:8550';
+  let commentRes = await createComment(
+    gamma,
+    gammaPost.id,
+    undefined,
+    commentContent
+  );
+  expect(commentRes.comment.content).toBe(commentContent);
+  expect(commentRes.comment.community_local).toBe(false);
+  expect(commentRes.comment.creator_local).toBe(true);
+  expect(commentRes.comment.score).toBe(1);
+
+  // Make sure alpha sees it
+  let alphaPost2 = await getPost(alpha, alphaPost.post.id);
+  expect(alphaPost2.comments[0].content).toBe(commentContent);
+  expect(alphaPost2.comments[0].community_local).toBe(true);
+  expect(alphaPost2.comments[0].creator_local).toBe(false);
+  expect(alphaPost2.comments[0].score).toBe(1);
+
+  // Make sure beta has mentions
+  let mentionsRes = await getMentions(beta);
+  expect(mentionsRes.mentions[0].content).toBe(commentContent);
+  expect(mentionsRes.mentions[0].community_local).toBe(false);
+  expect(mentionsRes.mentions[0].creator_local).toBe(false);
+  // TODO this is failing because fetchInReplyTos aren't getting score
+  // expect(mentionsRes.mentions[0].score).toBe(1);
+});
+
+test('Fetch in_reply_tos: A is unsubbed from B, B makes a post, and some embedded comments, A subs to B, B updates the lowest level comment, A fetches both the post and all the inreplyto comments for that post.', async () => {
+  // Unfollow all remote communities
+  let followed = await unfollowRemotes(alpha);
+  expect(
+    followed.communities.filter(c => c.community_local == false).length
+  ).toBe(0);
+
+  // B creates a post, and two comments, should be invisible to A
+  let postRes = await createPost(beta, 2);
+  expect(postRes.post.name).toBeDefined();
+
+  let parentCommentContent = 'An invisible top level comment from beta';
+  let parentCommentRes = await createComment(
+    beta,
+    postRes.post.id,
+    undefined,
+    parentCommentContent
+  );
+  expect(parentCommentRes.comment.content).toBe(parentCommentContent);
+
+  // B creates a comment, then a child one of that.
+  let childCommentContent = 'An invisible child comment from beta';
+  let childCommentRes = await createComment(
+    beta,
+    postRes.post.id,
+    parentCommentRes.comment.id,
+    childCommentContent
+  );
+  expect(childCommentRes.comment.content).toBe(childCommentContent);
+
+  // Follow beta again
+  let follow = await followBeta(alpha);
+  expect(follow.community.local).toBe(false);
+  expect(follow.community.name).toBe('main');
+
+  // An update to the child comment on beta, should push the post, parent, and child to alpha now
+  let updatedCommentContent = 'An update child comment from beta';
+  let updateRes = await updateComment(
+    beta,
+    childCommentRes.comment.id,
+    updatedCommentContent
+  );
+  expect(updateRes.comment.content).toBe(updatedCommentContent);
+
+  // Get the post from alpha
+  let createFakeAlphaPostToGetId = await createPost(alpha, 2);
+  let alphaPost = await getPost(alpha, createFakeAlphaPostToGetId.post.id - 1);
+  expect(alphaPost.post.name).toBeDefined();
+  expect(alphaPost.comments[1].content).toBe(parentCommentContent);
+  expect(alphaPost.comments[0].content).toBe(updatedCommentContent);
+  expect(alphaPost.post.community_local).toBe(false);
+  expect(alphaPost.post.creator_local).toBe(false);
+});
diff --git a/ui/src/api_tests/community.spec.ts b/ui/src/api_tests/community.spec.ts
new file mode 100644 (file)
index 0000000..6945e33
--- /dev/null
@@ -0,0 +1,88 @@
+import {
+  alpha,
+  beta,
+  setupLogins,
+  searchForBetaCommunity,
+  createCommunity,
+  deleteCommunity,
+  removeCommunity,
+} from './shared';
+
+beforeAll(async () => {
+  await setupLogins();
+});
+
+test('Create community', async () => {
+  let communityRes = await createCommunity(alpha);
+  expect(communityRes.community.name).toBeDefined();
+
+  // A dupe check
+  let prevName = communityRes.community.name;
+  let communityRes2 = await createCommunity(alpha, prevName);
+  expect(communityRes2['error']).toBe('community_already_exists');
+});
+
+test('Delete community', async () => {
+  let communityRes = await createCommunity(beta);
+  let deleteCommunityRes = await deleteCommunity(
+    beta,
+    true,
+    communityRes.community.id
+  );
+  expect(deleteCommunityRes.community.deleted).toBe(true);
+
+  // Make sure it got deleted on A
+  let search = await searchForBetaCommunity(alpha);
+  let communityA = search.communities[0];
+  // TODO this fails currently, because no updates are pushed
+  // expect(communityA.deleted).toBe(true);
+
+  // Undelete
+  let undeleteCommunityRes = await deleteCommunity(
+    beta,
+    false,
+    communityRes.community.id
+  );
+  expect(undeleteCommunityRes.community.deleted).toBe(false);
+
+  // Make sure it got undeleted on A
+  let search2 = await searchForBetaCommunity(alpha);
+  let communityA2 = search2.communities[0];
+  // TODO this fails currently, because no updates are pushed
+  // expect(communityA2.deleted).toBe(false);
+});
+
+test('Remove community', async () => {
+  let communityRes = await createCommunity(beta);
+  let removeCommunityRes = await removeCommunity(
+    beta,
+    true,
+    communityRes.community.id
+  );
+  expect(removeCommunityRes.community.removed).toBe(true);
+
+  // Make sure it got removed on A
+  let search = await searchForBetaCommunity(alpha);
+  let communityA = search.communities[0];
+  // TODO this fails currently, because no updates are pushed
+  // expect(communityA.removed).toBe(true);
+
+  // unremove
+  let unremoveCommunityRes = await removeCommunity(
+    beta,
+    false,
+    communityRes.community.id
+  );
+  expect(unremoveCommunityRes.community.removed).toBe(false);
+
+  // Make sure it got unremoved on A
+  let search2 = await searchForBetaCommunity(alpha);
+  let communityA2 = search2.communities[0];
+  // TODO this fails currently, because no updates are pushed
+  // expect(communityA2.removed).toBe(false);
+});
+
+test('Search for beta community', async () => {
+  let search = await searchForBetaCommunity(alpha);
+  expect(search.communities[0].name).toBe('main');
+});
diff --git a/ui/src/api_tests/follow.spec.ts b/ui/src/api_tests/follow.spec.ts
new file mode 100644 (file)
index 0000000..2f1f8cd
--- /dev/null
@@ -0,0 +1,40 @@
+import {
+  alpha,
+  setupLogins,
+  searchForBetaCommunity,
+  followCommunity,
+  checkFollowedCommunities,
+  unfollowRemotes,
+} from './shared';
+
+beforeAll(async () => {
+  await setupLogins();
+});
+
+afterAll(async () => {
+  await unfollowRemotes(alpha);
+});
+
+test('Follow federated community', async () => {
+  let search = await searchForBetaCommunity(alpha); // TODO sometimes this is returning null?
+  let follow = await followCommunity(alpha, true, search.communities[0].id);
+
+  // Make sure the follow response went through
+  expect(follow.community.local).toBe(false);
+  expect(follow.community.name).toBe('main');
+
+  // Check it from local
+  let followCheck = await checkFollowedCommunities(alpha);
+  let remoteCommunityId = followCheck.communities.filter(
+    c => c.community_local == false
+  )[0].community_id;
+  expect(remoteCommunityId).toBeDefined();
+
+  // Test an unfollow
+  let unfollow = await followCommunity(alpha, false, remoteCommunityId);
+  expect(unfollow.community.local).toBe(false);
+
+  // Make sure you are unsubbed locally
+  let unfollowCheck = await checkFollowedCommunities(alpha);
+  expect(unfollowCheck.communities.length).toBeGreaterThanOrEqual(1);
+});
diff --git a/ui/src/api_tests/post.spec.ts b/ui/src/api_tests/post.spec.ts
new file mode 100644 (file)
index 0000000..ab9c63f
--- /dev/null
@@ -0,0 +1,265 @@
+import {
+  alpha,
+  beta,
+  gamma,
+  delta,
+  epsilon,
+  setupLogins,
+  createPost,
+  updatePost,
+  stickyPost,
+  lockPost,
+  searchPost,
+  likePost,
+  followBeta,
+  searchForBetaCommunity,
+  createComment,
+  deletePost,
+  removePost,
+  getPost,
+  unfollowRemotes,
+} from './shared';
+
+beforeAll(async () => {
+  await setupLogins();
+  await followBeta(alpha);
+  await followBeta(gamma);
+  await followBeta(delta);
+  await followBeta(epsilon);
+});
+
+afterAll(async () => {
+  await unfollowRemotes(alpha);
+  await unfollowRemotes(gamma);
+  await unfollowRemotes(delta);
+  await unfollowRemotes(epsilon);
+});
+
+test('Create a post', async () => {
+  let search = await searchForBetaCommunity(alpha);
+  let postRes = await createPost(alpha, search.communities[0].id);
+  expect(postRes.post).toBeDefined();
+  expect(postRes.post.community_local).toBe(false);
+  expect(postRes.post.creator_local).toBe(true);
+  expect(postRes.post.score).toBe(1);
+
+  // Make sure that post is liked on beta
+  let searchBeta = await searchPost(beta, postRes.post);
+  let betaPost = searchBeta.posts[0];
+
+  expect(betaPost).toBeDefined();
+  expect(betaPost.community_local).toBe(true);
+  expect(betaPost.creator_local).toBe(false);
+  expect(betaPost.score).toBe(1);
+
+  // Delta only follows beta, so it should not see an alpha ap_id
+  let searchDelta = await searchPost(delta, postRes.post);
+  expect(searchDelta.posts[0]).toBeUndefined();
+
+  // Epsilon has alpha blocked, it should not see the alpha post
+  let searchEpsilon = await searchPost(epsilon, postRes.post);
+  expect(searchEpsilon.posts[0]).toBeUndefined();
+});
+
+test('Create a post in a non-existent community', async () => {
+  let postRes = await createPost(alpha, -2);
+  expect(postRes).toStrictEqual({ error: 'couldnt_create_post' });
+});
+
+test('Unlike a post', async () => {
+  let search = await searchForBetaCommunity(alpha);
+  let postRes = await createPost(alpha, search.communities[0].id);
+  let unlike = await likePost(alpha, 0, postRes.post);
+  expect(unlike.post.score).toBe(0);
+
+  // Try to unlike it again, make sure it stays at 0
+  let unlike2 = await likePost(alpha, 0, postRes.post);
+  expect(unlike2.post.score).toBe(0);
+
+  // Make sure that post is unliked on beta
+  let searchBeta = await searchPost(beta, postRes.post);
+  let betaPost = searchBeta.posts[0];
+
+  expect(betaPost).toBeDefined();
+  expect(betaPost.community_local).toBe(true);
+  expect(betaPost.creator_local).toBe(false);
+  expect(betaPost.score).toBe(0);
+});
+
+test('Update a post', async () => {
+  let search = await searchForBetaCommunity(alpha);
+  let postRes = await createPost(alpha, search.communities[0].id);
+
+  let updatedName = 'A jest test federated post, updated';
+  let updatedPost = await updatePost(alpha, postRes.post);
+  expect(updatedPost.post.name).toBe(updatedName);
+  expect(updatedPost.post.community_local).toBe(false);
+  expect(updatedPost.post.creator_local).toBe(true);
+
+  // Make sure that post is updated on beta
+  let searchBeta = await searchPost(beta, postRes.post);
+  let betaPost = searchBeta.posts[0];
+  expect(betaPost.community_local).toBe(true);
+  expect(betaPost.creator_local).toBe(false);
+  expect(betaPost.name).toBe(updatedName);
+
+  // Make sure lemmy beta cannot update the post
+  let updatedPostBeta = await updatePost(beta, betaPost);
+  expect(updatedPostBeta).toStrictEqual({ error: 'no_post_edit_allowed' });
+});
+
+test('Sticky a post', async () => {
+  let search = await searchForBetaCommunity(alpha);
+  let postRes = await createPost(alpha, search.communities[0].id);
+
+  let stickiedPostRes = await stickyPost(alpha, true, postRes.post);
+  expect(stickiedPostRes.post.stickied).toBe(true);
+
+  // Make sure that post is stickied on beta
+  let searchBeta = await searchPost(beta, postRes.post);
+  let betaPost = searchBeta.posts[0];
+  expect(betaPost.community_local).toBe(true);
+  expect(betaPost.creator_local).toBe(false);
+  expect(betaPost.stickied).toBe(true);
+
+  // Unsticky a post
+  let unstickiedPost = await stickyPost(alpha, false, postRes.post);
+  expect(unstickiedPost.post.stickied).toBe(false);
+
+  // Make sure that post is unstickied on beta
+  let searchBeta2 = await searchPost(beta, postRes.post);
+  let betaPost2 = searchBeta2.posts[0];
+  expect(betaPost2.community_local).toBe(true);
+  expect(betaPost2.creator_local).toBe(false);
+  expect(betaPost2.stickied).toBe(false);
+
+  // Make sure that gamma cannot sticky the post on beta
+  let searchGamma = await searchPost(gamma, postRes.post);
+  let gammaPost = searchGamma.posts[0];
+  let gammaTrySticky = await stickyPost(gamma, true, gammaPost);
+  let searchBeta3 = await searchPost(beta, postRes.post);
+  let betaPost3 = searchBeta3.posts[0];
+  expect(gammaTrySticky.post.stickied).toBe(true);
+  expect(betaPost3.stickied).toBe(false);
+});
+
+test('Lock a post', async () => {
+  let search = await searchForBetaCommunity(alpha);
+  let postRes = await createPost(alpha, search.communities[0].id);
+
+  let lockedPostRes = await lockPost(alpha, true, postRes.post);
+  expect(lockedPostRes.post.locked).toBe(true);
+
+  // Make sure that post is locked on beta
+  let searchBeta = await searchPost(beta, postRes.post);
+  let betaPost = searchBeta.posts[0];
+  expect(betaPost.community_local).toBe(true);
+  expect(betaPost.creator_local).toBe(false);
+  expect(betaPost.locked).toBe(true);
+
+  // Try to make a new comment there, on alpha
+  let comment = await createComment(alpha, postRes.post.id);
+  expect(comment['error']).toBe('locked');
+
+  // Try to create a new comment, on beta
+  let commentBeta = await createComment(beta, betaPost.id);
+  expect(commentBeta['error']).toBe('locked');
+
+  // Unlock a post
+  let unlockedPost = await lockPost(alpha, false, postRes.post);
+  expect(unlockedPost.post.locked).toBe(false);
+
+  // Make sure that post is unlocked on beta
+  let searchBeta2 = await searchPost(beta, postRes.post);
+  let betaPost2 = searchBeta2.posts[0];
+  expect(betaPost2.community_local).toBe(true);
+  expect(betaPost2.creator_local).toBe(false);
+  expect(betaPost2.locked).toBe(false);
+});
+
+test('Delete a post', async () => {
+  let search = await searchForBetaCommunity(alpha);
+  let postRes = await createPost(alpha, search.communities[0].id);
+
+  let deletedPost = await deletePost(alpha, true, postRes.post);
+  expect(deletedPost.post.deleted).toBe(true);
+
+  // Make sure lemmy beta sees post is deleted
+  let createFakeBetaPostToGetId = (await createPost(beta, 2)).post.id - 1;
+  let betaPost = await getPost(beta, createFakeBetaPostToGetId);
+  expect(betaPost.post.deleted).toBe(true);
+
+  // Undelete
+  let undeletedPost = await deletePost(alpha, false, postRes.post);
+  expect(undeletedPost.post.deleted).toBe(false);
+
+  // Make sure lemmy beta sees post is undeleted
+  let betaPost2 = await getPost(beta, createFakeBetaPostToGetId);
+  expect(betaPost2.post.deleted).toBe(false);
+
+  // Make sure lemmy beta cannot delete the post
+  let deletedPostBeta = await deletePost(beta, true, betaPost2.post);
+  expect(deletedPostBeta).toStrictEqual({ error: 'no_post_edit_allowed' });
+});
+
+test('Remove a post from admin and community on different instance', async () => {
+  let search = await searchForBetaCommunity(alpha);
+  let postRes = await createPost(alpha, search.communities[0].id);
+
+  let removedPost = await removePost(alpha, true, postRes.post);
+  expect(removedPost.post.removed).toBe(true);
+
+  // Make sure lemmy beta sees post is NOT removed
+  let createFakeBetaPostToGetId = (await createPost(beta, 2)).post.id - 1;
+  let betaPost = await getPost(beta, createFakeBetaPostToGetId);
+  expect(betaPost.post.removed).toBe(false);
+
+  // Undelete
+  let undeletedPost = await removePost(alpha, false, postRes.post);
+  expect(undeletedPost.post.removed).toBe(false);
+
+  // Make sure lemmy beta sees post is undeleted
+  let betaPost2 = await getPost(beta, createFakeBetaPostToGetId);
+  expect(betaPost2.post.removed).toBe(false);
+});
+
+test('Remove a post from admin and community on same instance', async () => {
+  let search = await searchForBetaCommunity(alpha);
+  let postRes = await createPost(alpha, search.communities[0].id);
+
+  // Get the id for beta
+  let createFakeBetaPostToGetId = (await createPost(beta, 2)).post.id - 1;
+  let betaPost = await getPost(beta, createFakeBetaPostToGetId);
+
+  // The beta admin removes it (the community lives on beta)
+  let removePostRes = await removePost(beta, true, betaPost.post);
+  expect(removePostRes.post.removed).toBe(true);
+
+  // Make sure lemmy alpha sees post is removed
+  let alphaPost = await getPost(alpha, postRes.post.id);
+  expect(alphaPost.post.removed).toBe(true);
+
+  // Undelete
+  let undeletedPost = await removePost(beta, false, betaPost.post);
+  expect(undeletedPost.post.removed).toBe(false);
+
+  // Make sure lemmy alpha sees post is undeleted
+  let alphaPost2 = await getPost(alpha, postRes.post.id);
+  expect(alphaPost2.post.removed).toBe(false);
+});
+
+test('Search for a post', async () => {
+  let search = await searchForBetaCommunity(alpha);
+  let postRes = await createPost(alpha, search.communities[0].id);
+  let searchBeta = await searchPost(beta, postRes.post);
+
+  expect(searchBeta.posts[0].name).toBeDefined();
+});
+
+test('A and G subscribe to B (center) A posts, it gets announced to G', async () => {
+  let search = await searchForBetaCommunity(alpha);
+  let postRes = await createPost(alpha, search.communities[0].id);
+
+  let search2 = await searchPost(gamma, postRes.post);
+  expect(search2.posts[0].name).toBeDefined();
+});
diff --git a/ui/src/api_tests/private_message.spec.ts b/ui/src/api_tests/private_message.spec.ts
new file mode 100644 (file)
index 0000000..4bf3f07
--- /dev/null
@@ -0,0 +1,71 @@
+import {
+  alpha,
+  beta,
+  setupLogins,
+  followBeta,
+  createPrivateMessage,
+  updatePrivateMessage,
+  listPrivateMessages,
+  deletePrivateMessage,
+  unfollowRemotes,
+} from './shared';
+
+let recipient_id: number;
+
+beforeAll(async () => {
+  await setupLogins();
+  recipient_id = (await followBeta(alpha)).community.creator_id;
+});
+
+afterAll(async () => {
+  await unfollowRemotes(alpha);
+});
+
+test('Create a private message', async () => {
+  let pmRes = await createPrivateMessage(alpha, recipient_id);
+  expect(pmRes.message.content).toBeDefined();
+  expect(pmRes.message.local).toBe(true);
+  expect(pmRes.message.creator_local).toBe(true);
+  expect(pmRes.message.recipient_local).toBe(false);
+
+  let betaPms = await listPrivateMessages(beta);
+  expect(betaPms.messages[0].content).toBeDefined();
+  expect(betaPms.messages[0].local).toBe(false);
+  expect(betaPms.messages[0].creator_local).toBe(false);
+  expect(betaPms.messages[0].recipient_local).toBe(true);
+});
+
+test('Update a private message', async () => {
+  let updatedContent = 'A jest test federated private message edited';
+
+  let pmRes = await createPrivateMessage(alpha, recipient_id);
+  let pmUpdated = await updatePrivateMessage(alpha, pmRes.message.id);
+  expect(pmUpdated.message.content).toBe(updatedContent);
+
+  let betaPms = await listPrivateMessages(beta);
+  expect(betaPms.messages[0].content).toBe(updatedContent);
+});
+
+test('Delete a private message', async () => {
+  let pmRes = await createPrivateMessage(alpha, recipient_id);
+  let betaPms1 = await listPrivateMessages(beta);
+  let deletedPmRes = await deletePrivateMessage(alpha, true, pmRes.message.id);
+  expect(deletedPmRes.message.deleted).toBe(true);
+
+  // The GetPrivateMessages filters out deleted,
+  // even though they are in the actual database.
+  // no reason to show them
+  let betaPms2 = await listPrivateMessages(beta);
+  expect(betaPms2.messages.length).toBe(betaPms1.messages.length - 1);
+
+  // Undelete
+  let undeletedPmRes = await deletePrivateMessage(
+    alpha,
+    false,
+    pmRes.message.id
+  );
+  expect(undeletedPmRes.message.deleted).toBe(false);
+
+  let betaPms3 = await listPrivateMessages(beta);
+  expect(betaPms3.messages.length).toBe(betaPms1.messages.length);
+});
diff --git a/ui/src/api_tests/shared.ts b/ui/src/api_tests/shared.ts
new file mode 100644 (file)
index 0000000..710671c
--- /dev/null
@@ -0,0 +1,539 @@
+import {
+  LoginForm,
+  LoginResponse,
+  Post,
+  PostForm,
+  Comment,
+  DeletePostForm,
+  RemovePostForm,
+  StickyPostForm,
+  LockPostForm,
+  PostResponse,
+  SearchResponse,
+  FollowCommunityForm,
+  CommunityResponse,
+  GetFollowedCommunitiesResponse,
+  GetPostResponse,
+  RegisterForm,
+  CommentForm,
+  DeleteCommentForm,
+  RemoveCommentForm,
+  SearchForm,
+  CommentResponse,
+  CommunityForm,
+  DeleteCommunityForm,
+  RemoveCommunityForm,
+  GetUserMentionsForm,
+  CommentLikeForm,
+  CreatePostLikeForm,
+  PrivateMessageForm,
+  EditPrivateMessageForm,
+  DeletePrivateMessageForm,
+  GetFollowedCommunitiesForm,
+  GetPrivateMessagesForm,
+  GetSiteForm,
+  GetPostForm,
+  PrivateMessageResponse,
+  PrivateMessagesResponse,
+  GetUserMentionsResponse,
+  UserSettingsForm,
+  SortType,
+  ListingType,
+  GetSiteResponse,
+  SearchType,
+  LemmyHttp,
+} from 'lemmy-js-client';
+
+export interface API {
+  client: LemmyHttp;
+  auth?: string;
+}
+
+export let alpha: API = {
+  client: new LemmyHttp('http://localhost:8540/api/v1'),
+};
+
+export let beta: API = {
+  client: new LemmyHttp('http://localhost:8550/api/v1'),
+};
+
+export let gamma: API = {
+  client: new LemmyHttp('http://localhost:8560/api/v1'),
+};
+
+export let delta: API = {
+  client: new LemmyHttp('http://localhost:8570/api/v1'),
+};
+
+export let epsilon: API = {
+  client: new LemmyHttp('http://localhost:8580/api/v1'),
+};
+
+export async function setupLogins() {
+  let formAlpha: LoginForm = {
+    username_or_email: 'lemmy_alpha',
+    password: 'lemmy',
+  };
+  let resAlpha = alpha.client.login(formAlpha);
+
+  let formBeta = {
+    username_or_email: 'lemmy_beta',
+    password: 'lemmy',
+  };
+  let resBeta = beta.client.login(formBeta);
+
+  let formGamma = {
+    username_or_email: 'lemmy_gamma',
+    password: 'lemmy',
+  };
+  let resGamma = gamma.client.login(formGamma);
+
+  let formDelta = {
+    username_or_email: 'lemmy_delta',
+    password: 'lemmy',
+  };
+  let resDelta = delta.client.login(formDelta);
+
+  let formEpsilon = {
+    username_or_email: 'lemmy_epsilon',
+    password: 'lemmy',
+  };
+  let resEpsilon = epsilon.client.login(formEpsilon);
+
+  let res = await Promise.all([
+    resAlpha,
+    resBeta,
+    resGamma,
+    resDelta,
+    resEpsilon,
+  ]);
+
+  alpha.auth = res[0].jwt;
+  beta.auth = res[1].jwt;
+  gamma.auth = res[2].jwt;
+  delta.auth = res[3].jwt;
+  epsilon.auth = res[4].jwt;
+}
+
+export async function createPost(
+  api: API,
+  community_id: number
+): Promise<PostResponse> {
+  let name = 'A jest test post';
+  let form: PostForm = {
+    name,
+    auth: api.auth,
+    community_id,
+    nsfw: false,
+  };
+  return api.client.createPost(form);
+}
+
+export async function updatePost(api: API, post: Post): Promise<PostResponse> {
+  let name = 'A jest test federated post, updated';
+  let form: PostForm = {
+    name,
+    edit_id: post.id,
+    auth: api.auth,
+    nsfw: false,
+  };
+  return api.client.editPost(form);
+}
+
+export async function deletePost(
+  api: API,
+  deleted: boolean,
+  post: Post
+): Promise<PostResponse> {
+  let form: DeletePostForm = {
+    edit_id: post.id,
+    deleted: deleted,
+    auth: api.auth,
+  };
+  return api.client.deletePost(form);
+}
+
+export async function removePost(
+  api: API,
+  removed: boolean,
+  post: Post
+): Promise<PostResponse> {
+  let form: RemovePostForm = {
+    edit_id: post.id,
+    removed,
+    auth: api.auth,
+  };
+  return api.client.removePost(form);
+}
+
+export async function stickyPost(
+  api: API,
+  stickied: boolean,
+  post: Post
+): Promise<PostResponse> {
+  let form: StickyPostForm = {
+    edit_id: post.id,
+    stickied,
+    auth: api.auth,
+  };
+  return api.client.stickyPost(form);
+}
+
+export async function lockPost(
+  api: API,
+  locked: boolean,
+  post: Post
+): Promise<PostResponse> {
+  let form: LockPostForm = {
+    edit_id: post.id,
+    locked,
+    auth: api.auth,
+  };
+  return api.client.lockPost(form);
+}
+
+export async function searchPost(
+  api: API,
+  post: Post
+): Promise<SearchResponse> {
+  let form: SearchForm = {
+    q: post.ap_id,
+    type_: SearchType.All,
+    sort: SortType.TopAll,
+  };
+  return api.client.search(form);
+}
+
+export async function getPost(
+  api: API,
+  post_id: number
+): Promise<GetPostResponse> {
+  let form: GetPostForm = {
+    id: post_id,
+  };
+  return api.client.getPost(form);
+}
+
+export async function searchComment(
+  api: API,
+  comment: Comment
+): Promise<SearchResponse> {
+  let form: SearchForm = {
+    q: comment.ap_id,
+    type_: SearchType.All,
+    sort: SortType.TopAll,
+  };
+  return api.client.search(form);
+}
+
+export async function searchForBetaCommunity(
+  api: API
+): Promise<SearchResponse> {
+  // Make sure lemmy-beta/c/main is cached on lemmy_alpha
+  // Use short-hand search url
+  let form: SearchForm = {
+    q: '!main@lemmy-beta:8550',
+    type_: SearchType.All,
+    sort: SortType.TopAll,
+  };
+  return api.client.search(form);
+}
+
+export async function searchForUser(
+  api: API,
+  apShortname: string
+): Promise<SearchResponse> {
+  // Make sure lemmy-beta/c/main is cached on lemmy_alpha
+  // Use short-hand search url
+  let form: SearchForm = {
+    q: apShortname,
+    type_: SearchType.All,
+    sort: SortType.TopAll,
+  };
+  return api.client.search(form);
+}
+
+export async function followCommunity(
+  api: API,
+  follow: boolean,
+  community_id: number
+): Promise<CommunityResponse> {
+  let form: FollowCommunityForm = {
+    community_id,
+    follow,
+    auth: api.auth,
+  };
+  return api.client.followCommunity(form);
+}
+
+export async function checkFollowedCommunities(
+  api: API
+): Promise<GetFollowedCommunitiesResponse> {
+  let form: GetFollowedCommunitiesForm = {
+    auth: api.auth,
+  };
+  return api.client.getFollowedCommunities(form);
+}
+
+export async function likePost(
+  api: API,
+  score: number,
+  post: Post
+): Promise<PostResponse> {
+  let form: CreatePostLikeForm = {
+    post_id: post.id,
+    score: score,
+    auth: api.auth,
+  };
+
+  return api.client.likePost(form);
+}
+
+export async function createComment(
+  api: API,
+  post_id: number,
+  parent_id?: number,
+  content = 'a jest test comment'
+): Promise<CommentResponse> {
+  let form: CommentForm = {
+    content,
+    post_id,
+    parent_id,
+    auth: api.auth,
+  };
+  return api.client.createComment(form);
+}
+
+export async function updateComment(
+  api: API,
+  edit_id: number,
+  content = 'A jest test federated comment update'
+): Promise<CommentResponse> {
+  let form: CommentForm = {
+    content,
+    edit_id,
+    auth: api.auth,
+  };
+  return api.client.editComment(form);
+}
+
+export async function deleteComment(
+  api: API,
+  deleted: boolean,
+  edit_id: number
+): Promise<CommentResponse> {
+  let form: DeleteCommentForm = {
+    edit_id,
+    deleted,
+    auth: api.auth,
+  };
+  return api.client.deleteComment(form);
+}
+
+export async function removeComment(
+  api: API,
+  removed: boolean,
+  edit_id: number
+): Promise<CommentResponse> {
+  let form: RemoveCommentForm = {
+    edit_id,
+    removed,
+    auth: api.auth,
+  };
+  return api.client.removeComment(form);
+}
+
+export async function getMentions(api: API): Promise<GetUserMentionsResponse> {
+  let form: GetUserMentionsForm = {
+    sort: SortType.New,
+    unread_only: false,
+    auth: api.auth,
+  };
+  return api.client.getUserMentions(form);
+}
+
+export async function likeComment(
+  api: API,
+  score: number,
+  comment: Comment
+): Promise<CommentResponse> {
+  let form: CommentLikeForm = {
+    comment_id: comment.id,
+    score,
+    auth: api.auth,
+  };
+  return api.client.likeComment(form);
+}
+
+export async function createCommunity(
+  api: API,
+  name_: string = randomString(5)
+): Promise<CommunityResponse> {
+  let form: CommunityForm = {
+    name: name_,
+    title: name_,
+    category_id: 1,
+    nsfw: false,
+    auth: api.auth,
+  };
+  return api.client.createCommunity(form);
+}
+
+export async function deleteCommunity(
+  api: API,
+  deleted: boolean,
+  edit_id: number
+): Promise<CommunityResponse> {
+  let form: DeleteCommunityForm = {
+    edit_id,
+    deleted,
+    auth: api.auth,
+  };
+  return api.client.deleteCommunity(form);
+}
+
+export async function removeCommunity(
+  api: API,
+  removed: boolean,
+  edit_id: number
+): Promise<CommunityResponse> {
+  let form: RemoveCommunityForm = {
+    edit_id,
+    removed,
+    auth: api.auth,
+  };
+  return api.client.removeCommunity(form);
+}
+
+export async function createPrivateMessage(
+  api: API,
+  recipient_id: number
+): Promise<PrivateMessageResponse> {
+  let content = 'A jest test federated private message';
+  let form: PrivateMessageForm = {
+    content,
+    recipient_id,
+    auth: api.auth,
+  };
+  return api.client.createPrivateMessage(form);
+}
+
+export async function updatePrivateMessage(
+  api: API,
+  edit_id: number
+): Promise<PrivateMessageResponse> {
+  let updatedContent = 'A jest test federated private message edited';
+  let form: EditPrivateMessageForm = {
+    content: updatedContent,
+    edit_id,
+    auth: api.auth,
+  };
+  return api.client.editPrivateMessage(form);
+}
+
+export async function deletePrivateMessage(
+  api: API,
+  deleted: boolean,
+  edit_id: number
+): Promise<PrivateMessageResponse> {
+  let form: DeletePrivateMessageForm = {
+    deleted,
+    edit_id,
+    auth: api.auth,
+  };
+  return api.client.deletePrivateMessage(form);
+}
+
+export async function registerUser(
+  api: API,
+  username: string = randomString(5)
+): Promise<LoginResponse> {
+  let form: RegisterForm = {
+    username,
+    password: 'test',
+    password_verify: 'test',
+    admin: false,
+    show_nsfw: true,
+  };
+  return api.client.register(form);
+}
+
+export async function saveUserSettingsBio(
+  api: API,
+  auth: string
+): Promise<LoginResponse> {
+  let form: UserSettingsForm = {
+    show_nsfw: true,
+    theme: 'darkly',
+    default_sort_type: Object.keys(SortType).indexOf(SortType.Active),
+    default_listing_type: Object.keys(ListingType).indexOf(ListingType.All),
+    lang: 'en',
+    show_avatars: true,
+    send_notifications_to_email: false,
+    bio: 'a changed bio',
+    auth,
+  };
+  return api.client.saveUserSettings(form);
+}
+
+export async function getSite(
+  api: API,
+  auth: string
+): Promise<GetSiteResponse> {
+  let form: GetSiteForm = {
+    auth,
+  };
+  return api.client.getSite(form);
+}
+
+export async function listPrivateMessages(
+  api: API
+): Promise<PrivateMessagesResponse> {
+  let form: GetPrivateMessagesForm = {
+    auth: api.auth,
+    unread_only: false,
+    limit: 999,
+  };
+  return api.client.getPrivateMessages(form);
+}
+
+export async function unfollowRemotes(
+  api: API
+): Promise<GetFollowedCommunitiesResponse> {
+  // Unfollow all remote communities
+  let followed = await checkFollowedCommunities(api);
+  let remoteFollowed = followed.communities.filter(
+    c => c.community_local == false
+  );
+  for (let cu of remoteFollowed) {
+    await followCommunity(api, false, cu.community_id);
+  }
+  let followed2 = await checkFollowedCommunities(api);
+  return followed2;
+}
+
+export async function followBeta(api: API): Promise<CommunityResponse> {
+  await unfollowRemotes(api);
+
+  // Cache it
+  let search = await searchForBetaCommunity(api);
+  let com = search.communities.filter(c => c.local == false);
+  if (com[0]) {
+    let follow = await followCommunity(api, true, com[0].id);
+    return follow;
+  }
+}
+
+export function wrapper(form: any): string {
+  return JSON.stringify(form);
+}
+
+function randomString(length: number): string {
+  var result = '';
+  var characters = 'abcdefghijklmnopqrstuvwxyz0123456789_';
+  var charactersLength = characters.length;
+  for (var i = 0; i < length; i++) {
+    result += characters.charAt(Math.floor(Math.random() * charactersLength));
+  }
+  return result;
+}
diff --git a/ui/src/api_tests/user.spec.ts b/ui/src/api_tests/user.spec.ts
new file mode 100644 (file)
index 0000000..909f9a1
--- /dev/null
@@ -0,0 +1,34 @@
+import {
+  alpha,
+  beta,
+  registerUser,
+  searchForUser,
+  saveUserSettingsBio,
+  getSite,
+} from './shared';
+
+let auth: string;
+let apShortname: string;
+
+test('Create user', async () => {
+  let userRes = await registerUser(alpha);
+  expect(userRes.jwt).toBeDefined();
+  auth = userRes.jwt;
+
+  let site = await getSite(alpha, auth);
+  expect(site.my_user).toBeDefined();
+  apShortname = `@${site.my_user.name}@lemmy-alpha:8540`;
+});
+
+test('Save user settings, check changed bio from beta', async () => {
+  let bio = 'a changed bio';
+  let userRes = await saveUserSettingsBio(alpha, auth);
+  expect(userRes.jwt).toBeDefined();
+
+  let site = await getSite(alpha, auth);
+  expect(site.my_user.bio).toBe(bio);
+
+  // Make sure beta sees this bio is changed
+  let search = await searchForUser(beta, apShortname);
+  expect(search.users[0].bio).toBe(bio);
+});
index 0034c229e9129724937c057f62ff642e2380de4f..a3bfdd81341d6489d1ab69f1742ad911c9d75363 100644 (file)
@@ -1,4 +1,5 @@
 import { Component, linkEvent } from 'inferno';
+import { Helmet } from 'inferno-helmet';
 import { Subscription } from 'rxjs';
 import { retryWhen, delay, take } from 'rxjs/operators';
 import {
@@ -8,7 +9,7 @@ import {
   SiteConfigForm,
   GetSiteConfigResponse,
   WebSocketJsonResponse,
-} from '../interfaces';
+} from 'lemmy-js-client';
 import { WebSocketService } from '../services';
 import { wsJsonToRes, capitalizeFirstLetter, toast, randomStr } from '../utils';
 import autosize from 'autosize';
@@ -46,6 +47,8 @@ export class AdminSettings extends Component<any, AdminSettingsState> {
       admins: [],
       banned: [],
       online: null,
+      version: null,
+      federated_instances: null,
     },
     siteConfigForm: {
       config_hjson: null,
@@ -79,9 +82,18 @@ export class AdminSettings extends Component<any, AdminSettingsState> {
     this.subscription.unsubscribe();
   }
 
+  get documentTitle(): string {
+    if (this.state.siteRes.site.name) {
+      return `${i18n.t('admin_settings')} - ${this.state.siteRes.site.name}`;
+    } else {
+      return 'Lemmy';
+    }
+  }
+
   render() {
     return (
       <div class="container">
+        <Helmet title={this.documentTitle} />
         {this.state.loading ? (
           <h5>
             <svg class="icon icon-spinner spin">
@@ -91,7 +103,9 @@ export class AdminSettings extends Component<any, AdminSettingsState> {
         ) : (
           <div class="row">
             <div class="col-12 col-md-6">
-              <SiteForm site={this.state.siteRes.site} />
+              {this.state.siteRes.site.id && (
+                <SiteForm site={this.state.siteRes.site} />
+              )}
               {this.admins()}
               {this.bannedUsers()}
             </div>
@@ -112,6 +126,7 @@ export class AdminSettings extends Component<any, AdminSettingsState> {
               <UserListing
                 user={{
                   name: admin.name,
+                  preferred_username: admin.preferred_username,
                   avatar: admin.avatar,
                   id: admin.id,
                   local: admin.local,
@@ -135,6 +150,7 @@ export class AdminSettings extends Component<any, AdminSettingsState> {
               <UserListing
                 user={{
                   name: banned.name,
+                  preferred_username: banned.preferred_username,
                   avatar: banned.avatar,
                   id: banned.id,
                   local: banned.local,
@@ -219,9 +235,6 @@ export class AdminSettings extends Component<any, AdminSettingsState> {
       }
       this.state.siteRes = data;
       this.setState(this.state);
-      document.title = `${i18n.t('admin_settings')} - ${
-        this.state.siteRes.site.name
-      }`;
     } else if (res.op == UserOperation.EditSite) {
       let data = res.data as SiteResponse;
       this.state.siteRes.site = data.site;
diff --git a/ui/src/components/banner-icon-header.tsx b/ui/src/components/banner-icon-header.tsx
new file mode 100644 (file)
index 0000000..8c0eedb
--- /dev/null
@@ -0,0 +1,30 @@
+import { Component } from 'inferno';
+
+interface BannerIconHeaderProps {
+  banner?: string;
+  icon?: string;
+}
+
+export class BannerIconHeader extends Component<BannerIconHeaderProps, any> {
+  constructor(props: any, context: any) {
+    super(props, context);
+  }
+
+  render() {
+    return (
+      <div class="position-relative mb-2">
+        {this.props.banner && (
+          <img src={this.props.banner} class="banner img-fluid" />
+        )}
+        {this.props.icon && (
+          <img
+            src={this.props.icon}
+            className={`ml-2 mb-0 ${
+              this.props.banner ? 'avatar-pushup' : ''
+            } rounded-circle avatar-overlay`}
+          />
+        )}
+      </div>
+    );
+  }
+}
index 72a4f398b5a6bf469cb521abaeb09e1ab268f032..dbd14dc76adc1ff82939d23f10714a2a60970331 100644 (file)
@@ -1,31 +1,19 @@
-import { Component, linkEvent } from 'inferno';
+import { Component } from 'inferno';
 import { Link } from 'inferno-router';
 import { Subscription } from 'rxjs';
 import { retryWhen, delay, take } from 'rxjs/operators';
-import { Prompt } from 'inferno-router';
 import {
   CommentNode as CommentNodeI,
   CommentForm as CommentFormI,
   WebSocketJsonResponse,
   UserOperation,
   CommentResponse,
-} from '../interfaces';
-import {
-  capitalizeFirstLetter,
-  mdToHtml,
-  randomStr,
-  markdownHelpUrl,
-  toast,
-  setupTribute,
-  wsJsonToRes,
-  pictrsDeleteToast,
-} from '../utils';
+} from 'lemmy-js-client';
+import { capitalizeFirstLetter, wsJsonToRes } from '../utils';
 import { WebSocketService, UserService } from '../services';
-import autosize from 'autosize';
-import Tribute from 'tributejs/src/Tribute.js';
-import emojiShortName from 'emoji-short-name';
 import { i18n } from '../i18next';
 import { T } from 'inferno-i18next';
+import { MarkdownTextArea } from './markdown-textarea';
 
 interface CommentFormProps {
   postId?: number;
@@ -39,15 +27,10 @@ interface CommentFormProps {
 interface CommentFormState {
   commentForm: CommentFormI;
   buttonTitle: string;
-  previewMode: boolean;
-  loading: boolean;
-  imageLoading: boolean;
+  finished: boolean;
 }
 
 export class CommentForm extends Component<CommentFormProps, CommentFormState> {
-  private id = `comment-textarea-${randomStr()}`;
-  private formId = `comment-form-${randomStr()}`;
-  private tribute: Tribute;
   private subscription: Subscription;
   private emptyState: CommentFormState = {
     commentForm: {
@@ -65,15 +48,14 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
       : this.props.edit
       ? capitalizeFirstLetter(i18n.t('save'))
       : capitalizeFirstLetter(i18n.t('reply')),
-    previewMode: false,
-    loading: false,
-    imageLoading: false,
+    finished: false,
   };
 
   constructor(props: any, context: any) {
     super(props, context);
 
-    this.tribute = setupTribute();
+    this.handleCommentSubmit = this.handleCommentSubmit.bind(this);
+    this.handleReplyCancel = this.handleReplyCancel.bind(this);
 
     this.state = this.emptyState;
 
@@ -98,160 +80,24 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
       );
   }
 
-  componentDidMount() {
-    let textarea: any = document.getElementById(this.id);
-    if (textarea) {
-      autosize(textarea);
-      this.tribute.attach(textarea);
-      textarea.addEventListener('tribute-replaced', () => {
-        this.state.commentForm.content = textarea.value;
-        this.setState(this.state);
-        autosize.update(textarea);
-      });
-
-      // Quoting of selected text
-      let selectedText = window.getSelection().toString();
-      if (selectedText) {
-        let quotedText =
-          selectedText
-            .split('\n')
-            .map(t => `> ${t}`)
-            .join('\n') + '\n\n';
-        this.state.commentForm.content = quotedText;
-        this.setState(this.state);
-        // Not sure why this needs a delay
-        setTimeout(() => autosize.update(textarea), 10);
-      }
-
-      if (this.props.focus) {
-        textarea.focus();
-      }
-    }
-  }
-
-  componentDidUpdate() {
-    if (this.state.commentForm.content) {
-      window.onbeforeunload = () => true;
-    } else {
-      window.onbeforeunload = undefined;
-    }
-  }
-
   componentWillUnmount() {
     this.subscription.unsubscribe();
-    window.onbeforeunload = null;
   }
 
   render() {
     return (
       <div class="mb-3">
-        <Prompt
-          when={this.state.commentForm.content}
-          message={i18n.t('block_leaving')}
-        />
         {UserService.Instance.user ? (
-          <form
-            id={this.formId}
-            onSubmit={linkEvent(this, this.handleCommentSubmit)}
-          >
-            <div class="form-group row">
-              <div className={`col-sm-12`}>
-                <textarea
-                  id={this.id}
-                  className={`form-control ${
-                    this.state.previewMode && 'd-none'
-                  }`}
-                  value={this.state.commentForm.content}
-                  onInput={linkEvent(this, this.handleCommentContentChange)}
-                  onPaste={linkEvent(this, this.handleImageUploadPaste)}
-                  required
-                  disabled={this.props.disabled}
-                  rows={2}
-                  maxLength={10000}
-                />
-                {this.state.previewMode && (
-                  <div
-                    className="card card-body md-div"
-                    dangerouslySetInnerHTML={mdToHtml(
-                      this.state.commentForm.content
-                    )}
-                  />
-                )}
-              </div>
-            </div>
-            <div class="row">
-              <div class="col-sm-12">
-                <button
-                  type="submit"
-                  class="btn btn-sm btn-secondary mr-2"
-                  disabled={this.props.disabled || this.state.loading}
-                >
-                  {this.state.loading ? (
-                    <svg class="icon icon-spinner spin">
-                      <use xlinkHref="#icon-spinner"></use>
-                    </svg>
-                  ) : (
-                    <span>{this.state.buttonTitle}</span>
-                  )}
-                </button>
-                {this.state.commentForm.content && (
-                  <button
-                    className={`btn btn-sm mr-2 btn-secondary ${
-                      this.state.previewMode && 'active'
-                    }`}
-                    onClick={linkEvent(this, this.handlePreviewToggle)}
-                  >
-                    {i18n.t('preview')}
-                  </button>
-                )}
-                {this.props.node && (
-                  <button
-                    type="button"
-                    class="btn btn-sm btn-secondary mr-2"
-                    onClick={linkEvent(this, this.handleReplyCancel)}
-                  >
-                    {i18n.t('cancel')}
-                  </button>
-                )}
-                <a
-                  href={markdownHelpUrl}
-                  target="_blank"
-                  class="d-inline-block float-right text-muted font-weight-bold"
-                  title={i18n.t('formatting_help')}
-                  rel="noopener"
-                >
-                  <svg class="icon icon-inline">
-                    <use xlinkHref="#icon-help-circle"></use>
-                  </svg>
-                </a>
-                <form class="d-inline-block mr-3 float-right text-muted font-weight-bold">
-                  <label
-                    htmlFor={`file-upload-${this.id}`}
-                    className={`${UserService.Instance.user && 'pointer'}`}
-                    data-tippy-content={i18n.t('upload_image')}
-                  >
-                    <svg class="icon icon-inline">
-                      <use xlinkHref="#icon-image"></use>
-                    </svg>
-                  </label>
-                  <input
-                    id={`file-upload-${this.id}`}
-                    type="file"
-                    accept="image/*,video/*"
-                    name="file"
-                    class="d-none"
-                    disabled={!UserService.Instance.user}
-                    onChange={linkEvent(this, this.handleImageUpload)}
-                  />
-                </form>
-                {this.state.imageLoading && (
-                  <svg class="icon icon-spinner spin">
-                    <use xlinkHref="#icon-spinner"></use>
-                  </svg>
-                )}
-              </div>
-            </div>
-          </form>
+          <MarkdownTextArea
+            initialContent={this.state.commentForm.content}
+            buttonTitle={this.state.buttonTitle}
+            finished={this.state.finished}
+            replyType={!!this.props.node}
+            focus={this.props.focus}
+            disabled={this.props.disabled}
+            onSubmit={this.handleCommentSubmit}
+            onReplyCancel={this.handleReplyCancel}
+          />
         ) : (
           <div class="alert alert-light" role="alert">
             <svg class="icon icon-inline mr-2">
@@ -269,128 +115,19 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
     );
   }
 
-  handleFinished(op: UserOperation, 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 &&
-        ((op == UserOperation.CreateComment &&
-          // 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))) ||
-      // If its a comment edit, only check that its from your user, and that its a
-      // text edit only
-
-      (data.comment.creator_id == UserService.Instance.user.id &&
-        op == UserOperation.EditComment &&
-        data.comment.content)
-    ) {
-      this.state.previewMode = false;
-      this.state.loading = false;
-      this.state.commentForm.content = '';
-      this.setState(this.state);
-      let form: any = document.getElementById(this.formId);
-      form.reset();
-      if (this.props.node) {
-        this.props.onReplyCancel();
-      }
-      autosize.update(form);
-      this.setState(this.state);
-    }
-  }
-
-  handleCommentSubmit(i: CommentForm, event: any) {
-    event.preventDefault();
-    if (i.props.edit) {
-      WebSocketService.Instance.editComment(i.state.commentForm);
+  handleCommentSubmit(msg: { val: string; formId: string }) {
+    this.state.commentForm.content = msg.val;
+    this.state.commentForm.form_id = msg.formId;
+    if (this.props.edit) {
+      WebSocketService.Instance.editComment(this.state.commentForm);
     } else {
-      WebSocketService.Instance.createComment(i.state.commentForm);
-    }
-
-    i.state.loading = true;
-    i.setState(i.state);
-  }
-
-  handleCommentContentChange(i: CommentForm, event: any) {
-    i.state.commentForm.content = event.target.value;
-    i.setState(i.state);
-  }
-
-  handlePreviewToggle(i: CommentForm, event: any) {
-    event.preventDefault();
-    i.state.previewMode = !i.state.previewMode;
-    i.setState(i.state);
-  }
-
-  handleReplyCancel(i: CommentForm) {
-    i.props.onReplyCancel();
-  }
-
-  handleImageUploadPaste(i: CommentForm, event: any) {
-    let image = event.clipboardData.files[0];
-    if (image) {
-      i.handleImageUpload(i, image);
+      WebSocketService.Instance.createComment(this.state.commentForm);
     }
+    this.setState(this.state);
   }
 
-  handleImageUpload(i: CommentForm, event: any) {
-    let file: any;
-    if (event.target) {
-      event.preventDefault();
-      file = event.target.files[0];
-    } else {
-      file = event;
-    }
-
-    const imageUploadUrl = `/pictrs/image`;
-    const formData = new FormData();
-    formData.append('images[]', file);
-
-    i.state.imageLoading = true;
-    i.setState(i.state);
-
-    fetch(imageUploadUrl, {
-      method: 'POST',
-      body: formData,
-    })
-      .then(res => res.json())
-      .then(res => {
-        console.log('pictrs upload:');
-        console.log(res);
-        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;
-          content = content ? `${content}\n${imageMarkdown}` : imageMarkdown;
-          i.state.commentForm.content = content;
-          i.state.imageLoading = false;
-          i.setState(i.state);
-          let textarea: any = document.getElementById(i.id);
-          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 => {
-        i.state.imageLoading = false;
-        i.setState(i.state);
-        toast(error, 'danger');
-      });
+  handleReplyCancel() {
+    this.props.onReplyCancel();
   }
 
   parseMessage(msg: WebSocketJsonResponse) {
@@ -398,12 +135,19 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
 
     // Only do the showing and hiding if logged in
     if (UserService.Instance.user) {
-      if (res.op == UserOperation.CreateComment) {
+      if (
+        res.op == UserOperation.CreateComment ||
+        res.op == UserOperation.EditComment
+      ) {
         let data = res.data as CommentResponse;
-        this.handleFinished(res.op, data);
-      } else if (res.op == UserOperation.EditComment) {
-        let data = res.data as CommentResponse;
-        this.handleFinished(res.op, data);
+
+        // This only finishes this form, if the randomly generated form_id matches the one received
+        if (this.state.commentForm.form_id == data.form_id) {
+          this.setState({ finished: true });
+
+          // Necessary because it broke tribute for some reaso
+          this.setState({ finished: false });
+        }
       }
     }
   }
index a6b9b7bac1921d888575d0be21fecf65ea9383f0..1992c4fc846bb9c6d4b1b656c5d8cd90aac2eda1 100644 (file)
@@ -3,8 +3,10 @@ import { Link } from 'inferno-router';
 import {
   CommentNode as CommentNodeI,
   CommentLikeForm,
-  CommentForm as CommentFormI,
-  EditUserMentionForm,
+  DeleteCommentForm,
+  RemoveCommentForm,
+  MarkCommentAsReadForm,
+  MarkUserMentionAsReadForm,
   SaveCommentForm,
   BanFromCommunityForm,
   BanUserForm,
@@ -14,10 +16,9 @@ import {
   AddAdminForm,
   TransferCommunityForm,
   TransferSiteForm,
-  BanType,
-  CommentSortType,
   SortType,
-} from '../interfaces';
+} from 'lemmy-js-client';
+import { CommentSortType, BanType } from '../interfaces';
 import { WebSocketService, UserService } from '../services';
 import {
   mdToHtml,
@@ -41,6 +42,7 @@ interface CommentNodeState {
   showRemoveDialog: boolean;
   removeReason: string;
   showBanDialog: boolean;
+  removeData: boolean;
   banReason: string;
   banExpires: string;
   banType: BanType;
@@ -62,6 +64,7 @@ interface CommentNodeState {
 
 interface CommentNodeProps {
   node: CommentNodeI;
+  noBorder?: boolean;
   noIndent?: boolean;
   viewOnly?: boolean;
   locked?: boolean;
@@ -84,6 +87,7 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
     showRemoveDialog: false,
     removeReason: null,
     showBanDialog: false,
+    removeData: null,
     banReason: null,
     banExpires: null,
     banType: BanType.Community,
@@ -134,9 +138,9 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
       >
         <div
           id={`comment-${node.comment.id}`}
-          className={`details comment-node border-top border-light py-2 ${
-            this.isCommentNew ? 'mark' : ''
-          }`}
+          className={`details comment-node py-2 ${
+            !this.props.noBorder ? 'border-top border-light' : ''
+          } ${this.isCommentNew ? 'mark' : ''}`}
           style={
             !this.props.noIndent &&
             this.props.node.comment.parent_id &&
@@ -155,6 +159,7 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
                 <UserListing
                   user={{
                     name: node.comment.creator_name,
+                    preferred_username: node.comment.creator_preferred_username,
                     avatar: node.comment.creator_avatar,
                     id: node.comment.creator_id,
                     local: node.comment.creator_local,
@@ -193,6 +198,7 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
                       id: node.comment.community_id,
                       local: node.comment.community_local,
                       actor_id: node.comment.community_actor_id,
+                      icon: node.comment.community_icon,
                     }}
                   />
                   <span class="mx-2">•</span>
@@ -202,7 +208,7 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
                 </>
               )}
               <button
-                class="btn btn-sm text-muted"
+                class="btn text-muted"
                 onClick={linkEvent(this, this.handleCommentCollapse)}
               >
                 {this.state.collapsed ? (
@@ -218,7 +224,7 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
               {/* This is an expanding spacer for mobile */}
               <div className="mr-lg-4 flex-grow-1 flex-lg-grow-0 unselectable pointer mx-2"></div>
               <button
-                className={`btn btn-sm p-0 unselectable pointer ${this.scoreColor}`}
+                className={`btn p-0 unselectable pointer ${this.scoreColor}`}
                 onClick={linkEvent(node, this.handleCommentUpvote)}
                 data-tippy-content={this.pointsTippy}
               >
@@ -470,6 +476,7 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
                                   </button>
                                 ))}
                               {!node.comment.banned_from_community &&
+                                node.comment.creator_local &&
                                 (!this.state.showConfirmAppointAsMod ? (
                                   <button
                                     class="btn btn-link btn-animate text-muted"
@@ -512,6 +519,7 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
                           {/* Community creators and admins can transfer community to another mod */}
                           {(this.amCommunityCreator || this.canAdmin) &&
                             this.isMod &&
+                            node.comment.creator_local &&
                             (!this.state.showConfirmTransferCommunity ? (
                               <button
                                 class="btn btn-link btn-animate text-muted"
@@ -574,6 +582,7 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
                                   </button>
                                 ))}
                               {!node.comment.banned &&
+                                node.comment.creator_local &&
                                 (!this.state.showConfirmAppointAsAdmin ? (
                                   <button
                                     class="btn btn-link btn-animate text-muted"
@@ -616,6 +625,7 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
                           {/* Site Creator can transfer to another admin */}
                           {this.amSiteCreator &&
                             this.isAdmin &&
+                            node.comment.creator_local &&
                             (!this.state.showConfirmTransferSite ? (
                               <button
                                 class="btn btn-link btn-animate text-muted"
@@ -690,6 +700,20 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
                 value={this.state.banReason}
                 onInput={linkEvent(this, this.handleModBanReasonChange)}
               />
+              <div class="form-group">
+                <div class="form-check">
+                  <input
+                    class="form-check-input"
+                    id="mod-ban-remove-data"
+                    type="checkbox"
+                    checked={this.state.removeData}
+                    onChange={linkEvent(this, this.handleModRemoveDataChange)}
+                  />
+                  <label class="form-check-label" htmlFor="mod-ban-remove-data">
+                    {i18n.t('remove_posts_comments')}
+                  </label>
+                </div>
+              </div>
             </div>
             {/* TODO hold off on expires until later */}
             {/* <div class="form-group row"> */}
@@ -848,16 +872,12 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
   }
 
   handleDeleteClick(i: CommentNode) {
-    let deleteForm: CommentFormI = {
-      content: i.props.node.comment.content,
+    let deleteForm: DeleteCommentForm = {
       edit_id: i.props.node.comment.id,
-      creator_id: i.props.node.comment.creator_id,
-      post_id: i.props.node.comment.post_id,
-      parent_id: i.props.node.comment.parent_id,
       deleted: !i.props.node.comment.deleted,
       auth: null,
     };
-    WebSocketService.Instance.editComment(deleteForm);
+    WebSocketService.Instance.deleteComment(deleteForm);
   }
 
   handleSaveCommentClick(i: CommentNode) {
@@ -901,7 +921,6 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
 
     let form: CommentLikeForm = {
       comment_id: i.comment.id,
-      post_id: i.comment.post_id,
       score: this.state.my_vote,
     };
 
@@ -929,7 +948,6 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
 
     let form: CommentLikeForm = {
       comment_id: i.comment.id,
-      post_id: i.comment.post_id,
       score: this.state.my_vote,
     };
 
@@ -948,19 +966,20 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
     i.setState(i.state);
   }
 
+  handleModRemoveDataChange(i: CommentNode, event: any) {
+    i.state.removeData = event.target.checked;
+    i.setState(i.state);
+  }
+
   handleModRemoveSubmit(i: CommentNode) {
     event.preventDefault();
-    let form: CommentFormI = {
-      content: i.props.node.comment.content,
+    let form: RemoveCommentForm = {
       edit_id: i.props.node.comment.id,
-      creator_id: i.props.node.comment.creator_id,
-      post_id: i.props.node.comment.post_id,
-      parent_id: i.props.node.comment.parent_id,
       removed: !i.props.node.comment.removed,
       reason: i.state.removeReason,
       auth: null,
     };
-    WebSocketService.Instance.editComment(form);
+    WebSocketService.Instance.removeComment(form);
 
     i.state.showRemoveDialog = false;
     i.setState(i.state);
@@ -969,22 +988,18 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
   handleMarkRead(i: CommentNode) {
     // if it has a user_mention_id field, then its a mention
     if (i.props.node.comment.user_mention_id) {
-      let form: EditUserMentionForm = {
+      let form: MarkUserMentionAsReadForm = {
         user_mention_id: i.props.node.comment.user_mention_id,
         read: !i.props.node.comment.read,
       };
-      WebSocketService.Instance.editUserMention(form);
+      WebSocketService.Instance.markUserMentionAsRead(form);
     } else {
-      let form: CommentFormI = {
-        content: i.props.node.comment.content,
+      let form: MarkCommentAsReadForm = {
         edit_id: i.props.node.comment.id,
-        creator_id: i.props.node.comment.creator_id,
-        post_id: i.props.node.comment.post_id,
-        parent_id: i.props.node.comment.parent_id,
         read: !i.props.node.comment.read,
         auth: null,
       };
-      WebSocketService.Instance.editComment(form);
+      WebSocketService.Instance.markCommentAsRead(form);
     }
 
     i.state.readLoading = true;
@@ -1029,18 +1044,30 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
     event.preventDefault();
 
     if (i.state.banType == BanType.Community) {
+      // If its an unban, restore all their data
+      let ban = !i.props.node.comment.banned_from_community;
+      if (ban == false) {
+        i.state.removeData = false;
+      }
       let form: BanFromCommunityForm = {
         user_id: i.props.node.comment.creator_id,
         community_id: i.props.node.comment.community_id,
-        ban: !i.props.node.comment.banned_from_community,
+        ban,
+        remove_data: i.state.removeData,
         reason: i.state.banReason,
         expires: getUnixTime(i.state.banExpires),
       };
       WebSocketService.Instance.banFromCommunity(form);
     } else {
+      // If its an unban, restore all their data
+      let ban = !i.props.node.comment.banned;
+      if (ban == false) {
+        i.state.removeData = false;
+      }
       let form: BanUserForm = {
         user_id: i.props.node.comment.creator_id,
-        ban: !i.props.node.comment.banned,
+        ban,
+        remove_data: i.state.removeData,
         reason: i.state.banReason,
         expires: getUnixTime(i.state.banExpires),
       };
index bd5ec20bd997b355b670f45d37f672774650e7bd..bdb8a545e589ceacd1d305ef9445a137c891955e 100644 (file)
@@ -1,11 +1,11 @@
 import { Component } from 'inferno';
+import { CommentSortType } from '../interfaces';
 import {
   CommentNode as CommentNodeI,
   CommunityUser,
   UserView,
-  CommentSortType,
   SortType,
-} from '../interfaces';
+} from 'lemmy-js-client';
 import { commentSort, commentSortSortType } from '../utils';
 import { CommentNode } from './comment-node';
 
@@ -16,6 +16,7 @@ interface CommentNodesProps {
   moderators?: Array<CommunityUser>;
   admins?: Array<UserView>;
   postCreatorId?: number;
+  noBorder?: boolean;
   noIndent?: boolean;
   viewOnly?: boolean;
   locked?: boolean;
@@ -42,6 +43,7 @@ export class CommentNodes extends Component<
           <CommentNode
             key={node.comment.id}
             node={node}
+            noBorder={this.props.noBorder}
             noIndent={this.props.noIndent}
             viewOnly={this.props.viewOnly}
             locked={this.props.locked}
index ba362accdba1ae07e891ec5aa93dd5072b01c26b..5be032c5e47f3790742cca62acc0df0ee9d3548e 100644 (file)
@@ -1,4 +1,5 @@
 import { Component, linkEvent } from 'inferno';
+import { Helmet } from 'inferno-helmet';
 import { Subscription } from 'rxjs';
 import { retryWhen, delay, take } from 'rxjs/operators';
 import {
@@ -11,7 +12,8 @@ import {
   SortType,
   WebSocketJsonResponse,
   GetSiteResponse,
-} from '../interfaces';
+  Site,
+} from 'lemmy-js-client';
 import { WebSocketService } from '../services';
 import { wsJsonToRes, toast, getPageFromProps } from '../utils';
 import { CommunityLink } from './community-link';
@@ -25,6 +27,7 @@ interface CommunitiesState {
   communities: Array<Community>;
   page: number;
   loading: boolean;
+  site: Site;
 }
 
 interface CommunitiesProps {
@@ -37,6 +40,7 @@ export class Communities extends Component<any, CommunitiesState> {
     communities: [],
     loading: true,
     page: getPageFromProps(this.props),
+    site: undefined,
   };
 
   constructor(props: any, context: any) {
@@ -71,9 +75,18 @@ export class Communities extends Component<any, CommunitiesState> {
     }
   }
 
+  get documentTitle(): string {
+    if (this.state.site) {
+      return `${i18n.t('communities')} - ${this.state.site.name}`;
+    } else {
+      return 'Lemmy';
+    }
+  }
+
   render() {
     return (
       <div class="container">
+        <Helmet title={this.documentTitle} />
         {this.state.loading ? (
           <h5 class="">
             <svg class="icon icon-spinner spin">
@@ -88,7 +101,6 @@ export class Communities extends Component<any, CommunitiesState> {
                 <thead class="pointer">
                   <tr>
                     <th>{i18n.t('name')}</th>
-                    <th class="d-none d-lg-table-cell">{i18n.t('title')}</th>
                     <th>{i18n.t('category')}</th>
                     <th class="text-right">{i18n.t('subscribers')}</th>
                     <th class="text-right d-none d-lg-table-cell">
@@ -106,7 +118,6 @@ export class Communities extends Component<any, CommunitiesState> {
                       <td>
                         <CommunityLink community={community} />
                       </td>
-                      <td class="d-none d-lg-table-cell">{community.title}</td>
                       <td>{community.category_name}</td>
                       <td class="text-right">
                         {community.number_of_subscribers}
@@ -157,7 +168,7 @@ export class Communities extends Component<any, CommunitiesState> {
       <div class="mt-2">
         {this.state.page > 1 && (
           <button
-            class="btn btn-sm btn-secondary mr-1"
+            class="btn btn-secondary mr-1"
             onClick={linkEvent(this, this.prevPage)}
           >
             {i18n.t('prev')}
@@ -166,7 +177,7 @@ export class Communities extends Component<any, CommunitiesState> {
 
         {this.state.communities.length > 0 && (
           <button
-            class="btn btn-sm btn-secondary"
+            class="btn btn-secondary"
             onClick={linkEvent(this, this.nextPage)}
           >
             {i18n.t('next')}
@@ -207,7 +218,7 @@ export class Communities extends Component<any, CommunitiesState> {
 
   refetch() {
     let listCommunitiesForm: ListCommunitiesForm = {
-      sort: SortType[SortType.TopAll],
+      sort: SortType.TopAll,
       limit: communityLimit,
       page: this.state.page,
     };
@@ -240,7 +251,8 @@ export class Communities extends Component<any, CommunitiesState> {
       this.setState(this.state);
     } else if (res.op == UserOperation.GetSite) {
       let data = res.data as GetSiteResponse;
-      document.title = `${i18n.t('communities')} - ${data.site.name}`;
+      this.state.site = data.site;
+      this.setState(this.state);
     }
   }
 }
index 95d9c1f745958e9ddac65572681eac63568a4a55..7b8c379ba043a9ccd617f359c28fe6fb06e6379f 100644 (file)
@@ -9,20 +9,14 @@ import {
   ListCategoriesResponse,
   CommunityResponse,
   WebSocketJsonResponse,
-} from '../interfaces';
+  Community,
+} from 'lemmy-js-client';
 import { WebSocketService } from '../services';
-import {
-  wsJsonToRes,
-  capitalizeFirstLetter,
-  toast,
-  randomStr,
-  setupTribute,
-} from '../utils';
-import Tribute from 'tributejs/src/Tribute.js';
-import autosize from 'autosize';
+import { wsJsonToRes, capitalizeFirstLetter, toast, randomStr } from '../utils';
 import { i18n } from '../i18next';
 
-import { Community } from '../interfaces';
+import { MarkdownTextArea } from './markdown-textarea';
+import { ImageUploadForm } from './image-upload-form';
 
 interface CommunityFormProps {
   community?: Community; // If a community is given, that means this is an edit
@@ -43,7 +37,6 @@ export class CommunityForm extends Component<
   CommunityFormState
 > {
   private id = `community-form-${randomStr()}`;
-  private tribute: Tribute;
   private subscription: Subscription;
 
   private emptyState: CommunityFormState = {
@@ -52,6 +45,8 @@ export class CommunityForm extends Component<
       title: null,
       category_id: null,
       nsfw: false,
+      icon: null,
+      banner: null,
     },
     categories: [],
     loading: false,
@@ -60,9 +55,18 @@ export class CommunityForm extends Component<
   constructor(props: any, context: any) {
     super(props, context);
 
-    this.tribute = setupTribute();
     this.state = this.emptyState;
 
+    this.handleCommunityDescriptionChange = this.handleCommunityDescriptionChange.bind(
+      this
+    );
+
+    this.handleIconUpload = this.handleIconUpload.bind(this);
+    this.handleIconRemove = this.handleIconRemove.bind(this);
+
+    this.handleBannerUpload = this.handleBannerUpload.bind(this);
+    this.handleBannerRemove = this.handleBannerRemove.bind(this);
+
     if (this.props.community) {
       this.state.communityForm = {
         name: this.props.community.name,
@@ -71,6 +75,8 @@ export class CommunityForm extends Component<
         description: this.props.community.description,
         edit_id: this.props.community.id,
         nsfw: this.props.community.nsfw,
+        icon: this.props.community.icon,
+        banner: this.props.community.banner,
         auth: null,
       };
     }
@@ -86,17 +92,6 @@ export class CommunityForm extends Component<
     WebSocketService.Instance.listCategories();
   }
 
-  componentDidMount() {
-    var textarea: any = document.getElementById(this.id);
-    autosize(textarea);
-    this.tribute.attach(textarea);
-    textarea.addEventListener('tribute-replaced', () => {
-      this.state.communityForm.description = textarea.value;
-      this.setState(this.state);
-      autosize.update(textarea);
-    });
-  }
-
   componentDidUpdate() {
     if (
       !this.state.loading &&
@@ -128,29 +123,46 @@ export class CommunityForm extends Component<
           message={i18n.t('block_leaving')}
         />
         <form onSubmit={linkEvent(this, this.handleCreateCommunitySubmit)}>
-          <div class="form-group row">
-            <label class="col-12 col-form-label" htmlFor="community-name">
-              {i18n.t('name')}
-            </label>
-            <div class="col-12">
-              <input
-                type="text"
-                id="community-name"
-                class="form-control"
-                value={this.state.communityForm.name}
-                onInput={linkEvent(this, this.handleCommunityNameChange)}
-                required
-                minLength={3}
-                maxLength={20}
-                pattern="[a-z0-9_]+"
-                title={i18n.t('community_reqs')}
-              />
+          {!this.props.community && (
+            <div class="form-group row">
+              <label class="col-12 col-form-label" htmlFor="community-name">
+                {i18n.t('name')}
+                <span
+                  class="pointer unselectable ml-2 text-muted"
+                  data-tippy-content={i18n.t('name_explain')}
+                >
+                  <svg class="icon icon-inline">
+                    <use xlinkHref="#icon-help-circle"></use>
+                  </svg>
+                </span>
+              </label>
+              <div class="col-12">
+                <input
+                  type="text"
+                  id="community-name"
+                  class="form-control"
+                  value={this.state.communityForm.name}
+                  onInput={linkEvent(this, this.handleCommunityNameChange)}
+                  required
+                  minLength={3}
+                  maxLength={20}
+                  pattern="[a-z0-9_]+"
+                  title={i18n.t('community_reqs')}
+                />
+              </div>
             </div>
-          </div>
-
+          )}
           <div class="form-group row">
             <label class="col-12 col-form-label" htmlFor="community-title">
-              {i18n.t('title')}
+              {i18n.t('display_name')}
+              <span
+                class="pointer unselectable ml-2 text-muted"
+                data-tippy-content={i18n.t('display_name_explain')}
+              >
+                <svg class="icon icon-inline">
+                  <use xlinkHref="#icon-help-circle"></use>
+                </svg>
+              </span>
             </label>
             <div class="col-12">
               <input
@@ -165,18 +177,33 @@ export class CommunityForm extends Component<
               />
             </div>
           </div>
+          <div class="form-group">
+            <label>{i18n.t('icon')}</label>
+            <ImageUploadForm
+              uploadTitle={i18n.t('upload_icon')}
+              imageSrc={this.state.communityForm.icon}
+              onUpload={this.handleIconUpload}
+              onRemove={this.handleIconRemove}
+              rounded
+            />
+          </div>
+          <div class="form-group">
+            <label>{i18n.t('banner')}</label>
+            <ImageUploadForm
+              uploadTitle={i18n.t('upload_banner')}
+              imageSrc={this.state.communityForm.banner}
+              onUpload={this.handleBannerUpload}
+              onRemove={this.handleBannerRemove}
+            />
+          </div>
           <div class="form-group row">
             <label class="col-12 col-form-label" htmlFor={this.id}>
               {i18n.t('sidebar')}
             </label>
             <div class="col-12">
-              <textarea
-                id={this.id}
-                value={this.state.communityForm.description}
-                onInput={linkEvent(this, this.handleCommunityDescriptionChange)}
-                class="form-control"
-                rows={3}
-                maxLength={10000}
+              <MarkdownTextArea
+                initialContent={this.state.communityForm.description}
+                onContentChange={this.handleCommunityDescriptionChange}
               />
             </div>
           </div>
@@ -270,9 +297,9 @@ export class CommunityForm extends Component<
     i.setState(i.state);
   }
 
-  handleCommunityDescriptionChange(i: CommunityForm, event: any) {
-    i.state.communityForm.description = event.target.value;
-    i.setState(i.state);
+  handleCommunityDescriptionChange(val: string) {
+    this.state.communityForm.description = val;
+    this.setState(this.state);
   }
 
   handleCommunityCategoryChange(i: CommunityForm, event: any) {
@@ -289,6 +316,26 @@ export class CommunityForm extends Component<
     i.props.onCancel();
   }
 
+  handleIconUpload(url: string) {
+    this.state.communityForm.icon = url;
+    this.setState(this.state);
+  }
+
+  handleIconRemove() {
+    this.state.communityForm.icon = '';
+    this.setState(this.state);
+  }
+
+  handleBannerUpload(url: string) {
+    this.state.communityForm.banner = url;
+    this.setState(this.state);
+  }
+
+  handleBannerRemove() {
+    this.state.communityForm.banner = '';
+    this.setState(this.state);
+  }
+
   parseMessage(msg: WebSocketJsonResponse) {
     let res = wsJsonToRes(msg);
     console.log(msg);
@@ -308,9 +355,7 @@ export class CommunityForm extends Component<
       let data = res.data as CommunityResponse;
       this.state.loading = false;
       this.props.onCreate(data.community);
-    }
-    // TODO is this necessary
-    else if (res.op == UserOperation.EditCommunity) {
+    } else if (res.op == UserOperation.EditCommunity) {
       let data = res.data as CommunityResponse;
       this.state.loading = false;
       this.props.onEdit(data.community);
index eb55400e159c422b49c481a46a639a3fd558781c..003f61e14464b357b97500ef40315a453ed50f7b 100644 (file)
@@ -1,11 +1,12 @@
 import { Component } from 'inferno';
 import { Link } from 'inferno-router';
-import { Community } from '../interfaces';
-import { hostname } from '../utils';
+import { Community } from 'lemmy-js-client';
+import { hostname, pictrsAvatarThumbnail, showAvatars } from '../utils';
 
 interface CommunityOther {
   name: string;
   id?: number; // Necessary if its federated
+  icon?: string;
   local?: boolean;
   actor_id?: string;
 }
@@ -13,6 +14,9 @@ interface CommunityOther {
 interface CommunityLinkProps {
   community: Community | CommunityOther;
   realLink?: boolean;
+  useApubName?: boolean;
+  muted?: boolean;
+  hideAvatar?: boolean;
 }
 
 export class CommunityLink extends Component<CommunityLinkProps, any> {
@@ -33,6 +37,24 @@ export class CommunityLink extends Component<CommunityLinkProps, any> {
         ? `/community/${community.id}`
         : community.actor_id;
     }
-    return <Link to={link}>{name_}</Link>;
+
+    let apubName = `!${name_}`;
+    let displayName = this.props.useApubName ? apubName : name_;
+    return (
+      <Link
+        title={apubName}
+        className={`${this.props.muted ? 'text-muted' : ''}`}
+        to={link}
+      >
+        {!this.props.hideAvatar && community.icon && showAvatars() && (
+          <img
+            style="width: 2rem; height: 2rem;"
+            src={pictrsAvatarThumbnail(community.icon)}
+            class="rounded-circle mr-2"
+          />
+        )}
+        <span>{displayName}</span>
+      </Link>
+    );
   }
 }
index 99b692cacf96694740d3bca8327278fae481328a..f86562f8c23286fe84bb38f92b5a708fb8d028eb 100644 (file)
@@ -1,6 +1,8 @@
 import { Component, linkEvent } from 'inferno';
+import { Helmet } from 'inferno-helmet';
 import { Subscription } from 'rxjs';
 import { retryWhen, delay, take } from 'rxjs/operators';
+import { DataType } from '../interfaces';
 import {
   UserOperation,
   Community as CommunityI,
@@ -13,7 +15,6 @@ import {
   GetPostsForm,
   GetCommunityForm,
   ListingType,
-  DataType,
   GetPostsResponse,
   PostResponse,
   AddModToCommunityResponse,
@@ -25,13 +26,15 @@ import {
   WebSocketJsonResponse,
   GetSiteResponse,
   Site,
-} from '../interfaces';
+} from 'lemmy-js-client';
 import { WebSocketService } from '../services';
 import { PostListings } from './post-listings';
 import { CommentNodes } from './comment-nodes';
 import { SortSelect } from './sort-select';
 import { DataTypeSelect } from './data-type-select';
 import { Sidebar } from './sidebar';
+import { CommunityLink } from './community-link';
+import { BannerIconHeader } from './banner-icon-header';
 import {
   wsJsonToRes,
   fetchLimit,
@@ -46,6 +49,8 @@ import {
   editPostFindRes,
   commentsToFlatNodes,
   setupTippy,
+  favIconUrl,
+  notifyPost,
 } from '../utils';
 import { i18n } from '../i18next';
 
@@ -73,7 +78,7 @@ interface CommunityProps {
 
 interface UrlParams {
   dataType?: string;
-  sort?: string;
+  sort?: SortType;
   page?: number;
 }
 
@@ -125,6 +130,9 @@ export class Community extends Component<any, State> {
       enable_downvotes: undefined,
       open_registration: undefined,
       enable_nsfw: undefined,
+      icon: undefined,
+      banner: undefined,
+      creator_preferred_username: undefined,
     },
   };
 
@@ -174,10 +182,29 @@ export class Community extends Component<any, State> {
     }
   }
 
+  get documentTitle(): string {
+    if (this.state.community.title) {
+      return `${this.state.community.title} - ${this.state.site.name}`;
+    } else {
+      return 'Lemmy';
+    }
+  }
+
+  get favIcon(): string {
+    return this.state.site.icon ? this.state.site.icon : favIconUrl;
+  }
+
   render() {
     return (
       <div class="container">
-        {this.selects()}
+        <Helmet title={this.documentTitle}>
+          <link
+            id="favicon"
+            rel="icon"
+            type="image/x-icon"
+            href={this.favIcon}
+          />
+        </Helmet>
         {this.state.loading ? (
           <h5>
             <svg class="icon icon-spinner spin">
@@ -187,19 +214,8 @@ export class Community extends Component<any, State> {
         ) : (
           <div class="row">
             <div class="col-12 col-md-8">
-              <h5>
-                {this.state.community.title}
-                {this.state.community.removed && (
-                  <small className="ml-2 text-muted font-italic">
-                    {i18n.t('removed')}
-                  </small>
-                )}
-                {this.state.community.nsfw && (
-                  <small className="ml-2 text-muted font-italic">
-                    {i18n.t('nsfw')}
-                  </small>
-                )}
-              </h5>
+              {this.communityInfo()}
+              {this.selects()}
               {this.listings()}
               {this.paginator()}
             </div>
@@ -238,6 +254,26 @@ export class Community extends Component<any, State> {
     );
   }
 
+  communityInfo() {
+    return (
+      <div>
+        <BannerIconHeader
+          banner={this.state.community.banner}
+          icon={this.state.community.icon}
+        />
+        <h5 class="mb-0">{this.state.community.title}</h5>
+        <CommunityLink
+          community={this.state.community}
+          realLink
+          useApubName
+          muted
+          hideAvatar
+        />
+        <hr />
+      </div>
+    );
+  }
+
   selects() {
     return (
       <div class="mb-3">
@@ -251,9 +287,7 @@ export class Community extends Component<any, State> {
           <SortSelect sort={this.state.sort} onChange={this.handleSortChange} />
         </span>
         <a
-          href={`/feeds/c/${this.state.communityName}.xml?sort=${
-            SortType[this.state.sort]
-          }`}
+          href={`/feeds/c/${this.state.communityName}.xml?sort=${this.state.sort}`}
           target="_blank"
           title="RSS"
           rel="noopener"
@@ -271,7 +305,7 @@ export class Community extends Component<any, State> {
       <div class="my-2">
         {this.state.page > 1 && (
           <button
-            class="btn btn-sm btn-secondary mr-1"
+            class="btn btn-secondary mr-1"
             onClick={linkEvent(this, this.prevPage)}
           >
             {i18n.t('prev')}
@@ -279,7 +313,7 @@ export class Community extends Component<any, State> {
         )}
         {this.state.posts.length > 0 && (
           <button
-            class="btn btn-sm btn-secondary"
+            class="btn btn-secondary"
             onClick={linkEvent(this, this.nextPage)}
           >
             {i18n.t('next')}
@@ -300,20 +334,18 @@ export class Community extends Component<any, State> {
   }
 
   handleSortChange(val: SortType) {
-    this.updateUrl({ sort: SortType[val].toLowerCase(), page: 1 });
+    this.updateUrl({ sort: val, page: 1 });
     window.scrollTo(0, 0);
   }
 
   handleDataTypeChange(val: DataType) {
-    this.updateUrl({ dataType: DataType[val].toLowerCase(), page: 1 });
+    this.updateUrl({ dataType: DataType[val], page: 1 });
     window.scrollTo(0, 0);
   }
 
   updateUrl(paramUpdates: UrlParams) {
-    const dataTypeStr =
-      paramUpdates.dataType || DataType[this.state.dataType].toLowerCase();
-    const sortStr =
-      paramUpdates.sort || SortType[this.state.sort].toLowerCase();
+    const dataTypeStr = paramUpdates.dataType || DataType[this.state.dataType];
+    const sortStr = paramUpdates.sort || this.state.sort;
     const page = paramUpdates.page || this.state.page;
     this.props.history.push(
       `/c/${this.state.community.name}/data_type/${dataTypeStr}/sort/${sortStr}/page/${page}`
@@ -325,8 +357,8 @@ export class Community extends Component<any, State> {
       let getPostsForm: GetPostsForm = {
         page: this.state.page,
         limit: fetchLimit,
-        sort: SortType[this.state.sort],
-        type_: ListingType[ListingType.Community],
+        sort: this.state.sort,
+        type_: ListingType.Community,
         community_id: this.state.community.id,
       };
       WebSocketService.Instance.getPosts(getPostsForm);
@@ -334,8 +366,8 @@ export class Community extends Component<any, State> {
       let getCommentsForm: GetCommentsForm = {
         page: this.state.page,
         limit: fetchLimit,
-        sort: SortType[this.state.sort],
-        type_: ListingType[ListingType.Community],
+        sort: this.state.sort,
+        type_: ListingType.Community,
         community_id: this.state.community.id,
       };
       WebSocketService.Instance.getComments(getCommentsForm);
@@ -355,12 +387,14 @@ export class Community extends Component<any, State> {
       let data = res.data as GetCommunityResponse;
       this.state.community = data.community;
       this.state.moderators = data.moderators;
-      this.state.admins = data.admins;
       this.state.online = data.online;
-      document.title = `/c/${this.state.community.name} - ${this.state.site.name}`;
       this.setState(this.state);
       this.fetchData();
-    } else if (res.op == UserOperation.EditCommunity) {
+    } else if (
+      res.op == UserOperation.EditCommunity ||
+      res.op == UserOperation.DeleteCommunity ||
+      res.op == UserOperation.RemoveCommunity
+    ) {
       let data = res.data as CommunityResponse;
       this.state.community = data.community;
       this.setState(this.state);
@@ -376,13 +410,20 @@ export class Community extends Component<any, State> {
       this.state.loading = false;
       this.setState(this.state);
       setupTippy();
-    } else if (res.op == UserOperation.EditPost) {
+    } else if (
+      res.op == UserOperation.EditPost ||
+      res.op == UserOperation.DeletePost ||
+      res.op == UserOperation.RemovePost ||
+      res.op == UserOperation.LockPost ||
+      res.op == UserOperation.StickyPost
+    ) {
       let data = res.data as PostResponse;
       editPostFindRes(data, this.state.posts);
       this.setState(this.state);
     } else if (res.op == UserOperation.CreatePost) {
       let data = res.data as PostResponse;
       this.state.posts.unshift(data.post);
+      notifyPost(data.post, this.context.router);
       this.setState(this.state);
     } else if (res.op == UserOperation.CreatePostLike) {
       let data = res.data as PostResponse;
@@ -405,7 +446,11 @@ export class Community extends Component<any, State> {
       this.state.comments = data.comments;
       this.state.loading = false;
       this.setState(this.state);
-    } else if (res.op == UserOperation.EditComment) {
+    } else if (
+      res.op == UserOperation.EditComment ||
+      res.op == UserOperation.DeleteComment ||
+      res.op == UserOperation.RemoveComment
+    ) {
       let data = res.data as CommentResponse;
       editCommentRes(data, this.state.comments);
       this.setState(this.state);
@@ -428,6 +473,7 @@ export class Community extends Component<any, State> {
     } else if (res.op == UserOperation.GetSite) {
       let data = res.data as GetSiteResponse;
       this.state.site = data.site;
+      this.state.admins = data.admins;
       this.setState(this.state);
     }
   }
index 3a5d943d4ac64bc81c90056f4958e8860631e103..6f156211d34be49fb5bc1c80b30ef781f8f14410 100644 (file)
@@ -1,4 +1,5 @@
 import { Component } from 'inferno';
+import { Helmet } from 'inferno-helmet';
 import { Subscription } from 'rxjs';
 import { retryWhen, delay, take } from 'rxjs/operators';
 import { CommunityForm } from './community-form';
@@ -7,19 +8,33 @@ import {
   UserOperation,
   WebSocketJsonResponse,
   GetSiteResponse,
-} from '../interfaces';
+  Site,
+} from 'lemmy-js-client';
 import { toast, wsJsonToRes } from '../utils';
 import { WebSocketService, UserService } from '../services';
 import { i18n } from '../i18next';
 
 interface CreateCommunityState {
-  enableNsfw: boolean;
+  site: Site;
 }
 
 export class CreateCommunity extends Component<any, CreateCommunityState> {
   private subscription: Subscription;
   private emptyState: CreateCommunityState = {
-    enableNsfw: null,
+    site: {
+      id: undefined,
+      name: undefined,
+      creator_id: undefined,
+      published: undefined,
+      creator_name: undefined,
+      number_of_users: undefined,
+      number_of_posts: undefined,
+      number_of_comments: undefined,
+      number_of_communities: undefined,
+      enable_downvotes: undefined,
+      open_registration: undefined,
+      enable_nsfw: undefined,
+    },
   };
   constructor(props: any, context: any) {
     super(props, context);
@@ -46,15 +61,24 @@ export class CreateCommunity extends Component<any, CreateCommunityState> {
     this.subscription.unsubscribe();
   }
 
+  get documentTitle(): string {
+    if (this.state.site.name) {
+      return `${i18n.t('create_community')} - ${this.state.site.name}`;
+    } else {
+      return 'Lemmy';
+    }
+  }
+
   render() {
     return (
       <div class="container">
+        <Helmet title={this.documentTitle} />
         <div class="row">
           <div class="col-12 col-lg-6 offset-lg-3 mb-4">
             <h5>{i18n.t('create_community')}</h5>
             <CommunityForm
               onCreate={this.handleCommunityCreate}
-              enableNsfw={this.state.enableNsfw}
+              enableNsfw={this.state.site.enable_nsfw}
             />
           </div>
         </div>
@@ -70,13 +94,12 @@ export class CreateCommunity extends Component<any, CreateCommunityState> {
     console.log(msg);
     let res = wsJsonToRes(msg);
     if (msg.error) {
-      toast(i18n.t(msg.error), 'danger');
+      // Toast errors are already handled by community-form
       return;
     } else if (res.op == UserOperation.GetSite) {
       let data = res.data as GetSiteResponse;
-      this.state.enableNsfw = data.site.enable_nsfw;
+      this.state.site = data.site;
       this.setState(this.state);
-      document.title = `${i18n.t('create_community')} - ${data.site.name}`;
     }
   }
 }
index 4554326daaa34de2af329a0cf6253d93a035cf49..f4c03b653ecd72659137a86fe03b8398f5381e14 100644 (file)
@@ -1,4 +1,5 @@
 import { Component } from 'inferno';
+import { Helmet } from 'inferno-helmet';
 import { Subscription } from 'rxjs';
 import { retryWhen, delay, take } from 'rxjs/operators';
 import { PostForm } from './post-form';
@@ -10,7 +11,7 @@ import {
   WebSocketJsonResponse,
   GetSiteResponse,
   Site,
-} from '../interfaces';
+} from 'lemmy-js-client';
 import { i18n } from '../i18next';
 
 interface CreatePostState {
@@ -61,9 +62,18 @@ export class CreatePost extends Component<any, CreatePostState> {
     this.subscription.unsubscribe();
   }
 
+  get documentTitle(): string {
+    if (this.state.site.name) {
+      return `${i18n.t('create_post')} - ${this.state.site.name}`;
+    } else {
+      return 'Lemmy';
+    }
+  }
+
   render() {
     return (
       <div class="container">
+        <Helmet title={this.documentTitle} />
         <div class="row">
           <div class="col-12 col-lg-6 offset-lg-3 mb-4">
             <h5>{i18n.t('create_post')}</h5>
@@ -100,7 +110,7 @@ export class CreatePost extends Component<any, CreatePostState> {
         return lastLocation.split('/c/')[1];
       }
     }
-    return undefined;
+    return;
   }
 
   handlePostCreate(id: number) {
@@ -117,7 +127,6 @@ export class CreatePost extends Component<any, CreatePostState> {
       let data = res.data as GetSiteResponse;
       this.state.site = data.site;
       this.setState(this.state);
-      document.title = `${i18n.t('create_post')} - ${data.site.name}`;
     }
   }
 }
index c309cbe3e3ce269df06e1146f9ddd3c7e13842a6..98c69d5b74bfa15b30799ca024138e062fa7d341 100644 (file)
@@ -1,4 +1,5 @@
 import { Component } from 'inferno';
+import { Helmet } from 'inferno-helmet';
 import { Subscription } from 'rxjs';
 import { retryWhen, delay, take } from 'rxjs/operators';
 import { PrivateMessageForm } from './private-message-form';
@@ -7,15 +8,27 @@ import {
   UserOperation,
   WebSocketJsonResponse,
   GetSiteResponse,
+  Site,
   PrivateMessageFormParams,
-} from '../interfaces';
+} from 'lemmy-js-client';
 import { toast, wsJsonToRes } from '../utils';
 import { i18n } from '../i18next';
 
-export class CreatePrivateMessage extends Component<any, any> {
+interface CreatePrivateMessageState {
+  site: Site;
+}
+
+export class CreatePrivateMessage extends Component<
+  any,
+  CreatePrivateMessageState
+> {
   private subscription: Subscription;
+  private emptyState: CreatePrivateMessageState = {
+    site: undefined,
+  };
   constructor(props: any, context: any) {
     super(props, context);
+    this.state = this.emptyState;
     this.handlePrivateMessageCreate = this.handlePrivateMessageCreate.bind(
       this
     );
@@ -40,9 +53,18 @@ export class CreatePrivateMessage extends Component<any, any> {
     this.subscription.unsubscribe();
   }
 
+  get documentTitle(): string {
+    if (this.state.site) {
+      return `${i18n.t('create_private_message')} - ${this.state.site.name}`;
+    } else {
+      return 'Lemmy';
+    }
+  }
+
   render() {
     return (
       <div class="container">
+        <Helmet title={this.documentTitle} />
         <div class="row">
           <div class="col-12 col-lg-6 offset-lg-3 mb-4">
             <h5>{i18n.t('create_private_message')}</h5>
@@ -80,9 +102,8 @@ export class CreatePrivateMessage extends Component<any, any> {
       return;
     } else if (res.op == UserOperation.GetSite) {
       let data = res.data as GetSiteResponse;
-      document.title = `${i18n.t('create_private_message')} - ${
-        data.site.name
-      }`;
+      this.state.site = data.site;
+      this.setState(this.state);
     }
   }
 }
index d16c785d8bafeac01c3446ded7ffb598f18956a0..c3e1fc60573d8b3c697542ccc6899940cc9839d0 100644 (file)
@@ -33,9 +33,9 @@ export class DataTypeSelect extends Component<
 
   render() {
     return (
-      <div class="btn-group btn-group-toggle">
+      <div class="btn-group btn-group-toggle flex-wrap mb-2">
         <label
-          className={`pointer btn btn-sm btn-secondary 
+          className={`pointer btn btn-outline-secondary 
             ${this.state.type_ == DataType.Post && 'active'}
           `}
         >
@@ -48,7 +48,7 @@ export class DataTypeSelect extends Component<
           {i18n.t('posts')}
         </label>
         <label
-          className={`pointer btn btn-sm btn-secondary ${
+          className={`pointer btn btn-outline-secondary ${
             this.state.type_ == DataType.Comment && 'active'
           }`}
         >
index cadb6aa39eaafb07a256a93afb93318a4fcce4cf..62585ff30bb9b4e40693b4dc8c436035abfe6dfc 100644 (file)
@@ -1,12 +1,41 @@
 import { Component } from 'inferno';
 import { Link } from 'inferno-router';
-import { repoUrl } from '../utils';
-import { version } from '../version';
 import { i18n } from '../i18next';
+import { Subscription } from 'rxjs';
+import { retryWhen, delay, take } from 'rxjs/operators';
+import { WebSocketService } from '../services';
+import { repoUrl, wsJsonToRes } from '../utils';
+import {
+  UserOperation,
+  WebSocketJsonResponse,
+  GetSiteResponse,
+} from 'lemmy-js-client';
 
-export class Footer extends Component<any, any> {
+interface FooterState {
+  version: string;
+}
+
+export class Footer extends Component<any, FooterState> {
+  private wsSub: Subscription;
+  emptyState: FooterState = {
+    version: null,
+  };
   constructor(props: any, context: any) {
     super(props, context);
+
+    this.state = this.emptyState;
+
+    this.wsSub = WebSocketService.Instance.subject
+      .pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
+      .subscribe(
+        msg => this.parseMessage(msg),
+        err => console.error(err),
+        () => console.log('complete')
+      );
+  }
+
+  componentWillUnmount() {
+    this.wsSub.unsubscribe();
   }
 
   render() {
@@ -15,13 +44,18 @@ export class Footer extends Component<any, any> {
         <div className="navbar-collapse">
           <ul class="navbar-nav ml-auto">
             <li class="nav-item">
-              <span class="navbar-text">{version}</span>
+              <span class="navbar-text">{this.state.version}</span>
             </li>
             <li class="nav-item">
               <Link class="nav-link" to="/modlog">
                 {i18n.t('modlog')}
               </Link>
             </li>
+            <li class="nav-item">
+              <Link class="nav-link" to="/instances">
+                {i18n.t('instances')}
+              </Link>
+            </li>
             <li class="nav-item">
               <a class="nav-link" href={'/docs/index.html'}>
                 {i18n.t('docs')}
@@ -42,4 +76,12 @@ export class Footer extends Component<any, any> {
       </nav>
     );
   }
+  parseMessage(msg: WebSocketJsonResponse) {
+    let res = wsJsonToRes(msg);
+
+    if (res.op == UserOperation.GetSite) {
+      let data = res.data as GetSiteResponse;
+      this.setState({ version: data.version });
+    }
+  }
 }
index 0d3f43f69bdba13253a737153a94541487cf5b73..6a604f7c5ffbe8cabc88083c8c14da410634b5ae 100644 (file)
@@ -1,5 +1,5 @@
 import { Component, linkEvent } from 'inferno';
-import { Post } from '../interfaces';
+import { Post } from 'lemmy-js-client';
 import { mdToHtml } from '../utils';
 import { i18n } from '../i18next';
 
@@ -29,7 +29,7 @@ export class IFramelyCard extends Component<
     return (
       <>
         {post.embed_title && !this.state.expanded && (
-          <div class="card mt-3 mb-2">
+          <div class="card bg-transparent border-secondary mt-3 mb-2">
             <div class="row">
               <div class="col-12">
                 <div class="card-body">
diff --git a/ui/src/components/image-upload-form.tsx b/ui/src/components/image-upload-form.tsx
new file mode 100644 (file)
index 0000000..98206f1
--- /dev/null
@@ -0,0 +1,114 @@
+import { Component, linkEvent } from 'inferno';
+import { UserService } from '../services';
+import { toast, randomStr } from '../utils';
+
+interface ImageUploadFormProps {
+  uploadTitle: string;
+  imageSrc: string;
+  onUpload(url: string): any;
+  onRemove(): any;
+  rounded?: boolean;
+}
+
+interface ImageUploadFormState {
+  loading: boolean;
+}
+
+export class ImageUploadForm extends Component<
+  ImageUploadFormProps,
+  ImageUploadFormState
+> {
+  private id = `image-upload-form-${randomStr()}`;
+  private emptyState: ImageUploadFormState = {
+    loading: false,
+  };
+
+  constructor(props: any, context: any) {
+    super(props, context);
+    this.state = this.emptyState;
+  }
+
+  render() {
+    return (
+      <form class="d-inline">
+        <label
+          htmlFor={this.id}
+          class="pointer ml-4 text-muted small font-weight-bold"
+        >
+          {!this.props.imageSrc ? (
+            <span class="btn btn-secondary">{this.props.uploadTitle}</span>
+          ) : (
+            <span class="d-inline-block position-relative">
+              <img
+                src={this.props.imageSrc}
+                height={this.props.rounded ? 60 : ''}
+                width={this.props.rounded ? 60 : ''}
+                className={`img-fluid ${
+                  this.props.rounded ? 'rounded-circle' : ''
+                }`}
+              />
+              <a onClick={linkEvent(this, this.handleRemoveImage)}>
+                <svg class="icon mini-overlay">
+                  <use xlinkHref="#icon-x"></use>
+                </svg>
+              </a>
+            </span>
+          )}
+        </label>
+        <input
+          id={this.id}
+          type="file"
+          accept="image/*,video/*"
+          name={this.id}
+          class="d-none"
+          disabled={!UserService.Instance.user}
+          onChange={linkEvent(this, this.handleImageUpload)}
+        />
+      </form>
+    );
+  }
+
+  handleImageUpload(i: ImageUploadForm, event: any) {
+    event.preventDefault();
+    let file = event.target.files[0];
+    const imageUploadUrl = `/pictrs/image`;
+    const formData = new FormData();
+    formData.append('images[]', file);
+
+    i.state.loading = true;
+    i.setState(i.state);
+
+    fetch(imageUploadUrl, {
+      method: 'POST',
+      body: formData,
+    })
+      .then(res => res.json())
+      .then(res => {
+        console.log('pictrs upload:');
+        console.log(res);
+        if (res.msg == 'ok') {
+          let hash = res.files[0].file;
+          let url = `${window.location.origin}/pictrs/image/${hash}`;
+          i.state.loading = false;
+          i.setState(i.state);
+          i.props.onUpload(url);
+        } else {
+          i.state.loading = false;
+          i.setState(i.state);
+          toast(JSON.stringify(res), 'danger');
+        }
+      })
+      .catch(error => {
+        i.state.loading = false;
+        i.setState(i.state);
+        toast(error, 'danger');
+      });
+  }
+
+  handleRemoveImage(i: ImageUploadForm, event: any) {
+    event.preventDefault();
+    i.state.loading = true;
+    i.setState(i.state);
+    i.props.onRemove();
+  }
+}
index 8e148921fe1b1f41070fcfeb3c3f33ae5f32de4b..4da86e351a35c6b7f4a45c3840dc3190ef78703b 100644 (file)
@@ -1,4 +1,5 @@
 import { Component, linkEvent } from 'inferno';
+import { Helmet } from 'inferno-helmet';
 import { Subscription } from 'rxjs';
 import { retryWhen, delay, take } from 'rxjs/operators';
 import {
@@ -17,7 +18,8 @@ import {
   PrivateMessagesResponse,
   PrivateMessageResponse,
   GetSiteResponse,
-} from '../interfaces';
+  Site,
+} from 'lemmy-js-client';
 import { WebSocketService, UserService } from '../services';
 import {
   wsJsonToRes,
@@ -57,7 +59,7 @@ interface InboxState {
   messages: Array<PrivateMessageI>;
   sort: SortType;
   page: number;
-  enableDownvotes: boolean;
+  site: Site;
 }
 
 export class Inbox extends Component<any, InboxState> {
@@ -70,7 +72,20 @@ export class Inbox extends Component<any, InboxState> {
     messages: [],
     sort: SortType.New,
     page: 1,
-    enableDownvotes: undefined,
+    site: {
+      id: undefined,
+      name: undefined,
+      creator_id: undefined,
+      published: undefined,
+      creator_name: undefined,
+      number_of_users: undefined,
+      number_of_posts: undefined,
+      number_of_comments: undefined,
+      number_of_communities: undefined,
+      enable_downvotes: undefined,
+      open_registration: undefined,
+      enable_nsfw: undefined,
+    },
   };
 
   constructor(props: any, context: any) {
@@ -95,9 +110,20 @@ export class Inbox extends Component<any, InboxState> {
     this.subscription.unsubscribe();
   }
 
+  get documentTitle(): string {
+    if (this.state.site.name) {
+      return `@${UserService.Instance.user.name} ${i18n.t('inbox')} - ${
+        this.state.site.name
+      }`;
+    } else {
+      return 'Lemmy';
+    }
+  }
+
   render() {
     return (
       <div class="container">
+        <Helmet title={this.documentTitle} />
         <div class="row">
           <div class="col-12">
             <h5 class="mb-1">
@@ -145,9 +171,9 @@ export class Inbox extends Component<any, InboxState> {
 
   unreadOrAllRadios() {
     return (
-      <div class="btn-group btn-group-toggle">
+      <div class="btn-group btn-group-toggle flex-wrap mb-2">
         <label
-          className={`btn btn-sm btn-secondary pointer
+          className={`btn btn-outline-secondary pointer
             ${this.state.unreadOrAll == UnreadOrAll.Unread && 'active'}
           `}
         >
@@ -160,7 +186,7 @@ export class Inbox extends Component<any, InboxState> {
           {i18n.t('unread')}
         </label>
         <label
-          className={`btn btn-sm btn-secondary pointer
+          className={`btn btn-outline-secondary pointer
             ${this.state.unreadOrAll == UnreadOrAll.All && 'active'}
           `}
         >
@@ -178,9 +204,9 @@ export class Inbox extends Component<any, InboxState> {
 
   messageTypeRadios() {
     return (
-      <div class="btn-group btn-group-toggle">
+      <div class="btn-group btn-group-toggle flex-wrap mb-2">
         <label
-          className={`btn btn-sm btn-secondary pointer btn-outline-light
+          className={`btn btn-outline-secondary pointer
             ${this.state.messageType == MessageType.All && 'active'}
           `}
         >
@@ -193,7 +219,7 @@ export class Inbox extends Component<any, InboxState> {
           {i18n.t('all')}
         </label>
         <label
-          className={`btn btn-sm btn-secondary pointer btn-outline-light
+          className={`btn btn-outline-secondary pointer
             ${this.state.messageType == MessageType.Replies && 'active'}
           `}
         >
@@ -206,7 +232,7 @@ export class Inbox extends Component<any, InboxState> {
           {i18n.t('replies')}
         </label>
         <label
-          className={`btn btn-sm btn-secondary pointer btn-outline-light
+          className={`btn btn-outline-secondary pointer
             ${this.state.messageType == MessageType.Mentions && 'active'}
           `}
         >
@@ -219,7 +245,7 @@ export class Inbox extends Component<any, InboxState> {
           {i18n.t('mentions')}
         </label>
         <label
-          className={`btn btn-sm btn-secondary pointer btn-outline-light
+          className={`btn btn-outline-secondary pointer
             ${this.state.messageType == MessageType.Messages && 'active'}
           `}
         >
@@ -249,30 +275,30 @@ export class Inbox extends Component<any, InboxState> {
     );
   }
 
-  all() {
-    let combined: Array<ReplyType> = [];
-
-    combined.push(...this.state.replies);
-    combined.push(...this.state.mentions);
-    combined.push(...this.state.messages);
-
-    // Sort it
-    combined.sort((a, b) => b.published.localeCompare(a.published));
+  combined(): Array<ReplyType> {
+    return [
+      ...this.state.replies,
+      ...this.state.mentions,
+      ...this.state.messages,
+    ].sort((a, b) => b.published.localeCompare(a.published));
+  }
 
+  all() {
     return (
       <div>
-        {combined.map(i =>
+        {this.combined().map(i =>
           isCommentType(i) ? (
             <CommentNodes
+              key={i.id}
               nodes={[{ comment: i }]}
               noIndent
               markable
               showCommunity
               showContext
-              enableDownvotes={this.state.enableDownvotes}
+              enableDownvotes={this.state.site.enable_downvotes}
             />
           ) : (
-            <PrivateMessage privateMessage={i} />
+            <PrivateMessage key={i.id} privateMessage={i} />
           )
         )}
       </div>
@@ -288,7 +314,7 @@ export class Inbox extends Component<any, InboxState> {
           markable
           showCommunity
           showContext
-          enableDownvotes={this.state.enableDownvotes}
+          enableDownvotes={this.state.site.enable_downvotes}
         />
       </div>
     );
@@ -299,12 +325,13 @@ export class Inbox extends Component<any, InboxState> {
       <div>
         {this.state.mentions.map(mention => (
           <CommentNodes
+            key={mention.id}
             nodes={[{ comment: mention }]}
             noIndent
             markable
             showCommunity
             showContext
-            enableDownvotes={this.state.enableDownvotes}
+            enableDownvotes={this.state.site.enable_downvotes}
           />
         ))}
       </div>
@@ -315,7 +342,7 @@ export class Inbox extends Component<any, InboxState> {
     return (
       <div>
         {this.state.messages.map(message => (
-          <PrivateMessage privateMessage={message} />
+          <PrivateMessage key={message.id} privateMessage={message} />
         ))}
       </div>
     );
@@ -326,7 +353,7 @@ export class Inbox extends Component<any, InboxState> {
       <div class="mt-2">
         {this.state.page > 1 && (
           <button
-            class="btn btn-sm btn-secondary mr-1"
+            class="btn btn-secondary mr-1"
             onClick={linkEvent(this, this.prevPage)}
           >
             {i18n.t('prev')}
@@ -334,7 +361,7 @@ export class Inbox extends Component<any, InboxState> {
         )}
         {this.unreadCount() > 0 && (
           <button
-            class="btn btn-sm btn-secondary"
+            class="btn btn-secondary"
             onClick={linkEvent(this, this.nextPage)}
           >
             {i18n.t('next')}
@@ -372,7 +399,7 @@ export class Inbox extends Component<any, InboxState> {
 
   refetch() {
     let repliesForm: GetRepliesForm = {
-      sort: SortType[this.state.sort],
+      sort: this.state.sort,
       unread_only: this.state.unreadOrAll == UnreadOrAll.Unread,
       page: this.state.page,
       limit: fetchLimit,
@@ -380,7 +407,7 @@ export class Inbox extends Component<any, InboxState> {
     WebSocketService.Instance.getReplies(repliesForm);
 
     let userMentionsForm: GetUserMentionsForm = {
-      sort: SortType[this.state.sort],
+      sort: this.state.sort,
       unread_only: this.state.unreadOrAll == UnreadOrAll.Unread,
       page: this.state.page,
       limit: fetchLimit,
@@ -446,27 +473,54 @@ export class Inbox extends Component<any, InboxState> {
       let found: PrivateMessageI = this.state.messages.find(
         m => m.id === data.message.id
       );
-      found.content = data.message.content;
-      found.updated = data.message.updated;
-      found.deleted = data.message.deleted;
-      // If youre in the unread view, just remove it from the list
-      if (this.state.unreadOrAll == UnreadOrAll.Unread && data.message.read) {
-        this.state.messages = this.state.messages.filter(
-          r => r.id !== data.message.id
-        );
-      } else {
-        let found = this.state.messages.find(c => c.id == data.message.id);
-        found.read = data.message.read;
+      if (found) {
+        found.content = data.message.content;
+        found.updated = data.message.updated;
+      }
+      this.setState(this.state);
+    } else if (res.op == UserOperation.DeletePrivateMessage) {
+      let data = res.data as PrivateMessageResponse;
+      let found: PrivateMessageI = this.state.messages.find(
+        m => m.id === data.message.id
+      );
+      if (found) {
+        found.deleted = data.message.deleted;
+        found.updated = data.message.updated;
+      }
+      this.setState(this.state);
+    } else if (res.op == UserOperation.MarkPrivateMessageAsRead) {
+      let data = res.data as PrivateMessageResponse;
+      let found: PrivateMessageI = this.state.messages.find(
+        m => m.id === data.message.id
+      );
+
+      if (found) {
+        found.updated = data.message.updated;
+
+        // If youre in the unread view, just remove it from the list
+        if (this.state.unreadOrAll == UnreadOrAll.Unread && data.message.read) {
+          this.state.messages = this.state.messages.filter(
+            r => r.id !== data.message.id
+          );
+        } else {
+          let found = this.state.messages.find(c => c.id == data.message.id);
+          found.read = data.message.read;
+        }
       }
       this.sendUnreadCount();
-      window.scrollTo(0, 0);
       this.setState(this.state);
-      setupTippy();
     } else if (res.op == UserOperation.MarkAllAsRead) {
       // Moved to be instant
-    } else if (res.op == UserOperation.EditComment) {
+    } else if (
+      res.op == UserOperation.EditComment ||
+      res.op == UserOperation.DeleteComment ||
+      res.op == UserOperation.RemoveComment
+    ) {
       let data = res.data as CommentResponse;
       editCommentRes(data, this.state.replies);
+      this.setState(this.state);
+    } else if (res.op == UserOperation.MarkCommentAsRead) {
+      let data = res.data as CommentResponse;
 
       // If youre in the unread view, just remove it from the list
       if (this.state.unreadOrAll == UnreadOrAll.Unread && data.comment.read) {
@@ -480,7 +534,7 @@ export class Inbox extends Component<any, InboxState> {
       this.sendUnreadCount();
       this.setState(this.state);
       setupTippy();
-    } else if (res.op == UserOperation.EditUserMention) {
+    } else if (res.op == UserOperation.MarkUserMentionAsRead) {
       let data = res.data as UserMentionResponse;
 
       let found = this.state.mentions.find(c => c.id == data.mention.id);
@@ -512,7 +566,6 @@ export class Inbox extends Component<any, InboxState> {
       } else if (data.comment.creator_id == UserService.Instance.user.id) {
         toast(i18n.t('reply_sent'));
       }
-      this.setState(this.state);
     } else if (res.op == UserOperation.CreatePrivateMessage) {
       let data = res.data as PrivateMessageResponse;
       if (data.message.recipient_id == UserService.Instance.user.id) {
@@ -530,19 +583,13 @@ export class Inbox extends Component<any, InboxState> {
       this.setState(this.state);
     } else if (res.op == UserOperation.GetSite) {
       let data = res.data as GetSiteResponse;
-      this.state.enableDownvotes = data.site.enable_downvotes;
+      this.state.site = data.site;
       this.setState(this.state);
-      document.title = `/u/${UserService.Instance.user.username} ${i18n.t(
-        'inbox'
-      )} - ${data.site.name}`;
     }
   }
 
   sendUnreadCount() {
-    UserService.Instance.user.unreadCount = this.unreadCount();
-    UserService.Instance.sub.next({
-      user: UserService.Instance.user,
-    });
+    UserService.Instance.unreadCountSub.next(this.unreadCount());
   }
 
   unreadCount(): number {
@@ -550,7 +597,10 @@ export class Inbox extends Component<any, InboxState> {
       this.state.replies.filter(r => !r.read).length +
       this.state.mentions.filter(r => !r.read).length +
       this.state.messages.filter(
-        r => !r.read && r.creator_id !== UserService.Instance.user.id
+        r =>
+          UserService.Instance.user &&
+          !r.read &&
+          r.creator_id !== UserService.Instance.user.id
       ).length
     );
   }
diff --git a/ui/src/components/instances.tsx b/ui/src/components/instances.tsx
new file mode 100644 (file)
index 0000000..bcc0248
--- /dev/null
@@ -0,0 +1,98 @@
+import { Component } from 'inferno';
+import { Helmet } from 'inferno-helmet';
+import { Subscription } from 'rxjs';
+import { retryWhen, delay, take } from 'rxjs/operators';
+import {
+  UserOperation,
+  WebSocketJsonResponse,
+  GetSiteResponse,
+} from 'lemmy-js-client';
+import { WebSocketService } from '../services';
+import { wsJsonToRes, toast } from '../utils';
+import { i18n } from '../i18next';
+
+interface InstancesState {
+  loading: boolean;
+  siteRes: GetSiteResponse;
+}
+
+export class Instances extends Component<any, InstancesState> {
+  private subscription: Subscription;
+  private emptyState: InstancesState = {
+    loading: true,
+    siteRes: undefined,
+  };
+
+  constructor(props: any, context: any) {
+    super(props, context);
+    this.state = this.emptyState;
+    this.subscription = WebSocketService.Instance.subject
+      .pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
+      .subscribe(
+        msg => this.parseMessage(msg),
+        err => console.error(err),
+        () => console.log('complete')
+      );
+
+    WebSocketService.Instance.getSite();
+  }
+
+  componentWillUnmount() {
+    this.subscription.unsubscribe();
+  }
+
+  get documentTitle(): string {
+    if (this.state.siteRes) {
+      return `${i18n.t('instances')} - ${this.state.siteRes.site.name}`;
+    } else {
+      return 'Lemmy';
+    }
+  }
+
+  render() {
+    return (
+      <div class="container">
+        <Helmet title={this.documentTitle} />
+        {this.state.loading ? (
+          <h5 class="">
+            <svg class="icon icon-spinner spin">
+              <use xlinkHref="#icon-spinner"></use>
+            </svg>
+          </h5>
+        ) : (
+          <div>
+            <h5>{i18n.t('linked_instances')}</h5>
+            {this.state.siteRes &&
+            this.state.siteRes.federated_instances.length ? (
+              <ul>
+                {this.state.siteRes.federated_instances.map(i => (
+                  <li>
+                    <a href={`https://${i}`} target="_blank" rel="noopener">
+                      {i}
+                    </a>
+                  </li>
+                ))}
+              </ul>
+            ) : (
+              <div>{i18n.t('none_found')}</div>
+            )}
+          </div>
+        )}
+      </div>
+    );
+  }
+
+  parseMessage(msg: WebSocketJsonResponse) {
+    console.log(msg);
+    let res = wsJsonToRes(msg);
+    if (msg.error) {
+      toast(i18n.t(msg.error), 'danger');
+      return;
+    } else if (res.op == UserOperation.GetSite) {
+      let data = res.data as GetSiteResponse;
+      this.state.siteRes = data;
+      this.state.loading = false;
+      this.setState(this.state);
+    }
+  }
+}
index e9b5a03125b7798554d6a3c2b109897aa238e220..3d12d43439e661b5e841f0f886657362562fd511 100644 (file)
@@ -1,7 +1,7 @@
 import { Component, linkEvent } from 'inferno';
-import { ListingType } from '../interfaces';
+import { ListingType } from 'lemmy-js-client';
 import { UserService } from '../services';
-
+import { randomStr } from '../utils';
 import { i18n } from '../i18next';
 
 interface ListingTypeSelectProps {
@@ -17,6 +17,8 @@ export class ListingTypeSelect extends Component<
   ListingTypeSelectProps,
   ListingTypeSelectState
 > {
+  private id = `listing-type-input-${randomStr()}`;
+
   private emptyState: ListingTypeSelectState = {
     type_: this.props.type_,
   };
@@ -34,14 +36,15 @@ export class ListingTypeSelect extends Component<
 
   render() {
     return (
-      <div class="btn-group btn-group-toggle">
+      <div class="btn-group btn-group-toggle flex-wrap mb-2">
         <label
-          className={`btn btn-sm btn-secondary 
+          className={`btn btn-outline-secondary 
             ${this.state.type_ == ListingType.Subscribed && 'active'}
             ${UserService.Instance.user == undefined ? 'disabled' : 'pointer'}
           `}
         >
           <input
+            id={`${this.id}-subscribed`}
             type="radio"
             value={ListingType.Subscribed}
             checked={this.state.type_ == ListingType.Subscribed}
@@ -51,11 +54,12 @@ export class ListingTypeSelect extends Component<
           {i18n.t('subscribed')}
         </label>
         <label
-          className={`pointer btn btn-sm btn-secondary ${
+          className={`pointer btn btn-outline-secondary ${
             this.state.type_ == ListingType.All && 'active'
           }`}
         >
           <input
+            id={`${this.id}-all`}
             type="radio"
             value={ListingType.All}
             checked={this.state.type_ == ListingType.All}
@@ -68,6 +72,6 @@ export class ListingTypeSelect extends Component<
   }
 
   handleTypeChange(i: ListingTypeSelect, event: any) {
-    i.props.onChange(Number(event.target.value));
+    i.props.onChange(event.target.value);
   }
 }
index 4dd3821a3a3b6cc1fa9e1abe0aa87da86a641e97..caf8c9cfb48c9463f04d149b7f0700c07231a94e 100644 (file)
@@ -1,4 +1,5 @@
 import { Component, linkEvent } from 'inferno';
+import { Helmet } from 'inferno-helmet';
 import { Subscription } from 'rxjs';
 import { retryWhen, delay, take } from 'rxjs/operators';
 import {
@@ -8,8 +9,10 @@ import {
   UserOperation,
   PasswordResetForm,
   GetSiteResponse,
+  GetCaptchaResponse,
   WebSocketJsonResponse,
-} from '../interfaces';
+  Site,
+} from 'lemmy-js-client';
 import { WebSocketService, UserService } from '../services';
 import { wsJsonToRes, validEmail, toast } from '../utils';
 import { i18n } from '../i18next';
@@ -19,12 +22,9 @@ interface State {
   registerForm: RegisterForm;
   loginLoading: boolean;
   registerLoading: boolean;
-  enable_nsfw: boolean;
-  mathQuestion: {
-    a: number;
-    b: number;
-    answer: number;
-  };
+  captcha: GetCaptchaResponse;
+  captchaPlaying: boolean;
+  site: Site;
 }
 
 export class Login extends Component<any, State> {
@@ -41,14 +41,26 @@ export class Login extends Component<any, State> {
       password_verify: undefined,
       admin: false,
       show_nsfw: false,
+      captcha_uuid: undefined,
+      captcha_answer: undefined,
     },
     loginLoading: false,
     registerLoading: false,
-    enable_nsfw: undefined,
-    mathQuestion: {
-      a: Math.floor(Math.random() * 10) + 1,
-      b: Math.floor(Math.random() * 10) + 1,
-      answer: undefined,
+    captcha: undefined,
+    captchaPlaying: false,
+    site: {
+      id: undefined,
+      name: undefined,
+      creator_id: undefined,
+      published: undefined,
+      creator_name: undefined,
+      number_of_users: undefined,
+      number_of_posts: undefined,
+      number_of_comments: undefined,
+      number_of_communities: undefined,
+      enable_downvotes: undefined,
+      open_registration: undefined,
+      enable_nsfw: undefined,
     },
   };
 
@@ -66,15 +78,25 @@ export class Login extends Component<any, State> {
       );
 
     WebSocketService.Instance.getSite();
+    WebSocketService.Instance.getCaptcha();
   }
 
   componentWillUnmount() {
     this.subscription.unsubscribe();
   }
 
+  get documentTitle(): string {
+    if (this.state.site.name) {
+      return `${i18n.t('login')} - ${this.state.site.name}`;
+    } else {
+      return 'Lemmy';
+    }
+  }
+
   render() {
     return (
       <div class="container">
+        <Helmet title={this.documentTitle} />
         <div class="row">
           <div class="col-12 col-lg-6 mb-4">{this.loginForm()}</div>
           <div class="col-12 col-lg-6">{this.registerForm()}</div>
@@ -148,6 +170,7 @@ export class Login extends Component<any, State> {
       </div>
     );
   }
+
   registerForm() {
     return (
       <form onSubmit={linkEvent(this, this.handleRegisterSubmit)}>
@@ -234,24 +257,38 @@ export class Login extends Component<any, State> {
             />
           </div>
         </div>
-        <div class="form-group row">
-          <label class="col-sm-10 col-form-label" htmlFor="register-math">
-            {i18n.t('what_is')}{' '}
-            {`${this.state.mathQuestion.a} + ${this.state.mathQuestion.b}?`}
-          </label>
 
-          <div class="col-sm-2">
-            <input
-              type="number"
-              id="register-math"
-              class="form-control"
-              value={this.state.mathQuestion.answer}
-              onInput={linkEvent(this, this.handleMathAnswerChange)}
-              required
-            />
+        {this.state.captcha && (
+          <div class="form-group row">
+            <label class="col-sm-2" htmlFor="register-captcha">
+              <span class="mr-2">{i18n.t('enter_code')}</span>
+              <button
+                type="button"
+                class="btn btn-secondary"
+                onClick={linkEvent(this, this.handleRegenCaptcha)}
+              >
+                <svg class="icon icon-refresh-cw">
+                  <use xlinkHref="#icon-refresh-cw"></use>
+                </svg>
+              </button>
+            </label>
+            {this.showCaptcha()}
+            <div class="col-sm-6">
+              <input
+                type="text"
+                class="form-control"
+                id="register-captcha"
+                value={this.state.registerForm.captcha_answer}
+                onInput={linkEvent(
+                  this,
+                  this.handleRegisterCaptchaAnswerChange
+                )}
+                required
+              />
+            </div>
           </div>
-        </div>
-        {this.state.enable_nsfw && (
+        )}
+        {this.state.site.enable_nsfw && (
           <div class="form-group row">
             <div class="col-sm-10">
               <div class="form-check">
@@ -271,11 +308,7 @@ export class Login extends Component<any, State> {
         )}
         <div class="form-group row">
           <div class="col-sm-10">
-            <button
-              type="submit"
-              class="btn btn-secondary"
-              disabled={this.mathCheck}
-            >
+            <button type="submit" class="btn btn-secondary">
               {this.state.registerLoading ? (
                 <svg class="icon icon-spinner spin">
                   <use xlinkHref="#icon-spinner"></use>
@@ -290,6 +323,36 @@ export class Login extends Component<any, State> {
     );
   }
 
+  showCaptcha() {
+    return (
+      <div class="col-sm-4">
+        {this.state.captcha.ok && (
+          <>
+            <img
+              class="rounded-top img-fluid"
+              src={this.captchaPngSrc()}
+              style="border-bottom-right-radius: 0; border-bottom-left-radius: 0;"
+            />
+            {this.state.captcha.ok.wav && (
+              <button
+                class="rounded-bottom btn btn-sm btn-secondary btn-block"
+                style="border-top-right-radius: 0; border-top-left-radius: 0;"
+                title={i18n.t('play_captcha_audio')}
+                onClick={linkEvent(this, this.handleCaptchaPlay)}
+                type="button"
+                disabled={this.state.captchaPlaying}
+              >
+                <svg class="icon icon-play">
+                  <use xlinkHref="#icon-play"></use>
+                </svg>
+              </button>
+            )}
+          </>
+        )}
+      </div>
+    );
+  }
+
   handleLoginSubmit(i: Login, event: any) {
     event.preventDefault();
     i.state.loginLoading = true;
@@ -311,10 +374,7 @@ export class Login extends Component<any, State> {
     event.preventDefault();
     i.state.registerLoading = true;
     i.setState(i.state);
-
-    if (!i.mathCheck) {
-      WebSocketService.Instance.register(i.state.registerForm);
-    }
+    WebSocketService.Instance.register(i.state.registerForm);
   }
 
   handleRegisterUsernameChange(i: Login, event: any) {
@@ -345,11 +405,16 @@ export class Login extends Component<any, State> {
     i.setState(i.state);
   }
 
-  handleMathAnswerChange(i: Login, event: any) {
-    i.state.mathQuestion.answer = event.target.value;
+  handleRegisterCaptchaAnswerChange(i: Login, event: any) {
+    i.state.registerForm.captcha_answer = event.target.value;
     i.setState(i.state);
   }
 
+  handleRegenCaptcha(_i: Login, _event: any) {
+    event.preventDefault();
+    WebSocketService.Instance.getCaptcha();
+  }
+
   handlePasswordReset(i: Login) {
     event.preventDefault();
     let resetForm: PasswordResetForm = {
@@ -358,11 +423,21 @@ export class Login extends Component<any, State> {
     WebSocketService.Instance.passwordReset(resetForm);
   }
 
-  get mathCheck(): boolean {
-    return (
-      this.state.mathQuestion.answer !=
-      this.state.mathQuestion.a + this.state.mathQuestion.b
-    );
+  handleCaptchaPlay(i: Login) {
+    event.preventDefault();
+    let snd = new Audio('data:audio/wav;base64,' + i.state.captcha.ok.wav);
+    snd.play();
+    i.state.captchaPlaying = true;
+    i.setState(i.state);
+    snd.addEventListener('ended', () => {
+      snd.currentTime = 0;
+      i.state.captchaPlaying = false;
+      i.setState(this.state);
+    });
+  }
+
+  captchaPngSrc() {
+    return `data:image/png;base64,${this.state.captcha.ok.png}`;
   }
 
   parseMessage(msg: WebSocketJsonResponse) {
@@ -370,6 +445,9 @@ export class Login extends Component<any, State> {
     if (msg.error) {
       toast(i18n.t(msg.error), 'danger');
       this.state = this.emptyState;
+      this.state.registerForm.captcha_answer = undefined;
+      // Refetch another captcha
+      WebSocketService.Instance.getCaptcha();
       this.setState(this.state);
       return;
     } else {
@@ -388,13 +466,19 @@ export class Login extends Component<any, State> {
         UserService.Instance.login(data);
         WebSocketService.Instance.userJoin();
         this.props.history.push('/communities');
+      } else if (res.op == UserOperation.GetCaptcha) {
+        let data = res.data as GetCaptchaResponse;
+        if (data.ok) {
+          this.state.captcha = data;
+          this.state.registerForm.captcha_uuid = data.ok.uuid;
+          this.setState(this.state);
+        }
       } else if (res.op == UserOperation.PasswordReset) {
         toast(i18n.t('reset_password_mail_sent'));
       } else if (res.op == UserOperation.GetSite) {
         let data = res.data as GetSiteResponse;
-        this.state.enable_nsfw = data.site.enable_nsfw;
+        this.state.site = data.site;
         this.setState(this.state);
-        document.title = `${i18n.t('login')} - ${data.site.name}`;
       }
     }
   }
index 0392090a5f3f26bc601a9470db0a7a644aba4d84..0f4b7da02d213052ad869743395b9f1a3c584af6 100644 (file)
@@ -1,4 +1,5 @@
 import { Component, linkEvent } from 'inferno';
+import { Helmet } from 'inferno-helmet';
 import { Link } from 'inferno-router';
 import { Subscription } from 'rxjs';
 import { retryWhen, delay, take } from 'rxjs/operators';
@@ -12,7 +13,6 @@ import {
   SortType,
   GetSiteResponse,
   ListingType,
-  DataType,
   SiteResponse,
   GetPostsResponse,
   PostResponse,
@@ -25,7 +25,8 @@ import {
   AddAdminResponse,
   BanUserResponse,
   WebSocketJsonResponse,
-} from '../interfaces';
+} from 'lemmy-js-client';
+import { DataType } from '../interfaces';
 import { WebSocketService, UserService } from '../services';
 import { PostListings } from './post-listings';
 import { CommentNodes } from './comment-nodes';
@@ -35,6 +36,7 @@ import { DataTypeSelect } from './data-type-select';
 import { SiteForm } from './site-form';
 import { UserListing } from './user-listing';
 import { CommunityLink } from './community-link';
+import { BannerIconHeader } from './banner-icon-header';
 import {
   wsJsonToRes,
   repoUrl,
@@ -52,6 +54,8 @@ import {
   editPostFindRes,
   commentsToFlatNodes,
   setupTippy,
+  favIconUrl,
+  notifyPost,
 } from '../utils';
 import { i18n } from '../i18next';
 import { T } from 'inferno-i18next';
@@ -78,9 +82,9 @@ interface MainProps {
 }
 
 interface UrlParams {
-  listingType?: string;
+  listingType?: ListingType;
   dataType?: string;
-  sort?: string;
+  sort?: SortType;
   page?: number;
 }
 
@@ -103,10 +107,15 @@ export class Main extends Component<any, MainState> {
         enable_downvotes: null,
         open_registration: null,
         enable_nsfw: null,
+        icon: null,
+        banner: null,
+        creator_preferred_username: null,
       },
       admins: [],
       banned: [],
       online: null,
+      version: null,
+      federated_instances: null,
     },
     showEditSite: false,
     loading: true,
@@ -142,7 +151,7 @@ export class Main extends Component<any, MainState> {
     }
 
     let listCommunitiesForm: ListCommunitiesForm = {
-      sort: SortType[SortType.Hot],
+      sort: SortType.Hot,
       limit: 6,
     };
 
@@ -176,70 +185,84 @@ export class Main extends Component<any, MainState> {
     }
   }
 
+  get documentTitle(): string {
+    if (this.state.siteRes.site.name) {
+      return `${this.state.siteRes.site.name}`;
+    } else {
+      return 'Lemmy';
+    }
+  }
+
+  get favIcon(): string {
+    return this.state.siteRes.site.icon
+      ? this.state.siteRes.site.icon
+      : favIconUrl;
+  }
+
   render() {
     return (
       <div class="container">
+        <Helmet title={this.documentTitle}>
+          <link
+            id="favicon"
+            rel="icon"
+            type="image/x-icon"
+            href={this.favIcon}
+          />
+        </Helmet>
         <div class="row">
           <main role="main" class="col-12 col-md-8">
             {this.posts()}
           </main>
-          <aside class="col-12 col-md-4">{this.my_sidebar()}</aside>
+          <aside class="col-12 col-md-4">{this.mySidebar()}</aside>
         </div>
       </div>
     );
   }
 
-  my_sidebar() {
+  mySidebar() {
     return (
       <div>
         {!this.state.loading && (
           <div>
-            <div class="card border-secondary mb-3">
+            <div class="card bg-transparent border-secondary mb-3">
+              <div class="card-header bg-transparent border-secondary">
+                <div class="mb-2">
+                  {this.siteName()}
+                  {this.adminButtons()}
+                </div>
+                <BannerIconHeader banner={this.state.siteRes.site.banner} />
+              </div>
               <div class="card-body">
                 {this.trendingCommunities()}
-                {UserService.Instance.user &&
-                  this.state.subscribedCommunities.length > 0 && (
-                    <div>
-                      <h5>
-                        <T i18nKey="subscribed_to_communities">
-                          #
-                          <Link class="text-body" to="/communities">
-                            #
-                          </Link>
-                        </T>
-                      </h5>
-                      <ul class="list-inline">
-                        {this.state.subscribedCommunities.map(community => (
-                          <li class="list-inline-item">
-                            <CommunityLink
-                              community={{
-                                name: community.community_name,
-                                id: community.community_id,
-                                local: community.community_local,
-                                actor_id: community.community_actor_id,
-                              }}
-                            />
-                          </li>
-                        ))}
-                      </ul>
-                    </div>
-                  )}
-                <Link
-                  class="btn btn-sm btn-secondary btn-block"
-                  to="/create_community"
-                >
-                  {i18n.t('create_a_community')}
-                </Link>
+                {this.createCommunityButton()}
+                {/*
+                {this.subscribedCommunities()}
+                */}
               </div>
             </div>
-            {this.sidebar()}
-            {this.landing()}
+
+            <div class="card bg-transparent border-secondary mb-3">
+              <div class="card-body">{this.sidebar()}</div>
+            </div>
+
+            <div class="card bg-transparent border-secondary">
+              <div class="card-body">{this.landing()}</div>
+            </div>
           </div>
         )}
       </div>
     );
   }
 
+  createCommunityButton() {
+    return (
+      <Link class="btn btn-secondary btn-block" to="/create_community">
+        {i18n.t('create_a_community')}
+      </Link>
+    );
+  }
+
   trendingCommunities() {
     return (
       <div>
@@ -262,6 +285,39 @@ export class Main extends Component<any, MainState> {
     );
   }
 
+  subscribedCommunities() {
+    return (
+      UserService.Instance.user &&
+      this.state.subscribedCommunities.length > 0 && (
+        <div>
+          <h5>
+            <T i18nKey="subscribed_to_communities">
+              #
+              <Link class="text-body" to="/communities">
+                #
+              </Link>
+            </T>
+          </h5>
+          <ul class="list-inline">
+            {this.state.subscribedCommunities.map(community => (
+              <li class="list-inline-item">
+                <CommunityLink
+                  community={{
+                    name: community.community_name,
+                    id: community.community_id,
+                    local: community.community_local,
+                    actor_id: community.community_actor_id,
+                    icon: community.community_icon,
+                  }}
+                />
+              </li>
+            ))}
+          </ul>
+        </div>
+      )
+    );
+  }
+
   sidebar() {
     return (
       <div>
@@ -278,13 +334,9 @@ export class Main extends Component<any, MainState> {
   }
 
   updateUrl(paramUpdates: UrlParams) {
-    const listingTypeStr =
-      paramUpdates.listingType ||
-      ListingType[this.state.listingType].toLowerCase();
-    const dataTypeStr =
-      paramUpdates.dataType || DataType[this.state.dataType].toLowerCase();
-    const sortStr =
-      paramUpdates.sort || SortType[this.state.sort].toLowerCase();
+    const listingTypeStr = paramUpdates.listingType || this.state.listingType;
+    const dataTypeStr = paramUpdates.dataType || DataType[this.state.dataType];
+    const sortStr = paramUpdates.sort || this.state.sort;
     const page = paramUpdates.page || this.state.page;
     this.props.history.push(
       `/home/data_type/${dataTypeStr}/listing_type/${listingTypeStr}/sort/${sortStr}/page/${page}`
@@ -294,136 +346,146 @@ export class Main extends Component<any, MainState> {
   siteInfo() {
     return (
       <div>
-        <div class="card border-secondary mb-3">
-          <div class="card-body">
-            <h5 class="mb-0">{`${this.state.siteRes.site.name}`}</h5>
-            {this.canAdmin && (
-              <ul class="list-inline mb-1 text-muted font-weight-bold">
-                <li className="list-inline-item-action">
-                  <span
-                    class="pointer"
-                    onClick={linkEvent(this, this.handleEditClick)}
-                    data-tippy-content={i18n.t('edit')}
-                  >
-                    <svg class="icon icon-inline">
-                      <use xlinkHref="#icon-edit"></use>
-                    </svg>
-                  </span>
-                </li>
-              </ul>
-            )}
-            <ul class="my-2 list-inline">
-              {/*
-              <li className="list-inline-item badge badge-secondary">
-                {i18n.t('number_online', { count: this.state.siteRes.online })}
-              </li>
-              */}
-              <li className="list-inline-item badge badge-secondary">
-                {i18n.t('number_of_users', {
-                  count: this.state.siteRes.site.number_of_users,
-                })}
-              </li>
-              <li className="list-inline-item badge badge-secondary">
-                {i18n.t('number_of_communities', {
-                  count: this.state.siteRes.site.number_of_communities,
-                })}
-              </li>
-              <li className="list-inline-item badge badge-secondary">
-                {i18n.t('number_of_posts', {
-                  count: this.state.siteRes.site.number_of_posts,
-                })}
-              </li>
-              <li className="list-inline-item badge badge-secondary">
-                {i18n.t('number_of_comments', {
-                  count: this.state.siteRes.site.number_of_comments,
-                })}
-              </li>
-              <li className="list-inline-item">
-                <Link className="badge badge-secondary" to="/modlog">
-                  {i18n.t('modlog')}
-                </Link>
-              </li>
-            </ul>
-            <ul class="mt-1 list-inline small mb-0">
-              <li class="list-inline-item">{i18n.t('admins')}:</li>
-              {this.state.siteRes.admins.map(admin => (
-                <li class="list-inline-item">
-                  <UserListing
-                    user={{
-                      name: admin.name,
-                      avatar: admin.avatar,
-                      local: admin.local,
-                      actor_id: admin.actor_id,
-                      id: admin.id,
-                    }}
-                  />
-                </li>
-              ))}
-            </ul>
-          </div>
-        </div>
-        {this.state.siteRes.site.description && (
-          <div class="card border-secondary mb-3">
-            <div class="card-body">
-              <div
-                className="md-div"
-                dangerouslySetInnerHTML={mdToHtml(
-                  this.state.siteRes.site.description
-                )}
-              />
-            </div>
-          </div>
-        )}
+        {this.state.siteRes.site.description && this.siteDescription()}
+        {this.badges()}
+        {this.admins()}
       </div>
     );
   }
 
+  siteName() {
+    return <h5 class="mb-0">{`${this.state.siteRes.site.name}`}</h5>;
+  }
+
+  admins() {
+    return (
+      <ul class="mt-1 list-inline small mb-0">
+        <li class="list-inline-item">{i18n.t('admins')}:</li>
+        {this.state.siteRes.admins.map(admin => (
+          <li class="list-inline-item">
+            <UserListing
+              user={{
+                name: admin.name,
+                preferred_username: admin.preferred_username,
+                avatar: admin.avatar,
+                local: admin.local,
+                actor_id: admin.actor_id,
+                id: admin.id,
+              }}
+            />
+          </li>
+        ))}
+      </ul>
+    );
+  }
+
+  badges() {
+    return (
+      <ul class="my-2 list-inline">
+        <li className="list-inline-item badge badge-light">
+          {i18n.t('number_online', { count: this.state.siteRes.online })}
+        </li>
+        <li className="list-inline-item badge badge-light">
+          {i18n.t('number_of_users', {
+            count: this.state.siteRes.site.number_of_users,
+          })}
+        </li>
+        <li className="list-inline-item badge badge-light">
+          {i18n.t('number_of_communities', {
+            count: this.state.siteRes.site.number_of_communities,
+          })}
+        </li>
+        <li className="list-inline-item badge badge-light">
+          {i18n.t('number_of_posts', {
+            count: this.state.siteRes.site.number_of_posts,
+          })}
+        </li>
+        <li className="list-inline-item badge badge-light">
+          {i18n.t('number_of_comments', {
+            count: this.state.siteRes.site.number_of_comments,
+          })}
+        </li>
+        <li className="list-inline-item">
+          <Link className="badge badge-light" to="/modlog">
+            {i18n.t('modlog')}
+          </Link>
+        </li>
+      </ul>
+    );
+  }
+
+  adminButtons() {
+    return (
+      this.canAdmin && (
+        <ul class="list-inline mb-1 text-muted font-weight-bold">
+          <li className="list-inline-item-action">
+            <span
+              class="pointer"
+              onClick={linkEvent(this, this.handleEditClick)}
+              data-tippy-content={i18n.t('edit')}
+            >
+              <svg class="icon icon-inline">
+                <use xlinkHref="#icon-edit"></use>
+              </svg>
+            </span>
+          </li>
+        </ul>
+      )
+    );
+  }
+
+  siteDescription() {
+    return (
+      <div
+        className="md-div"
+        dangerouslySetInnerHTML={mdToHtml(this.state.siteRes.site.description)}
+      />
+    );
+  }
+
   landing() {
     return (
-      <div class="card border-secondary">
-        <div class="card-body">
-          <h5>
-            {i18n.t('powered_by')}
-            <svg class="icon mx-2">
-              <use xlinkHref="#icon-mouse">#</use>
-            </svg>
-            <a href={repoUrl}>
-              Lemmy<sup>beta</sup>
+      <>
+        <h5>
+          {i18n.t('powered_by')}
+          <svg class="icon mx-2">
+            <use xlinkHref="#icon-mouse">#</use>
+          </svg>
+          <a href={repoUrl}>
+            Lemmy<sup>beta</sup>
+          </a>
+        </h5>
+        <p class="mb-0">
+          <T i18nKey="landing_0">
+            #
+            <a href="https://en.wikipedia.org/wiki/Social_network_aggregation">
+              #
             </a>
-          </h5>
-          <p class="mb-0">
-            <T i18nKey="landing_0">
+            <a href="https://en.wikipedia.org/wiki/Fediverse">#</a>
+            <br class="big"></br>
+            <code>#</code>
+            <br></br>
+            <b>#</b>
+            <br class="big"></br>
+            <a href={repoUrl}>#</a>
+            <br class="big"></br>
+            <a href="https://www.rust-lang.org">#</a>
+            <a href="https://actix.rs/">#</a>
+            <a href="https://infernojs.org">#</a>
+            <a href="https://www.typescriptlang.org/">#</a>
+            <br class="big"></br>
+            <a href="https://github.com/LemmyNet/lemmy/graphs/contributors?type=a">
               #
-              <a href="https://en.wikipedia.org/wiki/Social_network_aggregation">
-                #
-              </a>
-              <a href="https://en.wikipedia.org/wiki/Fediverse">#</a>
-              <br class="big"></br>
-              <code>#</code>
-              <br></br>
-              <b>#</b>
-              <br class="big"></br>
-              <a href={repoUrl}>#</a>
-              <br class="big"></br>
-              <a href="https://www.rust-lang.org">#</a>
-              <a href="https://actix.rs/">#</a>
-              <a href="https://infernojs.org">#</a>
-              <a href="https://www.typescriptlang.org/">#</a>
-              <br class="big"></br>
-              <a href="https://github.com/LemmyNet/lemmy/graphs/contributors?type=a">
-                #
-              </a>
-            </T>
-          </p>
-        </div>
-      </div>
+            </a>
+          </T>
+        </p>
+      </>
     );
   }
 
   posts() {
     return (
       <div class="main-content-wrapper">
-        {this.selects()}
         {this.state.loading ? (
           <h5>
             <svg class="icon icon-spinner spin">
@@ -432,6 +494,7 @@ export class Main extends Component<any, MainState> {
           </h5>
         ) : (
           <div>
+            {this.selects()}
             {this.listings()}
             {this.paginator()}
           </div>
@@ -482,7 +545,7 @@ export class Main extends Component<any, MainState> {
         </span>
         {this.state.listingType == ListingType.All && (
           <a
-            href={`/feeds/all.xml?sort=${SortType[this.state.sort]}`}
+            href={`/feeds/all.xml?sort=${this.state.sort}`}
             target="_blank"
             rel="noopener"
             title="RSS"
@@ -495,9 +558,7 @@ export class Main extends Component<any, MainState> {
         {UserService.Instance.user &&
           this.state.listingType == ListingType.Subscribed && (
             <a
-              href={`/feeds/front/${UserService.Instance.auth}.xml?sort=${
-                SortType[this.state.sort]
-              }`}
+              href={`/feeds/front/${UserService.Instance.auth}.xml?sort=${this.state.sort}`}
               target="_blank"
               title="RSS"
               rel="noopener"
@@ -516,7 +577,7 @@ export class Main extends Component<any, MainState> {
       <div class="my-2">
         {this.state.page > 1 && (
           <button
-            class="btn btn-sm btn-secondary mr-1"
+            class="btn btn-secondary mr-1"
             onClick={linkEvent(this, this.prevPage)}
           >
             {i18n.t('prev')}
@@ -524,7 +585,7 @@ export class Main extends Component<any, MainState> {
         )}
         {this.state.posts.length > 0 && (
           <button
-            class="btn btn-sm btn-secondary"
+            class="btn btn-secondary"
             onClick={linkEvent(this, this.nextPage)}
           >
             {i18n.t('next')}
@@ -564,17 +625,17 @@ export class Main extends Component<any, MainState> {
   }
 
   handleSortChange(val: SortType) {
-    this.updateUrl({ sort: SortType[val].toLowerCase(), page: 1 });
+    this.updateUrl({ sort: val, page: 1 });
     window.scrollTo(0, 0);
   }
 
   handleListingTypeChange(val: ListingType) {
-    this.updateUrl({ listingType: ListingType[val].toLowerCase(), page: 1 });
+    this.updateUrl({ listingType: val, page: 1 });
     window.scrollTo(0, 0);
   }
 
   handleDataTypeChange(val: DataType) {
-    this.updateUrl({ dataType: DataType[val].toLowerCase(), page: 1 });
+    this.updateUrl({ dataType: DataType[val], page: 1 });
     window.scrollTo(0, 0);
   }
 
@@ -583,16 +644,16 @@ export class Main extends Component<any, MainState> {
       let getPostsForm: GetPostsForm = {
         page: this.state.page,
         limit: fetchLimit,
-        sort: SortType[this.state.sort],
-        type_: ListingType[this.state.listingType],
+        sort: this.state.sort,
+        type_: this.state.listingType,
       };
       WebSocketService.Instance.getPosts(getPostsForm);
     } else {
       let getCommentsForm: GetCommentsForm = {
         page: this.state.page,
         limit: fetchLimit,
-        sort: SortType[this.state.sort],
-        type_: ListingType[this.state.listingType],
+        sort: this.state.sort,
+        type_: this.state.listingType,
       };
       WebSocketService.Instance.getComments(getCommentsForm);
     }
@@ -626,7 +687,6 @@ export class Main extends Component<any, MainState> {
       this.state.siteRes.banned = data.banned;
       this.state.siteRes.online = data.online;
       this.setState(this.state);
-      document.title = `${this.state.siteRes.site.name}`;
     } else if (res.op == UserOperation.EditSite) {
       let data = res.data as SiteResponse;
       this.state.siteRes.site = data.site;
@@ -650,6 +710,7 @@ export class Main extends Component<any, MainState> {
             .includes(data.post.community_id)
         ) {
           this.state.posts.unshift(data.post);
+          notifyPost(data.post, this.context.router);
         }
       } else {
         // NSFW posts
@@ -663,6 +724,7 @@ export class Main extends Component<any, MainState> {
             UserService.Instance.user.show_nsfw)
         ) {
           this.state.posts.unshift(data.post);
+          notifyPost(data.post, this.context.router);
         }
       }
       this.setState(this.state);
@@ -701,7 +763,11 @@ export class Main extends Component<any, MainState> {
       this.state.comments = data.comments;
       this.state.loading = false;
       this.setState(this.state);
-    } else if (res.op == UserOperation.EditComment) {
+    } else if (
+      res.op == UserOperation.EditComment ||
+      res.op == UserOperation.DeleteComment ||
+      res.op == UserOperation.RemoveComment
+    ) {
       let data = res.data as CommentResponse;
       editCommentRes(data, this.state.comments);
       this.setState(this.state);
diff --git a/ui/src/components/markdown-textarea.tsx b/ui/src/components/markdown-textarea.tsx
new file mode 100644 (file)
index 0000000..0a7f904
--- /dev/null
@@ -0,0 +1,543 @@
+import { Component, linkEvent } from 'inferno';
+import { Prompt } from 'inferno-router';
+import {
+  mdToHtml,
+  randomStr,
+  markdownHelpUrl,
+  toast,
+  setupTribute,
+  pictrsDeleteToast,
+  setupTippy,
+} from '../utils';
+import { UserService } from '../services';
+import autosize from 'autosize';
+import Tribute from 'tributejs/src/Tribute.js';
+import { i18n } from '../i18next';
+
+interface MarkdownTextAreaProps {
+  initialContent: string;
+  finished?: boolean;
+  buttonTitle?: string;
+  replyType?: boolean;
+  focus?: boolean;
+  disabled?: boolean;
+  maxLength?: number;
+  onSubmit?(msg: { val: string; formId: string }): any;
+  onContentChange?(val: string): any;
+  onReplyCancel?(): any;
+  hideNavigationWarnings?: boolean;
+}
+
+interface MarkdownTextAreaState {
+  content: string;
+  previewMode: boolean;
+  loading: boolean;
+  imageLoading: boolean;
+}
+
+export class MarkdownTextArea extends Component<
+  MarkdownTextAreaProps,
+  MarkdownTextAreaState
+> {
+  private id = `comment-textarea-${randomStr()}`;
+  private formId = `comment-form-${randomStr()}`;
+  private tribute: Tribute;
+  private emptyState: MarkdownTextAreaState = {
+    content: this.props.initialContent,
+    previewMode: false,
+    loading: false,
+    imageLoading: false,
+  };
+
+  constructor(props: any, context: any) {
+    super(props, context);
+
+    this.tribute = setupTribute();
+    this.state = this.emptyState;
+  }
+
+  componentDidMount() {
+    let textarea: any = document.getElementById(this.id);
+    if (textarea) {
+      autosize(textarea);
+      this.tribute.attach(textarea);
+      textarea.addEventListener('tribute-replaced', () => {
+        this.state.content = textarea.value;
+        this.setState(this.state);
+        autosize.update(textarea);
+      });
+
+      this.quoteInsert();
+
+      if (this.props.focus) {
+        textarea.focus();
+      }
+
+      // TODO this is slow for some reason
+      setupTippy();
+    }
+  }
+
+  componentDidUpdate() {
+    if (!this.props.hideNavigationWarnings && this.state.content) {
+      window.onbeforeunload = () => true;
+    } else {
+      window.onbeforeunload = undefined;
+    }
+  }
+
+  componentWillReceiveProps(nextProps: MarkdownTextAreaProps) {
+    if (nextProps.finished) {
+      this.state.previewMode = false;
+      this.state.loading = false;
+      this.state.content = '';
+      this.setState(this.state);
+      if (this.props.replyType) {
+        this.props.onReplyCancel();
+      }
+
+      let textarea: any = document.getElementById(this.id);
+      let form: any = document.getElementById(this.formId);
+      form.reset();
+      setTimeout(() => autosize.update(textarea), 10);
+      this.setState(this.state);
+    }
+  }
+
+  componentWillUnmount() {
+    window.onbeforeunload = null;
+  }
+
+  render() {
+    return (
+      <form id={this.formId} onSubmit={linkEvent(this, this.handleSubmit)}>
+        <Prompt
+          when={!this.props.hideNavigationWarnings && this.state.content}
+          message={i18n.t('block_leaving')}
+        />
+        <div class="form-group row">
+          <div className={`col-sm-12`}>
+            <textarea
+              id={this.id}
+              className={`form-control ${this.state.previewMode && 'd-none'}`}
+              value={this.state.content}
+              onInput={linkEvent(this, this.handleContentChange)}
+              onPaste={linkEvent(this, this.handleImageUploadPaste)}
+              required
+              disabled={this.props.disabled}
+              rows={2}
+              maxLength={this.props.maxLength || 10000}
+            />
+            {this.state.previewMode && (
+              <div
+                className="card bg-transparent border-secondary card-body md-div"
+                dangerouslySetInnerHTML={mdToHtml(this.state.content)}
+              />
+            )}
+          </div>
+        </div>
+        <div class="row">
+          <div class="col-sm-12 d-flex flex-wrap">
+            {this.props.buttonTitle && (
+              <button
+                type="submit"
+                class="btn btn-sm btn-secondary mr-2"
+                disabled={this.props.disabled || this.state.loading}
+              >
+                {this.state.loading ? (
+                  <svg class="icon icon-spinner spin">
+                    <use xlinkHref="#icon-spinner"></use>
+                  </svg>
+                ) : (
+                  <span>{this.props.buttonTitle}</span>
+                )}
+              </button>
+            )}
+            {this.props.replyType && (
+              <button
+                type="button"
+                class="btn btn-sm btn-secondary mr-2"
+                onClick={linkEvent(this, this.handleReplyCancel)}
+              >
+                {i18n.t('cancel')}
+              </button>
+            )}
+            {this.state.content && (
+              <button
+                className={`btn btn-sm btn-secondary mr-2 ${
+                  this.state.previewMode && 'active'
+                }`}
+                onClick={linkEvent(this, this.handlePreviewToggle)}
+              >
+                {i18n.t('preview')}
+              </button>
+            )}
+            {/* A flex expander */}
+            <div class="flex-grow-1"></div>
+            <button
+              class="btn btn-sm text-muted"
+              data-tippy-content={i18n.t('bold')}
+              onClick={linkEvent(this, this.handleInsertBold)}
+            >
+              <svg class="icon icon-inline">
+                <use xlinkHref="#icon-bold"></use>
+              </svg>
+            </button>
+            <button
+              class="btn btn-sm text-muted"
+              data-tippy-content={i18n.t('italic')}
+              onClick={linkEvent(this, this.handleInsertItalic)}
+            >
+              <svg class="icon icon-inline">
+                <use xlinkHref="#icon-italic"></use>
+              </svg>
+            </button>
+            <button
+              class="btn btn-sm text-muted"
+              data-tippy-content={i18n.t('link')}
+              onClick={linkEvent(this, this.handleInsertLink)}
+            >
+              <svg class="icon icon-inline">
+                <use xlinkHref="#icon-link"></use>
+              </svg>
+            </button>
+            <form class="btn btn-sm text-muted font-weight-bold">
+              <label
+                htmlFor={`file-upload-${this.id}`}
+                className={`mb-0 ${UserService.Instance.user && 'pointer'}`}
+                data-tippy-content={i18n.t('upload_image')}
+              >
+                {this.state.imageLoading ? (
+                  <svg class="icon icon-spinner spin">
+                    <use xlinkHref="#icon-spinner"></use>
+                  </svg>
+                ) : (
+                  <svg class="icon icon-inline">
+                    <use xlinkHref="#icon-image"></use>
+                  </svg>
+                )}
+              </label>
+              <input
+                id={`file-upload-${this.id}`}
+                type="file"
+                accept="image/*,video/*"
+                name="file"
+                class="d-none"
+                disabled={!UserService.Instance.user}
+                onChange={linkEvent(this, this.handleImageUpload)}
+              />
+            </form>
+            <button
+              class="btn btn-sm text-muted"
+              data-tippy-content={i18n.t('header')}
+              onClick={linkEvent(this, this.handleInsertHeader)}
+            >
+              <svg class="icon icon-inline">
+                <use xlinkHref="#icon-header"></use>
+              </svg>
+            </button>
+            <button
+              class="btn btn-sm text-muted"
+              data-tippy-content={i18n.t('strikethrough')}
+              onClick={linkEvent(this, this.handleInsertStrikethrough)}
+            >
+              <svg class="icon icon-inline">
+                <use xlinkHref="#icon-strikethrough"></use>
+              </svg>
+            </button>
+            <button
+              class="btn btn-sm text-muted"
+              data-tippy-content={i18n.t('quote')}
+              onClick={linkEvent(this, this.handleInsertQuote)}
+            >
+              <svg class="icon icon-inline">
+                <use xlinkHref="#icon-format_quote"></use>
+              </svg>
+            </button>
+            <button
+              class="btn btn-sm text-muted"
+              data-tippy-content={i18n.t('list')}
+              onClick={linkEvent(this, this.handleInsertList)}
+            >
+              <svg class="icon icon-inline">
+                <use xlinkHref="#icon-list"></use>
+              </svg>
+            </button>
+            <button
+              class="btn btn-sm text-muted"
+              data-tippy-content={i18n.t('code')}
+              onClick={linkEvent(this, this.handleInsertCode)}
+            >
+              <svg class="icon icon-inline">
+                <use xlinkHref="#icon-code"></use>
+              </svg>
+            </button>
+            <button
+              class="btn btn-sm text-muted"
+              data-tippy-content={i18n.t('subscript')}
+              onClick={linkEvent(this, this.handleInsertSubscript)}
+            >
+              <svg class="icon icon-inline">
+                <use xlinkHref="#icon-subscript"></use>
+              </svg>
+            </button>
+            <button
+              class="btn btn-sm text-muted"
+              data-tippy-content={i18n.t('superscript')}
+              onClick={linkEvent(this, this.handleInsertSuperscript)}
+            >
+              <svg class="icon icon-inline">
+                <use xlinkHref="#icon-superscript"></use>
+              </svg>
+            </button>
+            <button
+              class="btn btn-sm text-muted"
+              data-tippy-content={i18n.t('spoiler')}
+              onClick={linkEvent(this, this.handleInsertSpoiler)}
+            >
+              <svg class="icon icon-inline">
+                <use xlinkHref="#icon-alert-triangle"></use>
+              </svg>
+            </button>
+            <a
+              href={markdownHelpUrl}
+              target="_blank"
+              class="btn btn-sm text-muted font-weight-bold"
+              title={i18n.t('formatting_help')}
+              rel="noopener"
+            >
+              <svg class="icon icon-inline">
+                <use xlinkHref="#icon-help-circle"></use>
+              </svg>
+            </a>
+          </div>
+        </div>
+      </form>
+    );
+  }
+
+  handleImageUploadPaste(i: MarkdownTextArea, event: any) {
+    let image = event.clipboardData.files[0];
+    if (image) {
+      i.handleImageUpload(i, image);
+    }
+  }
+
+  handleImageUpload(i: MarkdownTextArea, event: any) {
+    let file: any;
+    if (event.target) {
+      event.preventDefault();
+      file = event.target.files[0];
+    } else {
+      file = event;
+    }
+
+    const imageUploadUrl = `/pictrs/image`;
+    const formData = new FormData();
+    formData.append('images[]', file);
+
+    i.state.imageLoading = true;
+    i.setState(i.state);
+
+    fetch(imageUploadUrl, {
+      method: 'POST',
+      body: formData,
+    })
+      .then(res => res.json())
+      .then(res => {
+        console.log('pictrs upload:');
+        console.log(res);
+        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.content;
+          content = content ? `${content}\n${imageMarkdown}` : imageMarkdown;
+          i.state.content = content;
+          i.state.imageLoading = false;
+          i.setState(i.state);
+          let textarea: any = document.getElementById(i.id);
+          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 => {
+        i.state.imageLoading = false;
+        i.setState(i.state);
+        toast(error, 'danger');
+      });
+  }
+
+  handleContentChange(i: MarkdownTextArea, event: any) {
+    i.state.content = event.target.value;
+    i.setState(i.state);
+    if (i.props.onContentChange) {
+      i.props.onContentChange(i.state.content);
+    }
+  }
+
+  handlePreviewToggle(i: MarkdownTextArea, event: any) {
+    event.preventDefault();
+    i.state.previewMode = !i.state.previewMode;
+    i.setState(i.state);
+  }
+
+  handleSubmit(i: MarkdownTextArea, event: any) {
+    event.preventDefault();
+    i.state.loading = true;
+    i.setState(i.state);
+    let msg = { val: i.state.content, formId: i.formId };
+    i.props.onSubmit(msg);
+  }
+
+  handleReplyCancel(i: MarkdownTextArea) {
+    i.props.onReplyCancel();
+  }
+
+  handleInsertLink(i: MarkdownTextArea, event: any) {
+    event.preventDefault();
+    if (!i.state.content) {
+      i.state.content = '';
+    }
+    let textarea: any = document.getElementById(i.id);
+    let start: number = textarea.selectionStart;
+    let end: number = textarea.selectionEnd;
+
+    if (start !== end) {
+      let selectedText = i.state.content.substring(start, end);
+      i.state.content = `${i.state.content.substring(
+        0,
+        start
+      )} [${selectedText}]() ${i.state.content.substring(end)}`;
+      textarea.focus();
+      setTimeout(() => (textarea.selectionEnd = end + 4), 10);
+    } else {
+      i.state.content += '[]()';
+      textarea.focus();
+      setTimeout(() => (textarea.selectionEnd -= 1), 10);
+    }
+    i.setState(i.state);
+  }
+
+  simpleSurround(chars: string) {
+    this.simpleSurroundBeforeAfter(chars, chars);
+  }
+
+  simpleSurroundBeforeAfter(beforeChars: string, afterChars: string) {
+    if (!this.state.content) {
+      this.state.content = '';
+    }
+    let textarea: any = document.getElementById(this.id);
+    let start: number = textarea.selectionStart;
+    let end: number = textarea.selectionEnd;
+
+    if (start !== end) {
+      let selectedText = this.state.content.substring(start, end);
+      this.state.content = `${this.state.content.substring(
+        0,
+        start - 1
+      )} ${beforeChars}${selectedText}${afterChars} ${this.state.content.substring(
+        end + 1
+      )}`;
+    } else {
+      this.state.content += `${beforeChars}___${afterChars}`;
+    }
+    this.setState(this.state);
+    setTimeout(() => {
+      autosize.update(textarea);
+    }, 10);
+  }
+
+  handleInsertBold(i: MarkdownTextArea, event: any) {
+    event.preventDefault();
+    i.simpleSurround('**');
+  }
+
+  handleInsertItalic(i: MarkdownTextArea, event: any) {
+    event.preventDefault();
+    i.simpleSurround('*');
+  }
+
+  handleInsertCode(i: MarkdownTextArea, event: any) {
+    event.preventDefault();
+    i.simpleSurround('`');
+  }
+
+  handleInsertStrikethrough(i: MarkdownTextArea, event: any) {
+    event.preventDefault();
+    i.simpleSurround('~~');
+  }
+
+  handleInsertList(i: MarkdownTextArea, event: any) {
+    event.preventDefault();
+    i.simpleInsert('-');
+  }
+
+  handleInsertQuote(i: MarkdownTextArea, event: any) {
+    event.preventDefault();
+    i.simpleInsert('>');
+  }
+
+  handleInsertHeader(i: MarkdownTextArea, event: any) {
+    event.preventDefault();
+    i.simpleInsert('#');
+  }
+
+  handleInsertSubscript(i: MarkdownTextArea, event: any) {
+    event.preventDefault();
+    i.simpleSurround('~');
+  }
+
+  handleInsertSuperscript(i: MarkdownTextArea, event: any) {
+    event.preventDefault();
+    i.simpleSurround('^');
+  }
+
+  simpleInsert(chars: string) {
+    if (!this.state.content) {
+      this.state.content = `${chars} `;
+    } else {
+      this.state.content += `\n${chars} `;
+    }
+
+    let textarea: any = document.getElementById(this.id);
+    textarea.focus();
+    setTimeout(() => {
+      autosize.update(textarea);
+    }, 10);
+    this.setState(this.state);
+  }
+
+  handleInsertSpoiler(i: MarkdownTextArea, event: any) {
+    event.preventDefault();
+    let beforeChars = `\n::: spoiler ${i18n.t('spoiler')}\n`;
+    let afterChars = '\n:::\n';
+    i.simpleSurroundBeforeAfter(beforeChars, afterChars);
+  }
+
+  quoteInsert() {
+    let textarea: any = document.getElementById(this.id);
+    let selectedText = window.getSelection().toString();
+    if (selectedText) {
+      let quotedText =
+        selectedText
+          .split('\n')
+          .map(t => `> ${t}`)
+          .join('\n') + '\n\n';
+      this.state.content = quotedText;
+      this.setState(this.state);
+      // Not sure why this needs a delay
+      setTimeout(() => autosize.update(textarea), 10);
+    }
+  }
+}
index 9a5131c9b4753decafbeb534d9493d7b9303a5cc..106015a4ab74550d01f5a4810916de2ca4373382 100644 (file)
@@ -1,4 +1,5 @@
 import { Component, linkEvent } from 'inferno';
+import { Helmet } from 'inferno-helmet';
 import { Link } from 'inferno-router';
 import { Subscription } from 'rxjs';
 import { retryWhen, delay, take } from 'rxjs/operators';
@@ -17,7 +18,8 @@ import {
   ModAdd,
   WebSocketJsonResponse,
   GetSiteResponse,
-} from '../interfaces';
+  Site,
+} from 'lemmy-js-client';
 import { WebSocketService } from '../services';
 import { wsJsonToRes, addTypeInfo, fetchLimit, toast } from '../utils';
 import { MomentTime } from './moment-time';
@@ -38,6 +40,7 @@ interface ModlogState {
   communityId?: number;
   communityName?: string;
   page: number;
+  site: Site;
   loading: boolean;
 }
 
@@ -47,6 +50,7 @@ export class Modlog extends Component<any, ModlogState> {
     combined: [],
     page: 1,
     loading: true,
+    site: undefined,
   };
 
   constructor(props: any, context: any) {
@@ -338,9 +342,18 @@ export class Modlog extends Component<any, ModlogState> {
     );
   }
 
+  get documentTitle(): string {
+    if (this.state.site) {
+      return `Modlog - ${this.state.site.name}`;
+    } else {
+      return 'Lemmy';
+    }
+  }
+
   render() {
     return (
       <div class="container">
+        <Helmet title={this.documentTitle} />
         {this.state.loading ? (
           <h5 class="">
             <svg class="icon icon-spinner spin">
@@ -384,14 +397,14 @@ export class Modlog extends Component<any, ModlogState> {
       <div class="mt-2">
         {this.state.page > 1 && (
           <button
-            class="btn btn-sm btn-secondary mr-1"
+            class="btn btn-secondary mr-1"
             onClick={linkEvent(this, this.prevPage)}
           >
             {i18n.t('prev')}
           </button>
         )}
         <button
-          class="btn btn-sm btn-secondary"
+          class="btn btn-secondary"
           onClick={linkEvent(this, this.nextPage)}
         >
           {i18n.t('next')}
@@ -434,7 +447,8 @@ export class Modlog extends Component<any, ModlogState> {
       this.setCombined(data);
     } else if (res.op == UserOperation.GetSite) {
       let data = res.data as GetSiteResponse;
-      document.title = `Modlog - ${data.site.name}`;
+      this.state.site = data.site;
+      this.setState(this.state);
     }
   }
 }
index a332f0e5f9ce91ad6121ffb9c4c85e90c143b692..4ef5276c36c9a72f030483f0a19c3aafe5ece433 100644 (file)
@@ -16,21 +16,20 @@ import {
   Comment,
   CommentResponse,
   PrivateMessage,
-  UserView,
   PrivateMessageResponse,
   WebSocketJsonResponse,
-} from '../interfaces';
+} from 'lemmy-js-client';
 import {
   wsJsonToRes,
   pictrsAvatarThumbnail,
   showAvatars,
   fetchLimit,
-  isCommentType,
   toast,
-  messageToastify,
-  md,
+  setTheme,
+  getLanguage,
+  notifyComment,
+  notifyPrivateMessage,
 } from '../utils';
-import { version } from '../version';
 import { i18n } from '../i18next';
 
 interface NavbarState {
@@ -40,43 +39,59 @@ interface NavbarState {
   mentions: Array<Comment>;
   messages: Array<PrivateMessage>;
   unreadCount: number;
-  siteName: string;
-  admins: Array<UserView>;
   searchParam: string;
   toggleSearch: boolean;
+  siteLoading: boolean;
+  siteRes: GetSiteResponse;
+  onSiteBanner?(url: string): any;
 }
 
 export class Navbar extends Component<any, NavbarState> {
   private wsSub: Subscription;
   private userSub: Subscription;
+  private unreadCountSub: Subscription;
   private searchTextField: RefObject<HTMLInputElement>;
   emptyState: NavbarState = {
-    isLoggedIn: UserService.Instance.user !== undefined,
+    isLoggedIn: false,
     unreadCount: 0,
     replies: [],
     mentions: [],
     messages: [],
     expanded: false,
-    siteName: undefined,
-    admins: [],
+    siteRes: {
+      site: {
+        id: null,
+        name: null,
+        creator_id: null,
+        creator_name: null,
+        published: null,
+        number_of_users: null,
+        number_of_posts: null,
+        number_of_comments: null,
+        number_of_communities: null,
+        enable_downvotes: null,
+        open_registration: null,
+        enable_nsfw: null,
+        icon: null,
+        banner: null,
+        creator_preferred_username: null,
+      },
+      my_user: null,
+      admins: [],
+      banned: [],
+      online: null,
+      version: null,
+      federated_instances: null,
+    },
     searchParam: '',
     toggleSearch: false,
+    siteLoading: true,
   };
 
   constructor(props: any, context: any) {
     super(props, context);
     this.state = this.emptyState;
 
-    // Subscribe to user changes
-    this.userSub = UserService.Instance.sub.subscribe(user => {
-      this.state.isLoggedIn = user.user !== undefined;
-      if (this.state.isLoggedIn) {
-        this.state.unreadCount = user.user.unreadCount;
-        this.requestNotificationPermission();
-      }
-      this.setState(this.state);
-    });
-
     this.wsSub = WebSocketService.Instance.subject
       .pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
       .subscribe(
@@ -85,17 +100,30 @@ export class Navbar extends Component<any, NavbarState> {
         () => console.log('complete')
       );
 
-    if (this.state.isLoggedIn) {
-      this.requestNotificationPermission();
-      // TODO couldn't get re-logging in to re-fetch unreads
-      this.fetchUnreads();
-    }
-
     WebSocketService.Instance.getSite();
 
     this.searchTextField = createRef();
   }
 
+  componentDidMount() {
+    // Subscribe to jwt changes
+    this.userSub = UserService.Instance.jwtSub.subscribe(res => {
+      // A login
+      if (res !== undefined) {
+        this.requestNotificationPermission();
+      } else {
+        this.state.isLoggedIn = false;
+      }
+      WebSocketService.Instance.getSite();
+      this.setState(this.state);
+    });
+
+    // Subscribe to unread count changes
+    this.unreadCountSub = UserService.Instance.unreadCountSub.subscribe(res => {
+      this.setState({ unreadCount: res });
+    });
+  }
+
   handleSearchParam(i: Navbar, event: any) {
     i.state.searchParam = event.target.value;
     i.setState(i.state);
@@ -109,7 +137,7 @@ export class Navbar extends Component<any, NavbarState> {
       this.context.router.history.push(`/search/`);
     } else {
       this.context.router.history.push(
-        `/search/q/${searchParam}/type/all/sort/topall/page/1`
+        `/search/q/${searchParam}/type/All/sort/TopAll/page/1`
       );
     }
   }
@@ -144,183 +172,215 @@ export class Navbar extends Component<any, NavbarState> {
   componentWillUnmount() {
     this.wsSub.unsubscribe();
     this.userSub.unsubscribe();
+    this.unreadCountSub.unsubscribe();
   }
 
   // TODO class active corresponding to current page
   navbar() {
+    let user = UserService.Instance.user;
     return (
-      <nav class="container-fluid navbar navbar-expand-md navbar-light shadow p-0 px-3">
-        <Link title={version} class="navbar-brand" to="/">
-          {this.state.siteName}
-        </Link>
-        {this.state.isLoggedIn && (
-          <Link
-            class="ml-auto p-0 navbar-toggler nav-link"
-            to="/inbox"
-            title={i18n.t('inbox')}
-          >
-            <svg class="icon">
-              <use xlinkHref="#icon-bell"></use>
-            </svg>
-            {this.state.unreadCount > 0 && (
-              <span class="ml-1 badge badge-light">
-                {this.state.unreadCount}
-              </span>
-            )}
-          </Link>
-        )}
-        <button
-          class="navbar-toggler"
-          type="button"
-          aria-label="menu"
-          onClick={linkEvent(this, this.expandNavbar)}
-          data-tippy-content={i18n.t('expand_here')}
-        >
-          <span class="navbar-toggler-icon"></span>
-        </button>
-        <div
-          className={`${!this.state.expanded && 'collapse'} navbar-collapse`}
-        >
-          <ul class="navbar-nav my-2 mr-auto">
-            <li class="nav-item">
-              <Link
-                class="nav-link"
-                to="/communities"
-                title={i18n.t('communities')}
-              >
-                {i18n.t('communities')}
-              </Link>
-            </li>
-            <li class="nav-item">
-              <Link
-                class="nav-link"
-                to={{
-                  pathname: '/create_post',
-                  state: { prevPath: this.currentLocation },
-                }}
-                title={i18n.t('create_post')}
-              >
-                {i18n.t('create_post')}
-              </Link>
-            </li>
-            <li class="nav-item">
-              <Link
-                class="nav-link"
-                to="/create_community"
-                title={i18n.t('create_community')}
-              >
-                {i18n.t('create_community')}
-              </Link>
-            </li>
-            <li className="nav-item">
-              <Link
-                class="nav-link"
-                to="/sponsors"
-                title={i18n.t('donate_to_lemmy')}
-              >
-                <svg class="icon">
-                  <use xlinkHref="#icon-coffee"></use>
-                </svg>
-              </Link>
-            </li>
-          </ul>
-          {!this.context.router.history.location.pathname.match(
-            /^\/search/
-          ) && (
-            <form
-              class="form-inline"
-              onSubmit={linkEvent(this, this.handleSearchSubmit)}
+      <nav class="navbar navbar-expand-lg navbar-light shadow-sm p-0 px-3">
+        <div class="container">
+          {!this.state.siteLoading ? (
+            <Link
+              title={this.state.siteRes.version}
+              class="d-flex align-items-center navbar-brand mr-md-3"
+              to="/"
             >
-              <input
-                class={`form-control mr-0 search-input ${
-                  this.state.toggleSearch ? 'show-input' : 'hide-input'
-                }`}
-                onInput={linkEvent(this, this.handleSearchParam)}
-                value={this.state.searchParam}
-                ref={this.searchTextField}
-                type="text"
-                placeholder={i18n.t('search')}
-                onBlur={linkEvent(this, this.handleSearchBlur)}
-              ></input>
-              <button
-                name="search-btn"
-                onClick={linkEvent(this, this.handleSearchBtn)}
-                class="btn btn-link"
-                style="color: var(--gray)"
-              >
-                <svg class="icon">
-                  <use xlinkHref="#icon-search"></use>
-                </svg>
-              </button>
-            </form>
+              {this.state.siteRes.site.icon && showAvatars() && (
+                <img
+                  src={pictrsAvatarThumbnail(this.state.siteRes.site.icon)}
+                  height="32"
+                  width="32"
+                  class="rounded-circle mr-2"
+                />
+              )}
+              {this.state.siteRes.site.name}
+            </Link>
+          ) : (
+            <div class="navbar-item">
+              <svg class="icon icon-spinner spin">
+                <use xlinkHref="#icon-spinner"></use>
+              </svg>
+            </div>
           )}
-          <ul class="navbar-nav my-2">
-            {this.canAdmin && (
-              <li className="nav-item">
-                <Link
-                  class="nav-link"
-                  to={`/admin`}
-                  title={i18n.t('admin_settings')}
-                >
-                  <svg class="icon">
-                    <use xlinkHref="#icon-settings"></use>
-                  </svg>
-                </Link>
-              </li>
-            )}
-          </ul>
-          {this.state.isLoggedIn ? (
-            <>
-              <ul class="navbar-nav my-2">
-                <li className="nav-item">
-                  <Link class="nav-link" to="/inbox" title={i18n.t('inbox')}>
-                    <svg class="icon">
-                      <use xlinkHref="#icon-bell"></use>
-                    </svg>
-                    {this.state.unreadCount > 0 && (
-                      <span class="ml-1 badge badge-light">
-                        {this.state.unreadCount}
-                      </span>
-                    )}
+          {this.state.isLoggedIn && (
+            <Link
+              class="ml-auto p-0 navbar-toggler nav-link border-0"
+              to="/inbox"
+              title={i18n.t('inbox')}
+            >
+              <svg class="icon">
+                <use xlinkHref="#icon-bell"></use>
+              </svg>
+              {this.state.unreadCount > 0 && (
+                <span class="mx-1 badge badge-light">
+                  {this.state.unreadCount}
+                </span>
+              )}
+            </Link>
+          )}
+          <button
+            class="navbar-toggler border-0 p-1"
+            type="button"
+            aria-label="menu"
+            onClick={linkEvent(this, this.expandNavbar)}
+            data-tippy-content={i18n.t('expand_here')}
+          >
+            <span class="navbar-toggler-icon"></span>
+          </button>
+          {!this.state.siteLoading && (
+            <div
+              className={`${
+                !this.state.expanded && 'collapse'
+              } navbar-collapse`}
+            >
+              <ul class="navbar-nav my-2 mr-auto">
+                <li class="nav-item">
+                  <Link
+                    class="nav-link"
+                    to="/communities"
+                    title={i18n.t('communities')}
+                  >
+                    {i18n.t('communities')}
+                  </Link>
+                </li>
+                <li class="nav-item">
+                  <Link
+                    class="nav-link"
+                    to={{
+                      pathname: '/create_post',
+                      state: { prevPath: this.currentLocation },
+                    }}
+                    title={i18n.t('create_post')}
+                  >
+                    {i18n.t('create_post')}
+                  </Link>
+                </li>
+                <li class="nav-item">
+                  <Link
+                    class="nav-link"
+                    to="/create_community"
+                    title={i18n.t('create_community')}
+                  >
+                    {i18n.t('create_community')}
                   </Link>
                 </li>
-              </ul>
-              <ul class="navbar-nav">
                 <li className="nav-item">
                   <Link
                     class="nav-link"
-                    to={`/u/${UserService.Instance.user.username}`}
-                    title={i18n.t('settings')}
+                    to="/sponsors"
+                    title={i18n.t('donate_to_lemmy')}
                   >
-                    <span>
-                      {UserService.Instance.user.avatar && showAvatars() && (
-                        <img
-                          src={pictrsAvatarThumbnail(
-                            UserService.Instance.user.avatar
-                          )}
-                          height="32"
-                          width="32"
-                          class="rounded-circle mr-2"
-                        />
-                      )}
-                      {UserService.Instance.user.username}
-                    </span>
+                    <svg class="icon">
+                      <use xlinkHref="#icon-coffee"></use>
+                    </svg>
                   </Link>
                 </li>
               </ul>
-            </>
-          ) : (
-            <ul class="navbar-nav my-2">
-              <li className="nav-item">
-                <Link
-                  class="nav-link"
-                  to="/login"
-                  title={i18n.t('login_sign_up')}
+              <ul class="navbar-nav my-2">
+                {this.canAdmin && (
+                  <li className="nav-item">
+                    <Link
+                      class="nav-link"
+                      to={`/admin`}
+                      title={i18n.t('admin_settings')}
+                    >
+                      <svg class="icon">
+                        <use xlinkHref="#icon-settings"></use>
+                      </svg>
+                    </Link>
+                  </li>
+                )}
+              </ul>
+              {!this.context.router.history.location.pathname.match(
+                /^\/search/
+              ) && (
+                <form
+                  class="form-inline"
+                  onSubmit={linkEvent(this, this.handleSearchSubmit)}
                 >
-                  {i18n.t('login_sign_up')}
-                </Link>
-              </li>
-            </ul>
+                  <input
+                    class={`form-control mr-0 search-input ${
+                      this.state.toggleSearch ? 'show-input' : 'hide-input'
+                    }`}
+                    onInput={linkEvent(this, this.handleSearchParam)}
+                    value={this.state.searchParam}
+                    ref={this.searchTextField}
+                    type="text"
+                    placeholder={i18n.t('search')}
+                    onBlur={linkEvent(this, this.handleSearchBlur)}
+                  ></input>
+                  <button
+                    name="search-btn"
+                    onClick={linkEvent(this, this.handleSearchBtn)}
+                    class="px-1 btn btn-link"
+                    style="color: var(--gray)"
+                  >
+                    <svg class="icon">
+                      <use xlinkHref="#icon-search"></use>
+                    </svg>
+                  </button>
+                </form>
+              )}
+              {this.state.isLoggedIn ? (
+                <>
+                  <ul class="navbar-nav my-2">
+                    <li className="nav-item">
+                      <Link
+                        class="nav-link"
+                        to="/inbox"
+                        title={i18n.t('inbox')}
+                      >
+                        <svg class="icon">
+                          <use xlinkHref="#icon-bell"></use>
+                        </svg>
+                        {this.state.unreadCount > 0 && (
+                          <span class="ml-1 badge badge-light">
+                            {this.state.unreadCount}
+                          </span>
+                        )}
+                      </Link>
+                    </li>
+                  </ul>
+                  <ul class="navbar-nav">
+                    <li className="nav-item">
+                      <Link
+                        class="nav-link"
+                        to={`/u/${user.name}`}
+                        title={i18n.t('settings')}
+                      >
+                        <span>
+                          {user.avatar && showAvatars() && (
+                            <img
+                              src={pictrsAvatarThumbnail(user.avatar)}
+                              height="32"
+                              width="32"
+                              class="rounded-circle mr-2"
+                            />
+                          )}
+                          {user.preferred_username
+                            ? user.preferred_username
+                            : user.name}
+                        </span>
+                      </Link>
+                    </li>
+                  </ul>
+                </>
+              ) : (
+                <ul class="navbar-nav my-2">
+                  <li className="ml-2 nav-item">
+                    <Link
+                      class="btn btn-success"
+                      to="/login"
+                      title={i18n.t('login_sign_up')}
+                    >
+                      {i18n.t('login_sign_up')}
+                    </Link>
+                  </li>
+                </ul>
+              )}
+            </div>
           )}
         </div>
       </nav>
@@ -375,7 +435,7 @@ export class Navbar extends Component<any, NavbarState> {
           this.state.unreadCount++;
           this.setState(this.state);
           this.sendUnreadCount();
-          this.notify(data.comment);
+          notifyComment(data.comment, this.context.router);
         }
       }
     } else if (res.op == UserOperation.CreatePrivateMessage) {
@@ -387,47 +447,59 @@ export class Navbar extends Component<any, NavbarState> {
           this.state.unreadCount++;
           this.setState(this.state);
           this.sendUnreadCount();
-          this.notify(data.message);
+          notifyPrivateMessage(data.message, this.context.router);
         }
       }
     } else if (res.op == UserOperation.GetSite) {
       let data = res.data as GetSiteResponse;
 
-      if (data.site && !this.state.siteName) {
-        this.state.siteName = data.site.name;
-        this.state.admins = data.admins;
-        this.setState(this.state);
+      this.state.siteRes = data;
+
+      // The login
+      if (data.my_user) {
+        UserService.Instance.user = data.my_user;
+        WebSocketService.Instance.userJoin();
+        // On the first load, check the unreads
+        if (this.state.isLoggedIn == false) {
+          this.requestNotificationPermission();
+          this.fetchUnreads();
+          setTheme(data.my_user.theme, true);
+          i18n.changeLanguage(getLanguage());
+        }
+        this.state.isLoggedIn = true;
       }
+
+      this.state.siteLoading = false;
+      this.setState(this.state);
     }
   }
 
   fetchUnreads() {
-    if (this.state.isLoggedIn) {
-      let repliesForm: GetRepliesForm = {
-        sort: SortType[SortType.New],
-        unread_only: true,
-        page: 1,
-        limit: fetchLimit,
-      };
-
-      let userMentionsForm: GetUserMentionsForm = {
-        sort: SortType[SortType.New],
-        unread_only: true,
-        page: 1,
-        limit: fetchLimit,
-      };
-
-      let privateMessagesForm: GetPrivateMessagesForm = {
-        unread_only: true,
-        page: 1,
-        limit: fetchLimit,
-      };
-
-      if (this.currentLocation !== '/inbox') {
-        WebSocketService.Instance.getReplies(repliesForm);
-        WebSocketService.Instance.getUserMentions(userMentionsForm);
-        WebSocketService.Instance.getPrivateMessages(privateMessagesForm);
-      }
+    console.log('Fetching unreads...');
+    let repliesForm: GetRepliesForm = {
+      sort: SortType.New,
+      unread_only: true,
+      page: 1,
+      limit: fetchLimit,
+    };
+
+    let userMentionsForm: GetUserMentionsForm = {
+      sort: SortType.New,
+      unread_only: true,
+      page: 1,
+      limit: fetchLimit,
+    };
+
+    let privateMessagesForm: GetPrivateMessagesForm = {
+      unread_only: true,
+      page: 1,
+      limit: fetchLimit,
+    };
+
+    if (this.currentLocation !== '/inbox') {
+      WebSocketService.Instance.getReplies(repliesForm);
+      WebSocketService.Instance.getUserMentions(userMentionsForm);
+      WebSocketService.Instance.getPrivateMessages(privateMessagesForm);
     }
   }
 
@@ -436,10 +508,7 @@ export class Navbar extends Component<any, NavbarState> {
   }
 
   sendUnreadCount() {
-    UserService.Instance.user.unreadCount = this.state.unreadCount;
-    UserService.Instance.sub.next({
-      user: UserService.Instance.user,
-    });
+    UserService.Instance.unreadCountSub.next(this.state.unreadCount);
   }
 
   calculateUnreadCount(): number {
@@ -453,7 +522,9 @@ export class Navbar extends Component<any, NavbarState> {
   get canAdmin(): boolean {
     return (
       UserService.Instance.user &&
-      this.state.admins.map(a => a.id).includes(UserService.Instance.user.id)
+      this.state.siteRes.admins
+        .map(a => a.id)
+        .includes(UserService.Instance.user.id)
     );
   }
 
@@ -470,37 +541,4 @@ export class Navbar extends Component<any, NavbarState> {
       });
     }
   }
-
-  notify(reply: Comment | PrivateMessage) {
-    let creator_name = reply.creator_name;
-    let creator_avatar = reply.creator_avatar
-      ? reply.creator_avatar
-      : `${window.location.protocol}//${window.location.host}/static/assets/apple-touch-icon.png`;
-    let link = isCommentType(reply)
-      ? `/post/${reply.post_id}/comment/${reply.id}`
-      : `/inbox`;
-    let htmlBody = md.render(reply.content);
-    let body = reply.content; // Unfortunately the notifications API can't do html
-
-    messageToastify(
-      creator_name,
-      creator_avatar,
-      htmlBody,
-      link,
-      this.context.router
-    );
-
-    if (Notification.permission !== 'granted') Notification.requestPermission();
-    else {
-      var notification = new Notification(reply.creator_name, {
-        icon: creator_avatar,
-        body: body,
-      });
-
-      notification.onclick = () => {
-        event.preventDefault();
-        this.context.router.history.push(link);
-      };
-    }
-  }
 }
index f33e9af25cb390306d29e3cf00999017878d9767..527f21e045e463362746789269c16b9deeed1a3b 100644 (file)
@@ -1,4 +1,5 @@
 import { Component, linkEvent } from 'inferno';
+import { Helmet } from 'inferno-helmet';
 import { Subscription } from 'rxjs';
 import { retryWhen, delay, take } from 'rxjs/operators';
 import {
@@ -7,7 +8,8 @@ import {
   PasswordChangeForm,
   WebSocketJsonResponse,
   GetSiteResponse,
-} from '../interfaces';
+  Site,
+} from 'lemmy-js-client';
 import { WebSocketService, UserService } from '../services';
 import { wsJsonToRes, capitalizeFirstLetter, toast } from '../utils';
 import { i18n } from '../i18next';
@@ -15,6 +17,7 @@ import { i18n } from '../i18next';
 interface State {
   passwordChangeForm: PasswordChangeForm;
   loading: boolean;
+  site: Site;
 }
 
 export class PasswordChange extends Component<any, State> {
@@ -27,6 +30,7 @@ export class PasswordChange extends Component<any, State> {
       password_verify: undefined,
     },
     loading: false,
+    site: undefined,
   };
 
   constructor(props: any, context: any) {
@@ -48,9 +52,18 @@ export class PasswordChange extends Component<any, State> {
     this.subscription.unsubscribe();
   }
 
+  get documentTitle(): string {
+    if (this.state.site) {
+      return `${i18n.t('password_change')} - ${this.state.site.name}`;
+    } else {
+      return 'Lemmy';
+    }
+  }
+
   render() {
     return (
       <div class="container">
+        <Helmet title={this.documentTitle} />
         <div class="row">
           <div class="col-12 col-lg-6 offset-lg-3 mb-4">
             <h5>{i18n.t('password_change')}</h5>
@@ -142,7 +155,8 @@ export class PasswordChange extends Component<any, State> {
       this.props.history.push('/');
     } else if (res.op == UserOperation.GetSite) {
       let data = res.data as GetSiteResponse;
-      document.title = `${i18n.t('password_change')} - ${data.site.name}`;
+      this.state.site = data.site;
+      this.setState(this.state);
     }
   }
 }
index e5efeaac508e5e8d439878dbeb410a7e4ec594d6..97b44f5fac584cac79b4fed24de9f17c1c73f096 100644 (file)
@@ -1,6 +1,7 @@
 import { Component, linkEvent } from 'inferno';
 import { Prompt } from 'inferno-router';
 import { PostListings } from './post-listings';
+import { MarkdownTextArea } from './markdown-textarea';
 import { Subscription } from 'rxjs';
 import { retryWhen, delay, take } from 'rxjs/operators';
 import {
@@ -17,29 +18,23 @@ import {
   SearchType,
   SearchResponse,
   WebSocketJsonResponse,
-} from '../interfaces';
+} from 'lemmy-js-client';
 import { WebSocketService, UserService } from '../services';
 import {
   wsJsonToRes,
   getPageTitle,
   validURL,
   capitalizeFirstLetter,
-  markdownHelpUrl,
   archiveUrl,
-  mdToHtml,
   debounce,
   isImage,
   toast,
   randomStr,
-  setupTribute,
   setupTippy,
   hostname,
   pictrsDeleteToast,
   validTitle,
 } from '../utils';
-import autosize from 'autosize';
-import Tribute from 'tributejs/src/Tribute.js';
-import emojiShortName from 'emoji-short-name';
 import Choices from 'choices.js';
 import { i18n } from '../i18next';
 
@@ -68,7 +63,6 @@ interface PostFormState {
 
 export class PostForm extends Component<PostFormProps, PostFormState> {
   private id = `post-form-${randomStr()}`;
-  private tribute: Tribute;
   private subscription: Subscription;
   private choices: Choices;
   private emptyState: PostFormState = {
@@ -77,9 +71,6 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
       nsfw: false,
       auth: null,
       community_id: null,
-      creator_id: UserService.Instance.user
-        ? UserService.Instance.user.id
-        : null,
     },
     communities: [],
     loading: false,
@@ -94,8 +85,7 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
     super(props, context);
     this.fetchSimilarPosts = debounce(this.fetchSimilarPosts).bind(this);
     this.fetchPageTitle = debounce(this.fetchPageTitle).bind(this);
-
-    this.tribute = setupTribute();
+    this.handlePostBodyChange = this.handlePostBodyChange.bind(this);
 
     this.state = this.emptyState;
 
@@ -106,7 +96,6 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
         name: this.props.post.name,
         community_id: this.props.post.community_id,
         edit_id: this.props.post.id,
-        creator_id: this.props.post.creator_id,
         url: this.props.post.url,
         nsfw: this.props.post.nsfw,
         auth: null,
@@ -132,7 +121,7 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
       );
 
     let listCommunitiesForm: ListCommunitiesForm = {
-      sort: SortType[SortType.TopAll],
+      sort: SortType.TopAll,
       limit: 9999,
     };
 
@@ -140,14 +129,6 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
   }
 
   componentDidMount() {
-    var textarea: any = document.getElementById(this.id);
-    autosize(textarea);
-    this.tribute.attach(textarea);
-    textarea.addEventListener('tribute-replaced', () => {
-      this.state.postForm.body = textarea.value;
-      this.setState(this.state);
-      autosize.update(textarea);
-    });
     setupTippy();
   }
 
@@ -166,7 +147,7 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
 
   componentWillUnmount() {
     this.subscription.unsubscribe();
-    this.choices && this.choices.destroy();
+    /* this.choices && this.choices.destroy(); */
     window.onbeforeunload = null;
   }
 
@@ -305,41 +286,10 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
               {i18n.t('body')}
             </label>
             <div class="col-sm-10">
-              <textarea
-                id={this.id}
-                value={this.state.postForm.body}
-                onInput={linkEvent(this, this.handlePostBodyChange)}
-                className={`form-control ${this.state.previewMode && 'd-none'}`}
-                rows={4}
-                maxLength={10000}
+              <MarkdownTextArea
+                initialContent={this.state.postForm.body}
+                onContentChange={this.handlePostBodyChange}
               />
-              {this.state.previewMode && (
-                <div
-                  className="card card-body md-div"
-                  dangerouslySetInnerHTML={mdToHtml(this.state.postForm.body)}
-                />
-              )}
-              {this.state.postForm.body && (
-                <button
-                  className={`mt-1 mr-2 btn btn-sm btn-secondary ${
-                    this.state.previewMode && 'active'
-                  }`}
-                  onClick={linkEvent(this, this.handlePreviewToggle)}
-                >
-                  {i18n.t('preview')}
-                </button>
-              )}
-              <a
-                href={markdownHelpUrl}
-                target="_blank"
-                rel="noopener"
-                class="d-inline-block float-right text-muted font-weight-bold"
-                title={i18n.t('formatting_help')}
-              >
-                <svg class="icon icon-inline">
-                  <use xlinkHref="#icon-help-circle"></use>
-                </svg>
-              </a>
             </div>
           </div>
           {!this.props.post && (
@@ -455,8 +405,8 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
     if (validURL(this.state.postForm.url)) {
       let form: SearchForm = {
         q: this.state.postForm.url,
-        type_: SearchType[SearchType.Url],
-        sort: SortType[SortType.TopAll],
+        type_: SearchType.Url,
+        sort: SortType.TopAll,
         page: 1,
         limit: 6,
       };
@@ -483,8 +433,8 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
   fetchSimilarPosts() {
     let form: SearchForm = {
       q: this.state.postForm.name,
-      type_: SearchType[SearchType.Posts],
-      sort: SortType[SortType.TopAll],
+      type_: SearchType.Posts,
+      sort: SortType.TopAll,
       community_id: this.state.postForm.community_id,
       page: 1,
       limit: 6,
@@ -499,9 +449,9 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
     this.setState(this.state);
   }
 
-  handlePostBodyChange(i: PostForm, event: any) {
-    i.state.postForm.body = event.target.value;
-    i.setState(i.state);
+  handlePostBodyChange(val: string) {
+    this.state.postForm.body = val;
+    this.setState(this.state);
   }
 
   handlePostCommunityChange(i: PostForm, event: any) {
index 418fe7b486a6c64e301b16f536091a43c23d77ee..fa4bf391177359df89f823244b21e72c6a925c5e 100644 (file)
@@ -4,18 +4,21 @@ import { WebSocketService, UserService } from '../services';
 import {
   Post,
   CreatePostLikeForm,
-  PostForm as PostFormI,
+  DeletePostForm,
+  RemovePostForm,
+  LockPostForm,
+  StickyPostForm,
   SavePostForm,
   CommunityUser,
   UserView,
-  BanType,
   BanFromCommunityForm,
   BanUserForm,
   AddModToCommunityForm,
   AddAdminForm,
   TransferSiteForm,
   TransferCommunityForm,
-} from '../interfaces';
+} from 'lemmy-js-client';
+import { BanType } from '../interfaces';
 import { MomentTime } from './moment-time';
 import { PostForm } from './post-form';
 import { IFramelyCard } from './iframely-card';
@@ -33,7 +36,6 @@ import {
   setupTippy,
   hostname,
   previewLines,
-  toast,
 } from '../utils';
 import { i18n } from '../i18next';
 
@@ -42,6 +44,7 @@ interface PostListingState {
   showRemoveDialog: boolean;
   removeReason: string;
   showBanDialog: boolean;
+  removeData: boolean;
   banReason: string;
   banExpires: string;
   banType: BanType;
@@ -72,6 +75,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
     showRemoveDialog: false,
     removeReason: null,
     showBanDialog: false,
+    removeData: null,
     banReason: null,
     banExpires: null,
     banType: BanType.Community,
@@ -101,6 +105,9 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
     this.state.upvotes = nextProps.post.upvotes;
     this.state.downvotes = nextProps.post.downvotes;
     this.state.score = nextProps.post.score;
+    if (this.props.post.id !== nextProps.post.id) {
+      this.state.imageExpanded = false;
+    }
     this.setState(this.state);
   }
 
@@ -158,7 +165,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
     return (
       <img
         className={`img-fluid thumbnail rounded ${
-          (post.nsfw || post.community_nsfw) && 'img-blur'
+          post.nsfw || post.community_nsfw ? 'img-blur' : ''
         }`}
         src={src}
       />
@@ -185,8 +192,8 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
 
     if (isImage(post.url)) {
       return (
-        <span
-          class="text-body pointer"
+        <div
+          class="float-right text-body pointer d-inline-block position-relative"
           data-tippy-content={i18n.t('expand_here')}
           onClick={linkEvent(this, this.handleImageExpandClick)}
         >
@@ -194,12 +201,12 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
           <svg class="icon mini-overlay">
             <use xlinkHref="#icon-image"></use>
           </svg>
-        </span>
+        </div>
       );
     } else if (post.thumbnail_url) {
       return (
         <a
-          className="text-body"
+          class="float-right text-body d-inline-block position-relative"
           href={post.url}
           target="_blank"
           rel="noopener"
@@ -235,9 +242,11 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
             title={post.url}
             rel="noopener"
           >
-            <svg class="icon thumbnail">
-              <use xlinkHref="#icon-external-link"></use>
-            </svg>
+            <div class="thumbnail rounded bg-light d-flex justify-content-center">
+              <svg class="icon d-flex align-items-center">
+                <use xlinkHref="#icon-external-link"></use>
+              </svg>
+            </div>
           </a>
         );
       }
@@ -248,698 +257,794 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
           to={`/post/${post.id}`}
           title={i18n.t('comments')}
         >
-          <svg class="icon thumbnail">
-            <use xlinkHref="#icon-message-square"></use>
-          </svg>
+          <div class="thumbnail rounded bg-light d-flex justify-content-center">
+            <svg class="icon d-flex align-items-center">
+              <use xlinkHref="#icon-message-square"></use>
+            </svg>
+          </div>
         </Link>
       );
     }
   }
 
-  listing() {
+  createdLine() {
     let post = this.props.post;
     return (
-      <div class="row">
-        <div className={`vote-bar col-1 pr-0 small text-center`}>
+      <ul class="list-inline mb-1 text-muted small">
+        <li className="list-inline-item">
+          <UserListing
+            user={{
+              name: post.creator_name,
+              preferred_username: post.creator_preferred_username,
+              avatar: post.creator_avatar,
+              id: post.creator_id,
+              local: post.creator_local,
+              actor_id: post.creator_actor_id,
+              published: post.creator_published,
+            }}
+          />
+
+          {this.isMod && (
+            <span className="mx-1 badge badge-light">{i18n.t('mod')}</span>
+          )}
+          {this.isAdmin && (
+            <span className="mx-1 badge badge-light">{i18n.t('admin')}</span>
+          )}
+          {(post.banned_from_community || post.banned) && (
+            <span className="mx-1 badge badge-danger">{i18n.t('banned')}</span>
+          )}
+          {this.props.showCommunity && (
+            <span>
+              <span class="mx-1"> {i18n.t('to')} </span>
+              <CommunityLink
+                community={{
+                  name: post.community_name,
+                  id: post.community_id,
+                  local: post.community_local,
+                  actor_id: post.community_actor_id,
+                  icon: post.community_icon,
+                }}
+              />
+            </span>
+          )}
+        </li>
+        <li className="list-inline-item">•</li>
+        {post.url && !(hostname(post.url) == window.location.hostname) && (
+          <>
+            <li className="list-inline-item">
+              <a
+                className="text-muted font-italic"
+                href={post.url}
+                target="_blank"
+                title={post.url}
+                rel="noopener"
+              >
+                {hostname(post.url)}
+              </a>
+            </li>
+            <li className="list-inline-item">•</li>
+          </>
+        )}
+        <li className="list-inline-item">
+          <span>
+            <MomentTime data={post} />
+          </span>
+        </li>
+        {post.body && (
+          <>
+            <li className="list-inline-item">•</li>
+            <li className="list-inline-item">
+              {/* Using a link with tippy doesn't work on touch devices unfortunately */}
+              <Link
+                className="text-muted"
+                data-tippy-content={md.render(previewLines(post.body))}
+                data-tippy-allowHtml={true}
+                to={`/post/${post.id}`}
+              >
+                <svg class="mr-1 icon icon-inline">
+                  <use xlinkHref="#icon-book-open"></use>
+                </svg>
+              </Link>
+            </li>
+          </>
+        )}
+      </ul>
+    );
+  }
+
+  voteBar() {
+    return (
+      <div className={`vote-bar col-1 pr-0 small text-center`}>
+        <button
+          className={`btn-animate btn btn-link p-0 ${
+            this.state.my_vote == 1 ? 'text-info' : 'text-muted'
+          }`}
+          onClick={linkEvent(this, this.handlePostLike)}
+          data-tippy-content={i18n.t('upvote')}
+        >
+          <svg class="icon upvote">
+            <use xlinkHref="#icon-arrow-up1"></use>
+          </svg>
+        </button>
+        <div
+          class={`unselectable pointer font-weight-bold text-muted px-1`}
+          data-tippy-content={this.pointsTippy}
+        >
+          {this.state.score}
+        </div>
+        {this.props.enableDownvotes && (
           <button
             className={`btn-animate btn btn-link p-0 ${
-              this.state.my_vote == 1 ? 'text-info' : 'text-muted'
+              this.state.my_vote == -1 ? 'text-danger' : 'text-muted'
             }`}
-            onClick={linkEvent(this, this.handlePostLike)}
-            data-tippy-content={i18n.t('upvote')}
+            onClick={linkEvent(this, this.handlePostDisLike)}
+            data-tippy-content={i18n.t('downvote')}
           >
-            <svg class="icon upvote">
-              <use xlinkHref="#icon-arrow-up1"></use>
+            <svg class="icon downvote">
+              <use xlinkHref="#icon-arrow-down1"></use>
             </svg>
           </button>
-          <div
-            class={`unselectable pointer font-weight-bold text-muted px-1`}
-            data-tippy-content={this.pointsTippy}
-          >
-            {this.state.score}
-          </div>
-          {this.props.enableDownvotes && (
-            <button
-              className={`btn-animate btn btn-link p-0 ${
-                this.state.my_vote == -1 ? 'text-danger' : 'text-muted'
-              }`}
-              onClick={linkEvent(this, this.handlePostDisLike)}
-              data-tippy-content={i18n.t('downvote')}
+        )}
+      </div>
+    );
+  }
+
+  postTitleLine() {
+    let post = this.props.post;
+    return (
+      <div className="post-title overflow-hidden">
+        <h5>
+          {this.props.showBody && post.url ? (
+            <a
+              className={!post.stickied ? 'text-body' : 'text-primary'}
+              href={post.url}
+              target="_blank"
+              title={post.url}
+              rel="noopener"
+            >
+              {post.name}
+            </a>
+          ) : (
+            <Link
+              className={!post.stickied ? 'text-body' : 'text-primary'}
+              to={`/post/${post.id}`}
+              title={i18n.t('comments')}
+            >
+              {post.name}
+            </Link>
+          )}
+          {(isImage(post.url) || this.props.post.thumbnail_url) && (
+            <>
+              {!this.state.imageExpanded ? (
+                <span
+                  class="text-monospace unselectable pointer ml-2 text-muted small"
+                  data-tippy-content={i18n.t('expand_here')}
+                  onClick={linkEvent(this, this.handleImageExpandClick)}
+                >
+                  <svg class="icon icon-inline">
+                    <use xlinkHref="#icon-plus-square"></use>
+                  </svg>
+                </span>
+              ) : (
+                <span>
+                  <span
+                    class="text-monospace unselectable pointer ml-2 text-muted small"
+                    onClick={linkEvent(this, this.handleImageExpandClick)}
+                  >
+                    <svg class="icon icon-inline">
+                      <use xlinkHref="#icon-minus-square"></use>
+                    </svg>
+                  </span>
+                  <div>
+                    <span
+                      class="pointer"
+                      onClick={linkEvent(this, this.handleImageExpandClick)}
+                    >
+                      <img
+                        class="img-fluid img-expanded"
+                        src={this.getImage()}
+                      />
+                    </span>
+                  </div>
+                </span>
+              )}
+            </>
+          )}
+          {post.removed && (
+            <small className="ml-2 text-muted font-italic">
+              {i18n.t('removed')}
+            </small>
+          )}
+          {post.deleted && (
+            <small
+              className="unselectable pointer ml-2 text-muted font-italic"
+              data-tippy-content={i18n.t('deleted')}
             >
-              <svg class="icon downvote">
-                <use xlinkHref="#icon-arrow-down1"></use>
+              <svg class={`icon icon-inline text-danger`}>
+                <use xlinkHref="#icon-trash"></use>
               </svg>
-            </button>
+            </small>
           )}
-        </div>
-        {!this.state.imageExpanded && (
-          <div class="col-3 col-sm-2 pr-0 mt-1">
-            <div class="position-relative">{this.thumbnail()}</div>
-          </div>
+          {post.locked && (
+            <small
+              className="unselectable pointer ml-2 text-muted font-italic"
+              data-tippy-content={i18n.t('locked')}
+            >
+              <svg class={`icon icon-inline text-danger`}>
+                <use xlinkHref="#icon-lock"></use>
+              </svg>
+            </small>
+          )}
+          {post.stickied && (
+            <small
+              className="unselectable pointer ml-2 text-muted font-italic"
+              data-tippy-content={i18n.t('stickied')}
+            >
+              <svg class={`icon icon-inline text-primary`}>
+                <use xlinkHref="#icon-pin"></use>
+              </svg>
+            </small>
+          )}
+          {post.nsfw && (
+            <small className="ml-2 text-muted font-italic">
+              {i18n.t('nsfw')}
+            </small>
+          )}
+        </h5>
+      </div>
+    );
+  }
+
+  commentsLine(showVotes: boolean = false) {
+    let post = this.props.post;
+    return (
+      <ul class="d-flex align-items-center list-inline mb-1 text-muted small">
+        <li className="list-inline-item">
+          <Link
+            className="text-muted"
+            title={i18n.t('number_of_comments', {
+              count: post.number_of_comments,
+            })}
+            to={`/post/${post.id}`}
+          >
+            <svg class="mr-1 icon icon-inline">
+              <use xlinkHref="#icon-message-square"></use>
+            </svg>
+            {i18n.t('number_of_comments', {
+              count: post.number_of_comments,
+            })}
+          </Link>
+        </li>
+        {(showVotes || this.state.upvotes !== this.state.score) && (
+          <>
+            <span
+              class="unselectable pointer ml-3"
+              data-tippy-content={this.pointsTippy}
+            >
+              <li className="list-inline-item">
+                <a
+                  className={`btn-animate btn btn-link p-0 ${
+                    this.state.my_vote == 1 ? 'text-info' : 'text-muted'
+                  }`}
+                  onClick={linkEvent(this, this.handlePostLike)}
+                >
+                  <svg class="small icon icon-inline mx-1">
+                    <use xlinkHref="#icon-arrow-up1"></use>
+                  </svg>
+                  {this.state.upvotes}
+                </a>
+              </li>
+              <li className="list-inline-item">
+                <a
+                  className={`btn-animate btn btn-link p-0 ${
+                    this.state.my_vote == -1 ? 'text-danger' : 'text-muted'
+                  }`}
+                  onClick={linkEvent(this, this.handlePostDisLike)}
+                >
+                  <svg class="small icon icon-inline mx-1">
+                    <use xlinkHref="#icon-arrow-down1"></use>
+                  </svg>
+                  {this.state.downvotes}
+                </a>
+              </li>
+            </span>
+          </>
         )}
-        <div
-          class={`${this.state.imageExpanded ? 'col-12' : 'col-8 col-sm-9'}`}
-        >
-          <div class="row">
-            <div className="col-12">
-              <div className="post-title">
-                <h5 className="mb-0 d-inline">
-                  {this.props.showBody && post.url ? (
-                    <a
-                      className="text-body"
-                      href={post.url}
-                      target="_blank"
-                      title={post.url}
-                      rel="noopener"
-                    >
-                      {post.name}
-                    </a>
-                  ) : (
-                    <Link
-                      className="text-body"
-                      to={`/post/${post.id}`}
-                      title={i18n.t('comments')}
-                    >
-                      {post.name}
-                    </Link>
-                  )}
-                </h5>
-                {post.url && !(hostname(post.url) == window.location.hostname) && (
-                  <small class="d-inline-block">
-                    <a
-                      className="ml-2 text-muted font-italic"
-                      href={post.url}
-                      target="_blank"
-                      title={post.url}
-                      rel="noopener"
-                    >
-                      {hostname(post.url)}
-                      <svg class="ml-1 icon icon-inline">
-                        <use xlinkHref="#icon-external-link"></use>
-                      </svg>
-                    </a>
-                  </small>
-                )}
-                {(isImage(post.url) || this.props.post.thumbnail_url) && (
-                  <>
-                    {!this.state.imageExpanded ? (
-                      <span
-                        class="text-monospace unselectable pointer ml-2 text-muted small"
-                        data-tippy-content={i18n.t('expand_here')}
-                        onClick={linkEvent(this, this.handleImageExpandClick)}
-                      >
-                        <svg class="icon icon-inline">
-                          <use xlinkHref="#icon-plus-square"></use>
-                        </svg>
-                      </span>
-                    ) : (
-                      <span>
-                        <span
-                          class="text-monospace unselectable pointer ml-2 text-muted small"
-                          onClick={linkEvent(this, this.handleImageExpandClick)}
-                        >
-                          <svg class="icon icon-inline">
-                            <use xlinkHref="#icon-minus-square"></use>
-                          </svg>
-                        </span>
-                        <div>
-                          <span
-                            class="pointer"
-                            onClick={linkEvent(
-                              this,
-                              this.handleImageExpandClick
-                            )}
-                          >
-                            <img
-                              class="img-fluid img-expanded"
-                              src={this.getImage()}
-                            />
-                          </span>
-                        </div>
-                      </span>
-                    )}
-                  </>
-                )}
-                {post.removed && (
-                  <small className="ml-2 text-muted font-italic">
-                    {i18n.t('removed')}
-                  </small>
-                )}
-                {post.deleted && (
-                  <small
-                    className="unselectable pointer ml-2 text-muted font-italic"
-                    data-tippy-content={i18n.t('deleted')}
+      </ul>
+    );
+  }
+
+  duplicatesLine() {
+    return (
+      this.props.post.duplicates && (
+        <ul class="list-inline mb-1 small text-muted">
+          <>
+            <li className="list-inline-item mr-2">
+              {i18n.t('cross_posted_to')}
+            </li>
+            {this.props.post.duplicates.map(post => (
+              <li className="list-inline-item mr-2">
+                <Link to={`/post/${post.id}`}>{post.community_name}</Link>
+              </li>
+            ))}
+          </>
+        </ul>
+      )
+    );
+  }
+
+  postActions() {
+    let post = this.props.post;
+    return (
+      <ul class="list-inline mb-1 text-muted font-weight-bold">
+        {UserService.Instance.user && (
+          <>
+            {this.props.showBody && (
+              <>
+                <li className="list-inline-item">
+                  <button
+                    class="btn btn-link btn-animate text-muted"
+                    onClick={linkEvent(this, this.handleSavePostClick)}
+                    data-tippy-content={
+                      post.saved ? i18n.t('unsave') : i18n.t('save')
+                    }
                   >
-                    <svg class={`icon icon-inline text-danger`}>
-                      <use xlinkHref="#icon-trash"></use>
+                    <svg
+                      class={`icon icon-inline ${post.saved && 'text-warning'}`}
+                    >
+                      <use xlinkHref="#icon-star"></use>
                     </svg>
-                  </small>
-                )}
-                {post.locked && (
-                  <small
-                    className="unselectable pointer ml-2 text-muted font-italic"
-                    data-tippy-content={i18n.t('locked')}
+                  </button>
+                </li>
+                <li className="list-inline-item">
+                  <Link
+                    class="btn btn-link btn-animate text-muted"
+                    to={`/create_post${this.crossPostParams}`}
+                    title={i18n.t('cross_post')}
                   >
-                    <svg class={`icon icon-inline text-danger`}>
-                      <use xlinkHref="#icon-lock"></use>
+                    <svg class="icon icon-inline">
+                      <use xlinkHref="#icon-copy"></use>
                     </svg>
-                  </small>
-                )}
-                {post.stickied && (
-                  <small
-                    className="unselectable pointer ml-2 text-muted font-italic"
-                    data-tippy-content={i18n.t('stickied')}
+                  </Link>
+                </li>
+              </>
+            )}
+            {this.myPost && this.props.showBody && (
+              <>
+                <li className="list-inline-item">
+                  <button
+                    class="btn btn-link btn-animate text-muted"
+                    onClick={linkEvent(this, this.handleEditClick)}
+                    data-tippy-content={i18n.t('edit')}
                   >
-                    <svg class={`icon icon-inline text-success`}>
-                      <use xlinkHref="#icon-pin"></use>
+                    <svg class="icon icon-inline">
+                      <use xlinkHref="#icon-edit"></use>
                     </svg>
-                  </small>
-                )}
-                {post.nsfw && (
-                  <small className="ml-2 text-muted font-italic">
-                    {i18n.t('nsfw')}
-                  </small>
-                )}
-              </div>
-            </div>
-          </div>
-          <div class="row">
-            <div className="details col-12">
-              <ul class="list-inline mb-0 text-muted small">
-                <li className="list-inline-item">
-                  <span>{i18n.t('by')} </span>
-                  <UserListing
-                    user={{
-                      name: post.creator_name,
-                      avatar: post.creator_avatar,
-                      id: post.creator_id,
-                      local: post.creator_local,
-                      actor_id: post.creator_actor_id,
-                      published: post.creator_published,
-                    }}
-                  />
-
-                  {this.isMod && (
-                    <span className="mx-1 badge badge-light">
-                      {i18n.t('mod')}
-                    </span>
-                  )}
-                  {this.isAdmin && (
-                    <span className="mx-1 badge badge-light">
-                      {i18n.t('admin')}
-                    </span>
-                  )}
-                  {(post.banned_from_community || post.banned) && (
-                    <span className="mx-1 badge badge-danger">
-                      {i18n.t('banned')}
-                    </span>
-                  )}
-                  {this.props.showCommunity && (
-                    <span>
-                      <span> {i18n.t('to')} </span>
-                      <CommunityLink
-                        community={{
-                          name: post.community_name,
-                          id: post.community_id,
-                          local: post.community_local,
-                          actor_id: post.community_actor_id,
-                        }}
-                      />
-                    </span>
-                  )}
+                  </button>
                 </li>
-                <li className="list-inline-item">•</li>
                 <li className="list-inline-item">
-                  <span>
-                    <MomentTime data={post} />
-                  </span>
+                  <button
+                    class="btn btn-link btn-animate text-muted"
+                    onClick={linkEvent(this, this.handleDeleteClick)}
+                    data-tippy-content={
+                      !post.deleted ? i18n.t('delete') : i18n.t('restore')
+                    }
+                  >
+                    <svg
+                      class={`icon icon-inline ${
+                        post.deleted && 'text-danger'
+                      }`}
+                    >
+                      <use xlinkHref="#icon-trash"></use>
+                    </svg>
+                  </button>
                 </li>
-                {post.body && (
+              </>
+            )}
+
+            {!this.state.showAdvanced && this.props.showBody ? (
+              <li className="list-inline-item">
+                <button
+                  class="btn btn-link btn-animate text-muted"
+                  onClick={linkEvent(this, this.handleShowAdvanced)}
+                  data-tippy-content={i18n.t('more')}
+                >
+                  <svg class="icon icon-inline">
+                    <use xlinkHref="#icon-more-vertical"></use>
+                  </svg>
+                </button>
+              </li>
+            ) : (
+              <>
+                {this.props.showBody && post.body && (
+                  <li className="list-inline-item">
+                    <button
+                      class="btn btn-link btn-animate text-muted"
+                      onClick={linkEvent(this, this.handleViewSource)}
+                      data-tippy-content={i18n.t('view_source')}
+                    >
+                      <svg
+                        class={`icon icon-inline ${
+                          this.state.viewSource && 'text-success'
+                        }`}
+                      >
+                        <use xlinkHref="#icon-file-text"></use>
+                      </svg>
+                    </button>
+                  </li>
+                )}
+                {this.canModOnSelf && (
                   <>
-                    <li className="list-inline-item">•</li>
                     <li className="list-inline-item">
-                      {/* Using a link with tippy doesn't work on touch devices unfortunately */}
-                      <Link
-                        className="text-muted"
-                        data-tippy-content={md.render(previewLines(post.body))}
-                        data-tippy-allowHtml={true}
-                        to={`/post/${post.id}`}
+                      <button
+                        class="btn btn-link btn-animate text-muted"
+                        onClick={linkEvent(this, this.handleModLock)}
+                        data-tippy-content={
+                          post.locked ? i18n.t('unlock') : i18n.t('lock')
+                        }
+                      >
+                        <svg
+                          class={`icon icon-inline ${
+                            post.locked && 'text-danger'
+                          }`}
+                        >
+                          <use xlinkHref="#icon-lock"></use>
+                        </svg>
+                      </button>
+                    </li>
+                    <li className="list-inline-item">
+                      <button
+                        class="btn btn-link btn-animate text-muted"
+                        onClick={linkEvent(this, this.handleModSticky)}
+                        data-tippy-content={
+                          post.stickied ? i18n.t('unsticky') : i18n.t('sticky')
+                        }
                       >
-                        <svg class="mr-1 icon icon-inline">
-                          <use xlinkHref="#icon-book-open"></use>
+                        <svg
+                          class={`icon icon-inline ${
+                            post.stickied && 'text-success'
+                          }`}
+                        >
+                          <use xlinkHref="#icon-pin"></use>
                         </svg>
-                      </Link>
+                      </button>
                     </li>
                   </>
                 )}
-                <li className="list-inline-item">•</li>
-                {this.state.upvotes !== this.state.score && (
+                {/* Mods can ban from community, and appoint as mods to community */}
+                {(this.canMod || this.canAdmin) && (
+                  <li className="list-inline-item">
+                    {!post.removed ? (
+                      <span
+                        class="pointer"
+                        onClick={linkEvent(this, this.handleModRemoveShow)}
+                      >
+                        {i18n.t('remove')}
+                      </span>
+                    ) : (
+                      <span
+                        class="pointer"
+                        onClick={linkEvent(this, this.handleModRemoveSubmit)}
+                      >
+                        {i18n.t('restore')}
+                      </span>
+                    )}
+                  </li>
+                )}
+                {this.canMod && (
                   <>
-                    <span
-                      class="unselectable pointer mr-2"
-                      data-tippy-content={this.pointsTippy}
-                    >
+                    {!this.isMod && (
                       <li className="list-inline-item">
-                        <span className="text-muted">
-                          <svg class="small icon icon-inline mr-1">
-                            <use xlinkHref="#icon-arrow-up"></use>
-                          </svg>
-                          {this.state.upvotes}
-                        </span>
+                        {!post.banned_from_community ? (
+                          <span
+                            class="pointer"
+                            onClick={linkEvent(
+                              this,
+                              this.handleModBanFromCommunityShow
+                            )}
+                          >
+                            {i18n.t('ban')}
+                          </span>
+                        ) : (
+                          <span
+                            class="pointer"
+                            onClick={linkEvent(
+                              this,
+                              this.handleModBanFromCommunitySubmit
+                            )}
+                          >
+                            {i18n.t('unban')}
+                          </span>
+                        )}
                       </li>
+                    )}
+                    {!post.banned_from_community && post.creator_local && (
                       <li className="list-inline-item">
-                        <span className="text-muted">
-                          <svg class="small icon icon-inline mr-1">
-                            <use xlinkHref="#icon-arrow-down"></use>
-                          </svg>
-                          {this.state.downvotes}
+                        <span
+                          class="pointer"
+                          onClick={linkEvent(
+                            this,
+                            this.handleAddModToCommunity
+                          )}
+                        >
+                          {this.isMod
+                            ? i18n.t('remove_as_mod')
+                            : i18n.t('appoint_as_mod')}
                         </span>
                       </li>
-                    </span>
-                    <li className="list-inline-item">•</li>
+                    )}
                   </>
                 )}
-                <li className="list-inline-item">
-                  <Link
-                    className="text-muted"
-                    title={i18n.t('number_of_comments', {
-                      count: post.number_of_comments,
-                    })}
-                    to={`/post/${post.id}`}
-                  >
-                    <svg class="mr-1 icon icon-inline">
-                      <use xlinkHref="#icon-message-square"></use>
-                    </svg>
-                    {post.number_of_comments}
-                  </Link>
-                </li>
-              </ul>
-              {this.props.post.duplicates && (
-                <ul class="list-inline mb-1 small text-muted">
-                  <>
-                    <li className="list-inline-item mr-2">
-                      {i18n.t('cross_posted_to')}
-                    </li>
-                    {this.props.post.duplicates.map(post => (
-                      <li className="list-inline-item mr-2">
-                        <Link to={`/post/${post.id}`}>
-                          {post.community_name}
-                        </Link>
-                      </li>
-                    ))}
-                  </>
-                </ul>
-              )}
-              <ul class="list-inline mb-1 text-muted font-weight-bold">
-                {UserService.Instance.user && (
-                  <>
-                    {this.props.showBody && (
-                      <>
-                        <li className="list-inline-item">
-                          <button
-                            class="btn btn-sm btn-link btn-animate text-muted"
-                            onClick={linkEvent(this, this.handleSavePostClick)}
-                            data-tippy-content={
-                              post.saved ? i18n.t('unsave') : i18n.t('save')
-                            }
+                {/* Community creators and admins can transfer community to another mod */}
+                {(this.amCommunityCreator || this.canAdmin) &&
+                  this.isMod &&
+                  post.creator_local && (
+                    <li className="list-inline-item">
+                      {!this.state.showConfirmTransferCommunity ? (
+                        <span
+                          class="pointer"
+                          onClick={linkEvent(
+                            this,
+                            this.handleShowConfirmTransferCommunity
+                          )}
+                        >
+                          {i18n.t('transfer_community')}
+                        </span>
+                      ) : (
+                        <>
+                          <span class="d-inline-block mr-1">
+                            {i18n.t('are_you_sure')}
+                          </span>
+                          <span
+                            class="pointer d-inline-block mr-1"
+                            onClick={linkEvent(
+                              this,
+                              this.handleTransferCommunity
+                            )}
                           >
-                            <svg
-                              class={`icon icon-inline ${
-                                post.saved && 'text-warning'
-                              }`}
-                            >
-                              <use xlinkHref="#icon-star"></use>
-                            </svg>
-                          </button>
-                        </li>
-                        <li className="list-inline-item">
-                          <Link
-                            class="btn btn-sm btn-link btn-animate text-muted"
-                            to={`/create_post${this.crossPostParams}`}
-                            title={i18n.t('cross_post')}
+                            {i18n.t('yes')}
+                          </span>
+                          <span
+                            class="pointer d-inline-block"
+                            onClick={linkEvent(
+                              this,
+                              this.handleCancelShowConfirmTransferCommunity
+                            )}
                           >
-                            <svg class="icon icon-inline">
-                              <use xlinkHref="#icon-copy"></use>
-                            </svg>
-                          </Link>
-                        </li>
-                      </>
-                    )}
-                    {this.myPost && this.props.showBody && (
-                      <>
-                        <li className="list-inline-item">
-                          <button
-                            class="btn btn-sm btn-link btn-animate text-muted"
-                            onClick={linkEvent(this, this.handleEditClick)}
-                            data-tippy-content={i18n.t('edit')}
+                            {i18n.t('no')}
+                          </span>
+                        </>
+                      )}
+                    </li>
+                  )}
+                {/* Admins can ban from all, and appoint other admins */}
+                {this.canAdmin && (
+                  <>
+                    {!this.isAdmin && (
+                      <li className="list-inline-item">
+                        {!post.banned ? (
+                          <span
+                            class="pointer"
+                            onClick={linkEvent(this, this.handleModBanShow)}
                           >
-                            <svg class="icon icon-inline">
-                              <use xlinkHref="#icon-edit"></use>
-                            </svg>
-                          </button>
-                        </li>
-                        <li className="list-inline-item">
-                          <button
-                            class="btn btn-sm btn-link btn-animate text-muted"
-                            onClick={linkEvent(this, this.handleDeleteClick)}
-                            data-tippy-content={
-                              !post.deleted
-                                ? i18n.t('delete')
-                                : i18n.t('restore')
-                            }
+                            {i18n.t('ban_from_site')}
+                          </span>
+                        ) : (
+                          <span
+                            class="pointer"
+                            onClick={linkEvent(this, this.handleModBanSubmit)}
                           >
-                            <svg
-                              class={`icon icon-inline ${
-                                post.deleted && 'text-danger'
-                              }`}
-                            >
-                              <use xlinkHref="#icon-trash"></use>
-                            </svg>
-                          </button>
-                        </li>
-                      </>
+                            {i18n.t('unban_from_site')}
+                          </span>
+                        )}
+                      </li>
                     )}
-
-                    {!this.state.showAdvanced && this.props.showBody ? (
+                    {!post.banned && post.creator_local && (
                       <li className="list-inline-item">
-                        <button
-                          class="btn btn-sm btn-link btn-animate text-muted"
-                          onClick={linkEvent(this, this.handleShowAdvanced)}
-                          data-tippy-content={i18n.t('more')}
+                        <span
+                          class="pointer"
+                          onClick={linkEvent(this, this.handleAddAdmin)}
                         >
-                          <svg class="icon icon-inline">
-                            <use xlinkHref="#icon-more-vertical"></use>
-                          </svg>
-                        </button>
+                          {this.isAdmin
+                            ? i18n.t('remove_as_admin')
+                            : i18n.t('appoint_as_admin')}
+                        </span>
                       </li>
+                    )}
+                  </>
+                )}
+                {/* Site Creator can transfer to another admin */}
+                {this.amSiteCreator && this.isAdmin && (
+                  <li className="list-inline-item">
+                    {!this.state.showConfirmTransferSite ? (
+                      <span
+                        class="pointer"
+                        onClick={linkEvent(
+                          this,
+                          this.handleShowConfirmTransferSite
+                        )}
+                      >
+                        {i18n.t('transfer_site')}
+                      </span>
                     ) : (
                       <>
-                        {this.props.showBody && post.body && (
-                          <li className="list-inline-item">
-                            <button
-                              class="btn btn-sm btn-link btn-animate text-muted"
-                              onClick={linkEvent(this, this.handleViewSource)}
-                              data-tippy-content={i18n.t('view_source')}
-                            >
-                              <svg
-                                class={`icon icon-inline ${
-                                  this.state.viewSource && 'text-success'
-                                }`}
-                              >
-                                <use xlinkHref="#icon-file-text"></use>
-                              </svg>
-                            </button>
-                          </li>
-                        )}
-                        {this.canModOnSelf && (
-                          <>
-                            <li className="list-inline-item">
-                              <button
-                                class="btn btn-sm btn-link btn-animate text-muted"
-                                onClick={linkEvent(this, this.handleModLock)}
-                                data-tippy-content={
-                                  post.locked
-                                    ? i18n.t('unlock')
-                                    : i18n.t('lock')
-                                }
-                              >
-                                <svg
-                                  class={`icon icon-inline ${
-                                    post.locked && 'text-danger'
-                                  }`}
-                                >
-                                  <use xlinkHref="#icon-lock"></use>
-                                </svg>
-                              </button>
-                            </li>
-                            <li className="list-inline-item">
-                              <button
-                                class="btn btn-sm btn-link btn-animate text-muted"
-                                onClick={linkEvent(this, this.handleModSticky)}
-                                data-tippy-content={
-                                  post.stickied
-                                    ? i18n.t('unsticky')
-                                    : i18n.t('sticky')
-                                }
-                              >
-                                <svg
-                                  class={`icon icon-inline ${
-                                    post.stickied && 'text-success'
-                                  }`}
-                                >
-                                  <use xlinkHref="#icon-pin"></use>
-                                </svg>
-                              </button>
-                            </li>
-                          </>
-                        )}
-                        {/* Mods can ban from community, and appoint as mods to community */}
-                        {(this.canMod || this.canAdmin) && (
-                          <li className="list-inline-item">
-                            {!post.removed ? (
-                              <span
-                                class="pointer"
-                                onClick={linkEvent(
-                                  this,
-                                  this.handleModRemoveShow
-                                )}
-                              >
-                                {i18n.t('remove')}
-                              </span>
-                            ) : (
-                              <span
-                                class="pointer"
-                                onClick={linkEvent(
-                                  this,
-                                  this.handleModRemoveSubmit
-                                )}
-                              >
-                                {i18n.t('restore')}
-                              </span>
-                            )}
-                          </li>
-                        )}
-                        {this.canMod && (
-                          <>
-                            {!this.isMod && (
-                              <li className="list-inline-item">
-                                {!post.banned_from_community ? (
-                                  <span
-                                    class="pointer"
-                                    onClick={linkEvent(
-                                      this,
-                                      this.handleModBanFromCommunityShow
-                                    )}
-                                  >
-                                    {i18n.t('ban')}
-                                  </span>
-                                ) : (
-                                  <span
-                                    class="pointer"
-                                    onClick={linkEvent(
-                                      this,
-                                      this.handleModBanFromCommunitySubmit
-                                    )}
-                                  >
-                                    {i18n.t('unban')}
-                                  </span>
-                                )}
-                              </li>
-                            )}
-                            {!post.banned_from_community && (
-                              <li className="list-inline-item">
-                                <span
-                                  class="pointer"
-                                  onClick={linkEvent(
-                                    this,
-                                    this.handleAddModToCommunity
-                                  )}
-                                >
-                                  {this.isMod
-                                    ? i18n.t('remove_as_mod')
-                                    : i18n.t('appoint_as_mod')}
-                                </span>
-                              </li>
-                            )}
-                          </>
-                        )}
-                        {/* Community creators and admins can transfer community to another mod */}
-                        {(this.amCommunityCreator || this.canAdmin) &&
-                          this.isMod && (
-                            <li className="list-inline-item">
-                              {!this.state.showConfirmTransferCommunity ? (
-                                <span
-                                  class="pointer"
-                                  onClick={linkEvent(
-                                    this,
-                                    this.handleShowConfirmTransferCommunity
-                                  )}
-                                >
-                                  {i18n.t('transfer_community')}
-                                </span>
-                              ) : (
-                                <>
-                                  <span class="d-inline-block mr-1">
-                                    {i18n.t('are_you_sure')}
-                                  </span>
-                                  <span
-                                    class="pointer d-inline-block mr-1"
-                                    onClick={linkEvent(
-                                      this,
-                                      this.handleTransferCommunity
-                                    )}
-                                  >
-                                    {i18n.t('yes')}
-                                  </span>
-                                  <span
-                                    class="pointer d-inline-block"
-                                    onClick={linkEvent(
-                                      this,
-                                      this
-                                        .handleCancelShowConfirmTransferCommunity
-                                    )}
-                                  >
-                                    {i18n.t('no')}
-                                  </span>
-                                </>
-                              )}
-                            </li>
+                        <span class="d-inline-block mr-1">
+                          {i18n.t('are_you_sure')}
+                        </span>
+                        <span
+                          class="pointer d-inline-block mr-1"
+                          onClick={linkEvent(this, this.handleTransferSite)}
+                        >
+                          {i18n.t('yes')}
+                        </span>
+                        <span
+                          class="pointer d-inline-block"
+                          onClick={linkEvent(
+                            this,
+                            this.handleCancelShowConfirmTransferSite
                           )}
-                        {/* Admins can ban from all, and appoint other admins */}
-                        {this.canAdmin && (
-                          <>
-                            {!this.isAdmin && (
-                              <li className="list-inline-item">
-                                {!post.banned ? (
-                                  <span
-                                    class="pointer"
-                                    onClick={linkEvent(
-                                      this,
-                                      this.handleModBanShow
-                                    )}
-                                  >
-                                    {i18n.t('ban_from_site')}
-                                  </span>
-                                ) : (
-                                  <span
-                                    class="pointer"
-                                    onClick={linkEvent(
-                                      this,
-                                      this.handleModBanSubmit
-                                    )}
-                                  >
-                                    {i18n.t('unban_from_site')}
-                                  </span>
-                                )}
-                              </li>
-                            )}
-                            {!post.banned && (
-                              <li className="list-inline-item">
-                                <span
-                                  class="pointer"
-                                  onClick={linkEvent(this, this.handleAddAdmin)}
-                                >
-                                  {this.isAdmin
-                                    ? i18n.t('remove_as_admin')
-                                    : i18n.t('appoint_as_admin')}
-                                </span>
-                              </li>
-                            )}
-                          </>
-                        )}
-                        {/* Site Creator can transfer to another admin */}
-                        {this.amSiteCreator && this.isAdmin && (
-                          <li className="list-inline-item">
-                            {!this.state.showConfirmTransferSite ? (
-                              <span
-                                class="pointer"
-                                onClick={linkEvent(
-                                  this,
-                                  this.handleShowConfirmTransferSite
-                                )}
-                              >
-                                {i18n.t('transfer_site')}
-                              </span>
-                            ) : (
-                              <>
-                                <span class="d-inline-block mr-1">
-                                  {i18n.t('are_you_sure')}
-                                </span>
-                                <span
-                                  class="pointer d-inline-block mr-1"
-                                  onClick={linkEvent(
-                                    this,
-                                    this.handleTransferSite
-                                  )}
-                                >
-                                  {i18n.t('yes')}
-                                </span>
-                                <span
-                                  class="pointer d-inline-block"
-                                  onClick={linkEvent(
-                                    this,
-                                    this.handleCancelShowConfirmTransferSite
-                                  )}
-                                >
-                                  {i18n.t('no')}
-                                </span>
-                              </>
-                            )}
-                          </li>
-                        )}
+                        >
+                          {i18n.t('no')}
+                        </span>
                       </>
                     )}
-                  </>
+                  </li>
                 )}
-              </ul>
-              {this.state.showRemoveDialog && (
-                <form
-                  class="form-inline"
-                  onSubmit={linkEvent(this, this.handleModRemoveSubmit)}
-                >
+              </>
+            )}
+          </>
+        )}
+      </ul>
+    );
+  }
+
+  removeAndBanDialogs() {
+    let post = this.props.post;
+    return (
+      <>
+        {this.state.showRemoveDialog && (
+          <form
+            class="form-inline"
+            onSubmit={linkEvent(this, this.handleModRemoveSubmit)}
+          >
+            <input
+              type="text"
+              class="form-control mr-2"
+              placeholder={i18n.t('reason')}
+              value={this.state.removeReason}
+              onInput={linkEvent(this, this.handleModRemoveReasonChange)}
+            />
+            <button type="submit" class="btn btn-secondary">
+              {i18n.t('remove_post')}
+            </button>
+          </form>
+        )}
+        {this.state.showBanDialog && (
+          <form onSubmit={linkEvent(this, this.handleModBanBothSubmit)}>
+            <div class="form-group row">
+              <label class="col-form-label" htmlFor="post-listing-reason">
+                {i18n.t('reason')}
+              </label>
+              <input
+                type="text"
+                id="post-listing-reason"
+                class="form-control mr-2"
+                placeholder={i18n.t('reason')}
+                value={this.state.banReason}
+                onInput={linkEvent(this, this.handleModBanReasonChange)}
+              />
+              <div class="form-group">
+                <div class="form-check">
                   <input
-                    type="text"
-                    class="form-control mr-2"
-                    placeholder={i18n.t('reason')}
-                    value={this.state.removeReason}
-                    onInput={linkEvent(this, this.handleModRemoveReasonChange)}
+                    class="form-check-input"
+                    id="mod-ban-remove-data"
+                    type="checkbox"
+                    checked={this.state.removeData}
+                    onChange={linkEvent(this, this.handleModRemoveDataChange)}
                   />
-                  <button type="submit" class="btn btn-secondary">
-                    {i18n.t('remove_post')}
-                  </button>
-                </form>
-              )}
-              {this.state.showBanDialog && (
-                <form onSubmit={linkEvent(this, this.handleModBanBothSubmit)}>
-                  <div class="form-group row">
-                    <label class="col-form-label" htmlFor="post-listing-reason">
-                      {i18n.t('reason')}
-                    </label>
-                    <input
-                      type="text"
-                      id="post-listing-reason"
-                      class="form-control mr-2"
-                      placeholder={i18n.t('reason')}
-                      value={this.state.banReason}
-                      onInput={linkEvent(this, this.handleModBanReasonChange)}
-                    />
-                  </div>
-                  {/* TODO hold off on expires until later */}
-                  {/* <div class="form-group row"> */}
-                  {/*   <label class="col-form-label">Expires</label> */}
-                  {/*   <input type="date" class="form-control mr-2" placeholder={i18n.t('expires')} value={this.state.banExpires} onInput={linkEvent(this, this.handleModBanExpiresChange)} /> */}
-                  {/* </div> */}
-                  <div class="form-group row">
-                    <button type="submit" class="btn btn-secondary">
-                      {i18n.t('ban')} {post.creator_name}
-                    </button>
-                  </div>
-                </form>
-              )}
+                  <label class="form-check-label" htmlFor="mod-ban-remove-data">
+                    {i18n.t('remove_posts_comments')}
+                  </label>
+                </div>
+              </div>
             </div>
-          </div>
+            {/* TODO hold off on expires until later */}
+            {/* <div class="form-group row"> */}
+            {/*   <label class="col-form-label">Expires</label> */}
+            {/*   <input type="date" class="form-control mr-2" placeholder={i18n.t('expires')} value={this.state.banExpires} onInput={linkEvent(this, this.handleModBanExpiresChange)} /> */}
+            {/* </div> */}
+            <div class="form-group row">
+              <button type="submit" class="btn btn-secondary">
+                {i18n.t('ban')} {post.creator_name}
+              </button>
+            </div>
+          </form>
+        )}
+      </>
+    );
+  }
+
+  mobileThumbnail() {
+    return this.props.post.thumbnail_url || isImage(this.props.post.url) ? (
+      <div class="row">
+        <div className={`${this.state.imageExpanded ? 'col-12' : 'col-8'}`}>
+          {this.postTitleLine()}
+        </div>
+        <div class="col-4">
+          {/* Post body prev or thumbnail */}
+          {!this.state.imageExpanded && this.thumbnail()}
         </div>
       </div>
+    ) : (
+      this.postTitleLine()
+    );
+  }
+
+  showMobilePreview() {
+    return (
+      this.props.post.body &&
+      !this.props.showBody && (
+        <div
+          className="md-div mb-1"
+          dangerouslySetInnerHTML={{
+            __html: md.render(previewLines(this.props.post.body)),
+          }}
+        />
+      )
+    );
+  }
+
+  listing() {
+    return (
+      <>
+        {/* The mobile view*/}
+        <div class="d-block d-sm-none">
+          <div class="row">
+            <div class="col-12">
+              {this.createdLine()}
+
+              {/* If it has a thumbnail, do a right aligned thumbnail */}
+              {this.mobileThumbnail()}
+
+              {/* Show a preview of the post body */}
+              {this.showMobilePreview()}
+
+              {this.commentsLine(true)}
+              {this.duplicatesLine()}
+              {this.postActions()}
+              {this.removeAndBanDialogs()}
+            </div>
+          </div>
+        </div>
+
+        {/* The larger view*/}
+        <div class="d-none d-sm-block">
+          <div class="row">
+            {this.voteBar()}
+            {!this.state.imageExpanded && (
+              <div class="col-sm-2 pr-0">
+                <div class="">{this.thumbnail()}</div>
+              </div>
+            )}
+            <div
+              class={`${
+                this.state.imageExpanded ? 'col-12' : 'col-12 col-sm-9'
+              }`}
+            >
+              <div class="row">
+                <div className="col-12">
+                  {this.postTitleLine()}
+                  {this.createdLine()}
+                  {this.commentsLine()}
+                  {this.duplicatesLine()}
+                  {this.postActions()}
+                  {this.removeAndBanDialogs()}
+                </div>
+              </div>
+            </div>
+          </div>
+        </div>
+      </>
     );
   }
 
@@ -1111,18 +1216,12 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
   }
 
   handleDeleteClick(i: PostListing) {
-    let deleteForm: PostFormI = {
-      body: i.props.post.body,
-      community_id: i.props.post.community_id,
-      name: i.props.post.name,
-      url: i.props.post.url,
+    let deleteForm: DeletePostForm = {
       edit_id: i.props.post.id,
-      creator_id: i.props.post.creator_id,
       deleted: !i.props.post.deleted,
-      nsfw: i.props.post.nsfw,
       auth: null,
     };
-    WebSocketService.Instance.editPost(deleteForm);
+    WebSocketService.Instance.deletePost(deleteForm);
   }
 
   handleSavePostClick(i: PostListing) {
@@ -1140,7 +1239,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
     let post = this.props.post;
 
     if (post.url) {
-      params += `&url=${post.url}`;
+      params += `&url=${encodeURIComponent(post.url)}`;
     }
     if (this.props.post.body) {
       params += `&body=${this.props.post.body}`;
@@ -1158,48 +1257,41 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
     i.setState(i.state);
   }
 
+  handleModRemoveDataChange(i: PostListing, event: any) {
+    i.state.removeData = event.target.checked;
+    i.setState(i.state);
+  }
+
   handleModRemoveSubmit(i: PostListing) {
     event.preventDefault();
-    let form: PostFormI = {
-      name: i.props.post.name,
-      community_id: i.props.post.community_id,
+    let form: RemovePostForm = {
       edit_id: i.props.post.id,
-      creator_id: i.props.post.creator_id,
       removed: !i.props.post.removed,
       reason: i.state.removeReason,
-      nsfw: i.props.post.nsfw,
       auth: null,
     };
-    WebSocketService.Instance.editPost(form);
+    WebSocketService.Instance.removePost(form);
 
     i.state.showRemoveDialog = false;
     i.setState(i.state);
   }
 
   handleModLock(i: PostListing) {
-    let form: PostFormI = {
-      name: i.props.post.name,
-      community_id: i.props.post.community_id,
+    let form: LockPostForm = {
       edit_id: i.props.post.id,
-      creator_id: i.props.post.creator_id,
-      nsfw: i.props.post.nsfw,
       locked: !i.props.post.locked,
       auth: null,
     };
-    WebSocketService.Instance.editPost(form);
+    WebSocketService.Instance.lockPost(form);
   }
 
   handleModSticky(i: PostListing) {
-    let form: PostFormI = {
-      name: i.props.post.name,
-      community_id: i.props.post.community_id,
+    let form: StickyPostForm = {
       edit_id: i.props.post.id,
-      creator_id: i.props.post.creator_id,
-      nsfw: i.props.post.nsfw,
       stickied: !i.props.post.stickied,
       auth: null,
     };
-    WebSocketService.Instance.editPost(form);
+    WebSocketService.Instance.stickyPost(form);
   }
 
   handleModBanFromCommunityShow(i: PostListing) {
@@ -1240,18 +1332,30 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
     event.preventDefault();
 
     if (i.state.banType == BanType.Community) {
+      // If its an unban, restore all their data
+      let ban = !i.props.post.banned_from_community;
+      if (ban == false) {
+        i.state.removeData = false;
+      }
       let form: BanFromCommunityForm = {
         user_id: i.props.post.creator_id,
         community_id: i.props.post.community_id,
-        ban: !i.props.post.banned_from_community,
+        ban,
+        remove_data: i.state.removeData,
         reason: i.state.banReason,
         expires: getUnixTime(i.state.banExpires),
       };
       WebSocketService.Instance.banFromCommunity(form);
     } else {
+      // If its an unban, restore all their data
+      let ban = !i.props.post.banned;
+      if (ban == false) {
+        i.state.removeData = false;
+      }
       let form: BanUserForm = {
         user_id: i.props.post.creator_id,
-        ban: !i.props.post.banned,
+        ban,
+        remove_data: i.state.removeData,
         reason: i.state.banReason,
         expires: getUnixTime(i.state.banExpires),
       };
index 80c9b1daebcd34f08cc5f15fc46afb9882e0b4d4..2c9b4a882663e4e154c6e406fbf11fd807c775aa 100644 (file)
@@ -1,6 +1,6 @@
 import { Component } from 'inferno';
 import { Link } from 'inferno-router';
-import { Post, SortType } from '../interfaces';
+import { Post, SortType } from 'lemmy-js-client';
 import { postSort } from '../utils';
 import { PostListing } from './post-listing';
 import { i18n } from '../i18next';
@@ -32,7 +32,7 @@ export class PostListings extends Component<PostListingsProps, any> {
                 enableDownvotes={this.props.enableDownvotes}
                 enableNsfw={this.props.enableNsfw}
               />
-              <hr class="my-2" />
+              <hr class="my-3" />
             </>
           ))
         ) : (
index 9eef286c9744cb5fea34aa4c91e1e03cb8da447d..e9427a5eb32f17243ebc05743db7866c616ebd1a 100644 (file)
@@ -1,4 +1,5 @@
 import { Component, linkEvent } from 'inferno';
+import { Helmet } from 'inferno-helmet';
 import { Subscription } from 'rxjs';
 import { retryWhen, delay, take } from 'rxjs/operators';
 import {
@@ -8,10 +9,8 @@ import {
   GetPostResponse,
   PostResponse,
   Comment,
-  CommentForm as CommentFormI,
+  MarkCommentAsReadForm,
   CommentResponse,
-  CommentSortType,
-  CommentViewType,
   CommunityUser,
   CommunityResponse,
   CommentNode as CommentNodeI,
@@ -27,7 +26,8 @@ import {
   GetSiteResponse,
   GetCommunityResponse,
   WebSocketJsonResponse,
-} from '../interfaces';
+} from 'lemmy-js-client';
+import { CommentSortType, CommentViewType } from '../interfaces';
 import { WebSocketService, UserService } from '../services';
 import {
   wsJsonToRes,
@@ -38,6 +38,7 @@ import {
   createPostLikeRes,
   commentsToFlatNodes,
   setupTippy,
+  favIconUrl,
 } from '../utils';
 import { PostListing } from './post-listing';
 import { Sidebar } from './sidebar';
@@ -90,8 +91,12 @@ export class Post extends Component<any, PostState> {
         enable_downvotes: undefined,
         open_registration: undefined,
         enable_nsfw: undefined,
+        icon: undefined,
+        banner: undefined,
       },
       online: null,
+      version: null,
+      federated_instances: undefined,
     },
   };
 
@@ -167,26 +172,43 @@ export class Post extends Component<any, PostState> {
       UserService.Instance.user &&
       UserService.Instance.user.id == parent_user_id
     ) {
-      let form: CommentFormI = {
-        content: found.content,
+      let form: MarkCommentAsReadForm = {
         edit_id: found.id,
-        creator_id: found.creator_id,
-        post_id: found.post_id,
-        parent_id: found.parent_id,
         read: true,
         auth: null,
       };
-      WebSocketService.Instance.editComment(form);
-      UserService.Instance.user.unreadCount--;
-      UserService.Instance.sub.next({
-        user: UserService.Instance.user,
-      });
+      WebSocketService.Instance.markCommentAsRead(form);
+      UserService.Instance.unreadCountSub.next(
+        UserService.Instance.unreadCountSub.value - 1
+      );
+    }
+  }
+
+  get documentTitle(): string {
+    if (this.state.post) {
+      return `${this.state.post.name} - ${this.state.siteRes.site.name}`;
+    } else {
+      return 'Lemmy';
     }
   }
 
+  get favIcon(): string {
+    return this.state.siteRes.site.icon
+      ? this.state.siteRes.site.icon
+      : favIconUrl;
+  }
+
   render() {
     return (
       <div class="container">
+        <Helmet title={this.documentTitle}>
+          <link
+            id="favicon"
+            rel="icon"
+            type="image/x-icon"
+            href={this.favIcon}
+          />
+        </Helmet>
         {this.state.loading ? (
           <h5>
             <svg class="icon icon-spinner spin">
@@ -226,9 +248,9 @@ export class Post extends Component<any, PostState> {
   sortRadios() {
     return (
       <>
-        <div class="btn-group btn-group-toggle mr-3 mb-2">
+        <div class="btn-group btn-group-toggle flex-wrap mr-3 mb-2">
           <label
-            className={`btn btn-sm btn-secondary pointer ${
+            className={`btn btn-outline-secondary pointer ${
               this.state.commentSort === CommentSortType.Hot && 'active'
             }`}
           >
@@ -241,7 +263,7 @@ export class Post extends Component<any, PostState> {
             />
           </label>
           <label
-            className={`btn btn-sm btn-secondary pointer ${
+            className={`btn btn-outline-secondary pointer ${
               this.state.commentSort === CommentSortType.Top && 'active'
             }`}
           >
@@ -254,7 +276,7 @@ export class Post extends Component<any, PostState> {
             />
           </label>
           <label
-            className={`btn btn-sm btn-secondary pointer ${
+            className={`btn btn-outline-secondary pointer ${
               this.state.commentSort === CommentSortType.New && 'active'
             }`}
           >
@@ -267,7 +289,7 @@ export class Post extends Component<any, PostState> {
             />
           </label>
           <label
-            className={`btn btn-sm btn-secondary pointer ${
+            className={`btn btn-outline-secondary pointer ${
               this.state.commentSort === CommentSortType.Old && 'active'
             }`}
           >
@@ -280,9 +302,9 @@ export class Post extends Component<any, PostState> {
             />
           </label>
         </div>
-        <div class="btn-group btn-group-toggle mb-2">
+        <div class="btn-group btn-group-toggle flex-wrap mb-2">
           <label
-            className={`btn btn-sm btn-secondary pointer ${
+            className={`btn btn-outline-secondary pointer ${
               this.state.commentViewType === CommentViewType.Chat && 'active'
             }`}
           >
@@ -326,6 +348,7 @@ export class Post extends Component<any, PostState> {
           admins={this.state.siteRes.admins}
           online={this.state.online}
           enableNsfw={this.state.siteRes.site.enable_nsfw}
+          showIcon
         />
       </div>
     );
@@ -408,17 +431,15 @@ export class Post extends Component<any, PostState> {
       this.state.comments = data.comments;
       this.state.community = data.community;
       this.state.moderators = data.moderators;
-      this.state.siteRes.admins = data.admins;
       this.state.online = data.online;
       this.state.loading = false;
-      document.title = `${this.state.post.name} - ${this.state.siteRes.site.name}`;
 
       // Get cross-posts
       if (this.state.post.url) {
         let form: SearchForm = {
           q: this.state.post.url,
-          type_: SearchType[SearchType.Url],
-          sort: SortType[SortType.TopAll],
+          type_: SearchType.Url,
+          sort: SortType.TopAll,
           page: 1,
           limit: 6,
         };
@@ -435,7 +456,11 @@ export class Post extends Component<any, PostState> {
         this.state.comments.unshift(data.comment);
         this.setState(this.state);
       }
-    } else if (res.op == UserOperation.EditComment) {
+    } else if (
+      res.op == UserOperation.EditComment ||
+      res.op == UserOperation.DeleteComment ||
+      res.op == UserOperation.RemoveComment
+    ) {
       let data = res.data as CommentResponse;
       editCommentRes(data, this.state.comments);
       this.setState(this.state);
@@ -452,7 +477,13 @@ export class Post extends Component<any, PostState> {
       let data = res.data as PostResponse;
       createPostLikeRes(data, this.state.post);
       this.setState(this.state);
-    } else if (res.op == UserOperation.EditPost) {
+    } else if (
+      res.op == UserOperation.EditPost ||
+      res.op == UserOperation.DeletePost ||
+      res.op == UserOperation.RemovePost ||
+      res.op == UserOperation.LockPost ||
+      res.op == UserOperation.StickyPost
+    ) {
       let data = res.data as PostResponse;
       this.state.post = data.post;
       this.setState(this.state);
@@ -462,7 +493,11 @@ export class Post extends Component<any, PostState> {
       this.state.post = data.post;
       this.setState(this.state);
       setupTippy();
-    } else if (res.op == UserOperation.EditCommunity) {
+    } else if (
+      res.op == UserOperation.EditCommunity ||
+      res.op == UserOperation.DeleteCommunity ||
+      res.op == UserOperation.RemoveCommunity
+    ) {
       let data = res.data as CommunityResponse;
       this.state.community = data.community;
       this.state.post.community_id = data.community.id;
@@ -520,7 +555,6 @@ export class Post extends Component<any, PostState> {
       let data = res.data as GetCommunityResponse;
       this.state.community = data.community;
       this.state.moderators = data.moderators;
-      this.state.siteRes.admins = data.admins;
       this.setState(this.state);
     }
   }
index 6ae7efe71d003836e70ce58d2689a46459ab7609..6d7825cd0c08c6455c9edbbd56d4e280222edb40 100644 (file)
@@ -14,21 +14,16 @@ import {
   GetUserDetailsForm,
   SortType,
   WebSocketJsonResponse,
-} from '../interfaces';
+} from 'lemmy-js-client';
 import { WebSocketService } from '../services';
 import {
   capitalizeFirstLetter,
-  markdownHelpUrl,
-  mdToHtml,
   wsJsonToRes,
   toast,
-  randomStr,
-  setupTribute,
   setupTippy,
 } from '../utils';
 import { UserListing } from './user-listing';
-import Tribute from 'tributejs/src/Tribute.js';
-import autosize from 'autosize';
+import { MarkdownTextArea } from './markdown-textarea';
 import { i18n } from '../i18next';
 import { T } from 'inferno-i18next';
 
@@ -52,8 +47,6 @@ export class PrivateMessageForm extends Component<
   PrivateMessageFormProps,
   PrivateMessageFormState
 > {
-  private id = `message-form-${randomStr()}`;
-  private tribute: Tribute;
   private subscription: Subscription;
   private emptyState: PrivateMessageFormState = {
     privateMessageForm: {
@@ -69,9 +62,10 @@ export class PrivateMessageForm extends Component<
   constructor(props: any, context: any) {
     super(props, context);
 
-    this.tribute = setupTribute();
     this.state = this.emptyState;
 
+    this.handleContentChange = this.handleContentChange.bind(this);
+
     if (this.props.privateMessage) {
       this.state.privateMessageForm = {
         content: this.props.privateMessage.content,
@@ -83,7 +77,7 @@ export class PrivateMessageForm extends Component<
       this.state.privateMessageForm.recipient_id = this.props.params.recipient_id;
       let form: GetUserDetailsForm = {
         user_id: this.state.privateMessageForm.recipient_id,
-        sort: SortType[SortType.New],
+        sort: SortType.New,
         saved_only: false,
       };
       WebSocketService.Instance.getUserDetails(form);
@@ -99,14 +93,6 @@ export class PrivateMessageForm extends Component<
   }
 
   componentDidMount() {
-    var textarea: any = document.getElementById(this.id);
-    autosize(textarea);
-    this.tribute.attach(textarea);
-    textarea.addEventListener('tribute-replaced', () => {
-      this.state.privateMessageForm.content = textarea.value;
-      this.setState(this.state);
-      autosize.update(textarea);
-    });
     setupTippy();
   }
 
@@ -142,6 +128,8 @@ export class PrivateMessageForm extends Component<
                   <UserListing
                     user={{
                       name: this.state.recipient.name,
+                      preferred_username: this.state.recipient
+                        .preferred_username,
                       avatar: this.state.recipient.avatar,
                       id: this.state.recipient.id,
                       local: this.state.recipient.local,
@@ -153,24 +141,23 @@ export class PrivateMessageForm extends Component<
             </div>
           )}
           <div class="form-group row">
-            <label class="col-sm-2 col-form-label">{i18n.t('message')}</label>
+            <label class="col-sm-2 col-form-label">
+              {i18n.t('message')}
+              <span
+                onClick={linkEvent(this, this.handleShowDisclaimer)}
+                class="ml-2 pointer text-danger"
+                data-tippy-content={i18n.t('disclaimer')}
+              >
+                <svg class={`icon icon-inline`}>
+                  <use xlinkHref="#icon-alert-triangle"></use>
+                </svg>
+              </span>
+            </label>
             <div class="col-sm-10">
-              <textarea
-                id={this.id}
-                value={this.state.privateMessageForm.content}
-                onInput={linkEvent(this, this.handleContentChange)}
-                className={`form-control ${this.state.previewMode && 'd-none'}`}
-                rows={4}
-                maxLength={10000}
+              <MarkdownTextArea
+                initialContent={this.state.privateMessageForm.content}
+                onContentChange={this.handleContentChange}
               />
-              {this.state.previewMode && (
-                <div
-                  className="card card-body md-div"
-                  dangerouslySetInnerHTML={mdToHtml(
-                    this.state.privateMessageForm.content
-                  )}
-                />
-              )}
             </div>
           </div>
 
@@ -184,7 +171,7 @@ export class PrivateMessageForm extends Component<
                       class="alert-link"
                       target="_blank"
                       rel="noopener"
-                      href="https://about.riot.im/"
+                      href="https://element.io/get-started"
                     >
                       #
                     </a>
@@ -210,16 +197,6 @@ export class PrivateMessageForm extends Component<
                   capitalizeFirstLetter(i18n.t('send_message'))
                 )}
               </button>
-              {this.state.privateMessageForm.content && (
-                <button
-                  className={`btn btn-secondary mr-2 ${
-                    this.state.previewMode && 'active'
-                  }`}
-                  onClick={linkEvent(this, this.handlePreviewToggle)}
-                >
-                  {i18n.t('preview')}
-                </button>
-              )}
               {this.props.privateMessage && (
                 <button
                   type="button"
@@ -230,30 +207,7 @@ export class PrivateMessageForm extends Component<
                 </button>
               )}
               <ul class="d-inline-block float-right list-inline mb-1 text-muted font-weight-bold">
-                <li class="list-inline-item">
-                  <span
-                    onClick={linkEvent(this, this.handleShowDisclaimer)}
-                    class="pointer"
-                    data-tippy-content={i18n.t('disclaimer')}
-                  >
-                    <svg class={`icon icon-inline`}>
-                      <use xlinkHref="#icon-alert-triangle"></use>
-                    </svg>
-                  </span>
-                </li>
-                <li class="list-inline-item">
-                  <a
-                    href={markdownHelpUrl}
-                    target="_blank"
-                    rel="noopener"
-                    class="text-muted"
-                    title={i18n.t('formatting_help')}
-                  >
-                    <svg class="icon icon-inline">
-                      <use xlinkHref="#icon-help-circle"></use>
-                    </svg>
-                  </a>
-                </li>
+                <li class="list-inline-item"></li>
               </ul>
             </div>
           </div>
@@ -284,9 +238,9 @@ export class PrivateMessageForm extends Component<
     i.setState(i.state);
   }
 
-  handleContentChange(i: PrivateMessageForm, event: any) {
-    i.state.privateMessageForm.content = event.target.value;
-    i.setState(i.state);
+  handleContentChange(val: string) {
+    this.state.privateMessageForm.content = val;
+    this.setState(this.state);
   }
 
   handleCancel(i: PrivateMessageForm) {
@@ -311,7 +265,11 @@ export class PrivateMessageForm extends Component<
       this.state.loading = false;
       this.setState(this.state);
       return;
-    } else if (res.op == UserOperation.EditPrivateMessage) {
+    } else if (
+      res.op == UserOperation.EditPrivateMessage ||
+      res.op == UserOperation.DeletePrivateMessage ||
+      res.op == UserOperation.MarkPrivateMessageAsRead
+    ) {
       let data = res.data as PrivateMessageResponse;
       this.state.loading = false;
       this.props.onEdit(data.message);
index 71924f0cbabb882c84a24b2718fe97afa62aec02..243d12e579eac09c1ff2691010af7aad17ee2fa7 100644 (file)
@@ -2,12 +2,14 @@ import { Component, linkEvent } from 'inferno';
 import { Link } from 'inferno-router';
 import {
   PrivateMessage as PrivateMessageI,
-  EditPrivateMessageForm,
-} from '../interfaces';
+  DeletePrivateMessageForm,
+  MarkPrivateMessageAsReadForm,
+} from 'lemmy-js-client';
 import { WebSocketService, UserService } from '../services';
 import { mdToHtml, pictrsAvatarThumbnail, showAvatars, toast } from '../utils';
 import { MomentTime } from './moment-time';
 import { PrivateMessageForm } from './private-message-form';
+import { UserListing, UserOther } from './user-listing';
 import { i18n } from '../i18next';
 
 interface PrivateMessageState {
@@ -44,11 +46,34 @@ export class PrivateMessage extends Component<
   }
 
   get mine(): boolean {
-    return UserService.Instance.user.id == this.props.privateMessage.creator_id;
+    return (
+      UserService.Instance.user &&
+      UserService.Instance.user.id == this.props.privateMessage.creator_id
+    );
   }
 
   render() {
     let message = this.props.privateMessage;
+    let userOther: UserOther = this.mine
+      ? {
+          name: message.recipient_name,
+          preferred_username: message.recipient_preferred_username,
+          id: message.id,
+          avatar: message.recipient_avatar,
+          local: message.recipient_local,
+          actor_id: message.recipient_actor_id,
+          published: message.published,
+        }
+      : {
+          name: message.creator_name,
+          preferred_username: message.creator_preferred_username,
+          id: message.id,
+          avatar: message.creator_avatar,
+          local: message.creator_local,
+          actor_id: message.creator_actor_id,
+          published: message.published,
+        };
+
     return (
       <div class="border-top border-light">
         <div>
@@ -58,33 +83,7 @@ export class PrivateMessage extends Component<
               {this.mine ? i18n.t('to') : i18n.t('from')}
             </li>
             <li className="list-inline-item">
-              <Link
-                className="text-body font-weight-bold"
-                to={
-                  this.mine
-                    ? `/u/${message.recipient_name}`
-                    : `/u/${message.creator_name}`
-                }
-              >
-                {(this.mine
-                  ? message.recipient_avatar
-                  : message.creator_avatar) &&
-                  showAvatars() && (
-                    <img
-                      height="32"
-                      width="32"
-                      src={pictrsAvatarThumbnail(
-                        this.mine
-                          ? message.recipient_avatar
-                          : message.creator_avatar
-                      )}
-                      class="rounded-circle mr-1"
-                    />
-                  )}
-                <span>
-                  {this.mine ? message.recipient_name : message.creator_name}
-                </span>
-              </Link>
+              <UserListing user={userOther} />
             </li>
             <li className="list-inline-item">
               <span>
@@ -112,6 +111,7 @@ export class PrivateMessage extends Component<
             <PrivateMessageForm
               privateMessage={message}
               onEdit={this.handlePrivateMessageEdit}
+              onCreate={this.handlePrivateMessageCreate}
               onCancel={this.handleReplyCancel}
             />
           )}
@@ -130,7 +130,7 @@ export class PrivateMessage extends Component<
                   <>
                     <li className="list-inline-item">
                       <button
-                        class="btn btn-link btn-sm btn-animate text-muted"
+                        class="btn btn-link btn-animate text-muted"
                         onClick={linkEvent(this, this.handleMarkRead)}
                         data-tippy-content={
                           message.read
@@ -149,7 +149,7 @@ export class PrivateMessage extends Component<
                     </li>
                     <li className="list-inline-item">
                       <button
-                        class="btn btn-link btn-sm btn-animate text-muted"
+                        class="btn btn-link btn-animate text-muted"
                         onClick={linkEvent(this, this.handleReplyClick)}
                         data-tippy-content={i18n.t('reply')}
                       >
@@ -164,7 +164,7 @@ export class PrivateMessage extends Component<
                   <>
                     <li className="list-inline-item">
                       <button
-                        class="btn btn-link btn-sm btn-animate text-muted"
+                        class="btn btn-link btn-animate text-muted"
                         onClick={linkEvent(this, this.handleEditClick)}
                         data-tippy-content={i18n.t('edit')}
                       >
@@ -175,7 +175,7 @@ export class PrivateMessage extends Component<
                     </li>
                     <li className="list-inline-item">
                       <button
-                        class="btn btn-link btn-sm btn-animate text-muted"
+                        class="btn btn-link btn-animate text-muted"
                         onClick={linkEvent(this, this.handleDeleteClick)}
                         data-tippy-content={
                           !message.deleted
@@ -196,7 +196,7 @@ export class PrivateMessage extends Component<
                 )}
                 <li className="list-inline-item">
                   <button
-                    class="btn btn-link btn-sm btn-animate text-muted"
+                    class="btn btn-link btn-animate text-muted"
                     onClick={linkEvent(this, this.handleViewSource)}
                     data-tippy-content={i18n.t('view_source')}
                   >
@@ -243,11 +243,11 @@ export class PrivateMessage extends Component<
   }
 
   handleDeleteClick(i: PrivateMessage) {
-    let form: EditPrivateMessageForm = {
+    let form: DeletePrivateMessageForm = {
       edit_id: i.props.privateMessage.id,
       deleted: !i.props.privateMessage.deleted,
     };
-    WebSocketService.Instance.editPrivateMessage(form);
+    WebSocketService.Instance.deletePrivateMessage(form);
   }
 
   handleReplyCancel() {
@@ -257,11 +257,11 @@ export class PrivateMessage extends Component<
   }
 
   handleMarkRead(i: PrivateMessage) {
-    let form: EditPrivateMessageForm = {
+    let form: MarkPrivateMessageAsReadForm = {
       edit_id: i.props.privateMessage.id,
       read: !i.props.privateMessage.read,
     };
-    WebSocketService.Instance.editPrivateMessage(form);
+    WebSocketService.Instance.markPrivateMessageAsRead(form);
   }
 
   handleMessageCollapse(i: PrivateMessage) {
@@ -279,9 +279,14 @@ export class PrivateMessage extends Component<
     this.setState(this.state);
   }
 
-  handlePrivateMessageCreate() {
-    this.state.showReply = false;
-    this.setState(this.state);
-    toast(i18n.t('message_sent'));
+  handlePrivateMessageCreate(message: PrivateMessageI) {
+    if (
+      UserService.Instance.user &&
+      message.creator_id == UserService.Instance.user.id
+    ) {
+      this.state.showReply = false;
+      this.setState(this.state);
+      toast(i18n.t('message_sent'));
+    }
   }
 }
index d1d99cee86e106ae47f6f08177065cdb66a0e75b..a18cc2d8e757ed219c8cd690bcea5e0c0603ff84 100644 (file)
@@ -1,5 +1,5 @@
 import { Component, linkEvent } from 'inferno';
-import { Link } from 'inferno-router';
+import { Helmet } from 'inferno-helmet';
 import { Subscription } from 'rxjs';
 import { retryWhen, delay, take } from 'rxjs/operators';
 import {
@@ -17,7 +17,7 @@ import {
   WebSocketJsonResponse,
   GetSiteResponse,
   Site,
-} from '../interfaces';
+} from 'lemmy-js-client';
 import { WebSocketService } from '../services';
 import {
   wsJsonToRes,
@@ -57,8 +57,8 @@ interface SearchProps {
 
 interface UrlParams {
   q?: string;
-  type_?: string;
-  sort?: string;
+  type_?: SearchType;
+  sort?: SortType;
   page?: number;
 }
 
@@ -156,9 +156,24 @@ export class Search extends Component<any, SearchState> {
     }
   }
 
+  get documentTitle(): string {
+    if (this.state.site.name) {
+      if (this.state.q) {
+        return `${i18n.t('search')} - ${this.state.q} - ${
+          this.state.site.name
+        }`;
+      } else {
+        return `${i18n.t('search')} - ${this.state.site.name}`;
+      }
+    } else {
+      return 'Lemmy';
+    }
+  }
+
   render() {
     return (
       <div class="container">
+        <Helmet title={this.documentTitle} />
         <h5>{i18n.t('search')}</h5>
         {this.selects()}
         {this.searchForm()}
@@ -181,14 +196,14 @@ export class Search extends Component<any, SearchState> {
       >
         <input
           type="text"
-          class="form-control mr-2"
+          class="form-control mr-2 mb-2"
           value={this.state.searchText}
           placeholder={`${i18n.t('search')}...`}
           onInput={linkEvent(this, this.handleQChange)}
           required
           minLength={3}
         />
-        <button type="submit" class="btn btn-secondary mr-2">
+        <button type="submit" class="btn btn-secondary mr-2 mb-2">
           {this.state.loading ? (
             <svg class="icon icon-spinner spin">
               <use xlinkHref="#icon-spinner"></use>
@@ -207,7 +222,7 @@ export class Search extends Component<any, SearchState> {
         <select
           value={this.state.type_}
           onChange={linkEvent(this, this.handleTypeChange)}
-          class="custom-select custom-select-sm w-auto"
+          class="custom-select w-auto mb-2"
         >
           <option disabled>{i18n.t('type')}</option>
           <option value={SearchType.All}>{i18n.t('all')}</option>
@@ -274,6 +289,7 @@ export class Search extends Component<any, SearchState> {
             <div class="col-12">
               {i.type_ == 'posts' && (
                 <PostListing
+                  key={(i.data as Post).id}
                   post={i.data as Post}
                   showCommunity
                   enableDownvotes={this.state.site.enable_downvotes}
@@ -282,6 +298,7 @@ export class Search extends Component<any, SearchState> {
               )}
               {i.type_ == 'comments' && (
                 <CommentNodes
+                  key={(i.data as Comment).id}
                   nodes={[{ comment: i.data as Comment }]}
                   locked
                   noIndent
@@ -298,6 +315,8 @@ export class Search extends Component<any, SearchState> {
                     <UserListing
                       user={{
                         name: (i.data as UserView).name,
+                        preferred_username: (i.data as UserView)
+                          .preferred_username,
                         avatar: (i.data as UserView).avatar,
                       }}
                     />
@@ -402,7 +421,7 @@ export class Search extends Component<any, SearchState> {
       <div class="mt-2">
         {this.state.page > 1 && (
           <button
-            class="btn btn-sm btn-secondary mr-1"
+            class="btn btn-secondary mr-1"
             onClick={linkEvent(this, this.prevPage)}
           >
             {i18n.t('prev')}
@@ -411,7 +430,7 @@ export class Search extends Component<any, SearchState> {
 
         {this.resultsCount() > 0 && (
           <button
-            class="btn btn-sm btn-secondary"
+            class="btn btn-secondary"
             onClick={linkEvent(this, this.nextPage)}
           >
             {i18n.t('next')}
@@ -442,8 +461,8 @@ export class Search extends Component<any, SearchState> {
   search() {
     let form: SearchForm = {
       q: this.state.q,
-      type_: SearchType[this.state.type_],
-      sort: SortType[this.state.sort],
+      type_: this.state.type_,
+      sort: this.state.sort,
       page: this.state.page,
       limit: fetchLimit,
     };
@@ -454,12 +473,12 @@ export class Search extends Component<any, SearchState> {
   }
 
   handleSortChange(val: SortType) {
-    this.updateUrl({ sort: SortType[val].toLowerCase(), page: 1 });
+    this.updateUrl({ sort: val, page: 1 });
   }
 
   handleTypeChange(i: Search, event: any) {
     i.updateUrl({
-      type_: SearchType[Number(event.target.value)].toLowerCase(),
+      type_: SearchType[event.target.value],
       page: 1,
     });
   }
@@ -468,8 +487,8 @@ export class Search extends Component<any, SearchState> {
     event.preventDefault();
     i.updateUrl({
       q: i.state.searchText,
-      type_: SearchType[i.state.type_].toLowerCase(),
-      sort: SortType[i.state.sort].toLowerCase(),
+      type_: i.state.type_,
+      sort: i.state.sort,
       page: i.state.page,
     });
   }
@@ -480,10 +499,8 @@ export class Search extends Component<any, SearchState> {
 
   updateUrl(paramUpdates: UrlParams) {
     const qStr = paramUpdates.q || this.state.q;
-    const typeStr =
-      paramUpdates.type_ || SearchType[this.state.type_].toLowerCase();
-    const sortStr =
-      paramUpdates.sort || SortType[this.state.sort].toLowerCase();
+    const typeStr = paramUpdates.type_ || this.state.type_;
+    const sortStr = paramUpdates.sort || this.state.sort;
     const page = paramUpdates.page || this.state.page;
     this.props.history.push(
       `/search/q/${qStr}/type/${typeStr}/sort/${sortStr}/page/${page}`
@@ -500,9 +517,6 @@ export class Search extends Component<any, SearchState> {
       let data = res.data as SearchResponse;
       this.state.searchResponse = data;
       this.state.loading = false;
-      document.title = `${i18n.t('search')} - ${this.state.q} - ${
-        this.state.site.name
-      }`;
       window.scrollTo(0, 0);
       this.setState(this.state);
     } else if (res.op == UserOperation.CreateCommentLike) {
@@ -517,7 +531,6 @@ export class Search extends Component<any, SearchState> {
       let data = res.data as GetSiteResponse;
       this.state.site = data.site;
       this.setState(this.state);
-      document.title = `${i18n.t('search')} - ${data.site.name}`;
     }
   }
 }
index ed1c88cd2677cc5b8ca0d5e140e77e19ca7e0dda..6360ec5a36022e4fcb7d21aabc748c1b479dbc50 100644 (file)
@@ -1,4 +1,5 @@
 import { Component, linkEvent } from 'inferno';
+import { Helmet } from 'inferno-helmet';
 import { Subscription } from 'rxjs';
 import { retryWhen, delay, take } from 'rxjs/operators';
 import {
@@ -6,7 +7,7 @@ import {
   LoginResponse,
   UserOperation,
   WebSocketJsonResponse,
-} from '../interfaces';
+} from 'lemmy-js-client';
 import { WebSocketService, UserService } from '../services';
 import { wsJsonToRes, toast } from '../utils';
 import { SiteForm } from './site-form';
@@ -28,6 +29,9 @@ export class Setup extends Component<any, State> {
       password_verify: undefined,
       admin: true,
       show_nsfw: true,
+      // The first admin signup doesn't need a captcha
+      captcha_uuid: '',
+      captcha_answer: '',
     },
     doneRegisteringUser: false,
     userLoading: false,
@@ -51,13 +55,14 @@ export class Setup extends Component<any, State> {
     this.subscription.unsubscribe();
   }
 
-  componentDidMount() {
-    document.title = `${i18n.t('setup')} - Lemmy`;
+  get documentTitle(): string {
+    return `${i18n.t('setup')} - Lemmy`;
   }
 
   render() {
     return (
       <div class="container">
+        <Helmet title={this.documentTitle} />
         <div class="row">
           <div class="col-12 offset-lg-3 col-lg-6">
             <h3>{i18n.t('lemmy_instance_setup')}</h3>
index 42abf65aab67626b0b15d59847f7d343adc85d5f..25cbd79722b65866309624119b3225ae9ac6bee7 100644 (file)
@@ -4,14 +4,17 @@ import {
   Community,
   CommunityUser,
   FollowCommunityForm,
-  CommunityForm as CommunityFormI,
+  DeleteCommunityForm,
+  RemoveCommunityForm,
   UserView,
-} from '../interfaces';
+  AddModToCommunityForm,
+} from 'lemmy-js-client';
 import { WebSocketService, UserService } from '../services';
-import { mdToHtml, getUnixTime, hostname } from '../utils';
+import { mdToHtml, getUnixTime } from '../utils';
 import { CommunityForm } from './community-form';
 import { UserListing } from './user-listing';
 import { CommunityLink } from './community-link';
+import { BannerIconHeader } from './banner-icon-header';
 import { i18n } from '../i18next';
 
 interface SidebarProps {
@@ -20,6 +23,7 @@ interface SidebarProps {
   admins: Array<UserView>;
   online: number;
   enableNsfw: boolean;
+  showIcon?: boolean;
 }
 
 interface SidebarState {
@@ -27,6 +31,7 @@ interface SidebarState {
   showRemoveDialog: boolean;
   removeReason: string;
   removeExpires: string;
+  showConfirmLeaveModTeam: boolean;
 }
 
 export class Sidebar extends Component<SidebarProps, SidebarState> {
@@ -35,6 +40,7 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
     showRemoveDialog: false,
     removeReason: null,
     removeExpires: null,
+    showConfirmLeaveModTeam: false,
   };
 
   constructor(props: any, context: any) {
@@ -62,208 +68,297 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
   }
 
   sidebar() {
-    let community = this.props.community;
-    let name_: string, link: string;
-
-    if (community.local) {
-      name_ = community.name;
-      link = `/c/${community.name}`;
-    } else {
-      name_ = `${community.name}@${hostname(community.actor_id)}`;
-      link = community.actor_id;
-    }
     return (
       <div>
-        <div class="card border-secondary mb-3">
+        <div class="card bg-transparent border-secondary mb-3">
+          <div class="card-header bg-transparent border-secondary">
+            {this.communityTitle()}
+            {this.adminButtons()}
+          </div>
+          <div class="card-body">{this.subscribes()}</div>
+        </div>
+        <div class="card bg-transparent border-secondary mb-3">
           <div class="card-body">
-            <h5 className="mb-0">
-              <span>{community.title}</span>
-              {community.removed && (
-                <small className="ml-2 text-muted font-italic">
-                  {i18n.t('removed')}
-                </small>
-              )}
-              {community.deleted && (
-                <small className="ml-2 text-muted font-italic">
-                  {i18n.t('deleted')}
-                </small>
-              )}
-            </h5>
-            <CommunityLink community={community} realLink />
-            <ul class="list-inline mb-1 text-muted font-weight-bold">
-              {this.canMod && (
-                <>
+            {this.description()}
+            {this.badges()}
+            {this.mods()}
+          </div>
+        </div>
+      </div>
+    );
+  }
+
+  communityTitle() {
+    let community = this.props.community;
+    return (
+      <div>
+        <h5 className="mb-0">
+          {this.props.showIcon && (
+            <BannerIconHeader icon={community.icon} banner={community.banner} />
+          )}
+          <span>{community.title}</span>
+          {community.removed && (
+            <small className="ml-2 text-muted font-italic">
+              {i18n.t('removed')}
+            </small>
+          )}
+          {community.deleted && (
+            <small className="ml-2 text-muted font-italic">
+              {i18n.t('deleted')}
+            </small>
+          )}
+          {community.nsfw && (
+            <small className="ml-2 text-muted font-italic">
+              {i18n.t('nsfw')}
+            </small>
+          )}
+        </h5>
+        <CommunityLink
+          community={community}
+          realLink
+          useApubName
+          muted
+          hideAvatar
+        />
+      </div>
+    );
+  }
+
+  badges() {
+    let community = this.props.community;
+    return (
+      <ul class="my-1 list-inline">
+        <li className="list-inline-item badge badge-light">
+          {i18n.t('number_online', { count: this.props.online })}
+        </li>
+        <li className="list-inline-item badge badge-light">
+          {i18n.t('number_of_subscribers', {
+            count: community.number_of_subscribers,
+          })}
+        </li>
+        <li className="list-inline-item badge badge-light">
+          {i18n.t('number_of_posts', {
+            count: community.number_of_posts,
+          })}
+        </li>
+        <li className="list-inline-item badge badge-light">
+          {i18n.t('number_of_comments', {
+            count: community.number_of_comments,
+          })}
+        </li>
+        <li className="list-inline-item">
+          <Link className="badge badge-light" to="/communities">
+            {community.category_name}
+          </Link>
+        </li>
+        <li className="list-inline-item">
+          <Link
+            className="badge badge-light"
+            to={`/modlog/community/${this.props.community.id}`}
+          >
+            {i18n.t('modlog')}
+          </Link>
+        </li>
+        <li className="list-inline-item badge badge-light">
+          <CommunityLink community={community} realLink />
+        </li>
+      </ul>
+    );
+  }
+
+  mods() {
+    return (
+      <ul class="list-inline small">
+        <li class="list-inline-item">{i18n.t('mods')}: </li>
+        {this.props.moderators.map(mod => (
+          <li class="list-inline-item">
+            <UserListing
+              user={{
+                name: mod.user_name,
+                preferred_username: mod.user_preferred_username,
+                avatar: mod.avatar,
+                id: mod.user_id,
+                local: mod.user_local,
+                actor_id: mod.user_actor_id,
+              }}
+            />
+          </li>
+        ))}
+      </ul>
+    );
+  }
+
+  subscribes() {
+    let community = this.props.community;
+    return (
+      <div class="d-flex flex-wrap">
+        <Link
+          class={`btn btn-secondary flex-fill mr-2 mb-2 ${
+            community.deleted || community.removed ? 'no-click' : ''
+          }`}
+          to={`/create_post?community=${community.name}`}
+        >
+          {i18n.t('create_a_post')}
+        </Link>
+        {community.subscribed ? (
+          <a
+            class="btn btn-secondary flex-fill mb-2"
+            href="#"
+            onClick={linkEvent(community.id, this.handleUnsubscribe)}
+          >
+            {i18n.t('unsubscribe')}
+          </a>
+        ) : (
+          <a
+            class="btn btn-secondary flex-fill mb-2"
+            href="#"
+            onClick={linkEvent(community.id, this.handleSubscribe)}
+          >
+            {i18n.t('subscribe')}
+          </a>
+        )}
+      </div>
+    );
+  }
+
+  description() {
+    let community = this.props.community;
+    return (
+      community.description && (
+        <div
+          className="md-div"
+          dangerouslySetInnerHTML={mdToHtml(community.description)}
+        />
+      )
+    );
+  }
+
+  adminButtons() {
+    let community = this.props.community;
+    return (
+      <>
+        <ul class="list-inline mb-1 text-muted font-weight-bold">
+          {this.canMod && (
+            <>
+              <li className="list-inline-item-action">
+                <span
+                  class="pointer"
+                  onClick={linkEvent(this, this.handleEditClick)}
+                  data-tippy-content={i18n.t('edit')}
+                >
+                  <svg class="icon icon-inline">
+                    <use xlinkHref="#icon-edit"></use>
+                  </svg>
+                </span>
+              </li>
+              {!this.amCreator &&
+                (!this.state.showConfirmLeaveModTeam ? (
                   <li className="list-inline-item-action">
                     <span
                       class="pointer"
-                      onClick={linkEvent(this, this.handleEditClick)}
-                      data-tippy-content={i18n.t('edit')}
+                      onClick={linkEvent(
+                        this,
+                        this.handleShowConfirmLeaveModTeamClick
+                      )}
                     >
-                      <svg class="icon icon-inline">
-                        <use xlinkHref="#icon-edit"></use>
-                      </svg>
+                      {i18n.t('leave_mod_team')}
                     </span>
                   </li>
-                  {this.amCreator && (
+                ) : (
+                  <>
+                    <li className="list-inline-item-action">
+                      {i18n.t('are_you_sure')}
+                    </li>
                     <li className="list-inline-item-action">
                       <span
                         class="pointer"
-                        onClick={linkEvent(this, this.handleDeleteClick)}
-                        data-tippy-content={
-                          !community.deleted
-                            ? i18n.t('delete')
-                            : i18n.t('restore')
-                        }
+                        onClick={linkEvent(this, this.handleLeaveModTeamClick)}
                       >
-                        <svg
-                          class={`icon icon-inline ${
-                            community.deleted && 'text-danger'
-                          }`}
-                        >
-                          <use xlinkHref="#icon-trash"></use>
-                        </svg>
+                        {i18n.t('yes')}
                       </span>
                     </li>
-                  )}
-                </>
-              )}
-              {this.canAdmin && (
-                <li className="list-inline-item">
-                  {!this.props.community.removed ? (
-                    <span
-                      class="pointer"
-                      onClick={linkEvent(this, this.handleModRemoveShow)}
-                    >
-                      {i18n.t('remove')}
-                    </span>
-                  ) : (
-                    <span
-                      class="pointer"
-                      onClick={linkEvent(this, this.handleModRemoveSubmit)}
+                    <li className="list-inline-item-action">
+                      <span
+                        class="pointer"
+                        onClick={linkEvent(
+                          this,
+                          this.handleCancelLeaveModTeamClick
+                        )}
+                      >
+                        {i18n.t('no')}
+                      </span>
+                    </li>
+                  </>
+                ))}
+              {this.amCreator && (
+                <li className="list-inline-item-action">
+                  <span
+                    class="pointer"
+                    onClick={linkEvent(this, this.handleDeleteClick)}
+                    data-tippy-content={
+                      !community.deleted ? i18n.t('delete') : i18n.t('restore')
+                    }
+                  >
+                    <svg
+                      class={`icon icon-inline ${
+                        community.deleted && 'text-danger'
+                      }`}
                     >
-                      {i18n.t('restore')}
-                    </span>
-                  )}
+                      <use xlinkHref="#icon-trash"></use>
+                    </svg>
+                  </span>
                 </li>
               )}
-            </ul>
-            {this.state.showRemoveDialog && (
-              <form onSubmit={linkEvent(this, this.handleModRemoveSubmit)}>
-                <div class="form-group row">
-                  <label class="col-form-label" htmlFor="remove-reason">
-                    {i18n.t('reason')}
-                  </label>
-                  <input
-                    type="text"
-                    id="remove-reason"
-                    class="form-control mr-2"
-                    placeholder={i18n.t('optional')}
-                    value={this.state.removeReason}
-                    onInput={linkEvent(this, this.handleModRemoveReasonChange)}
-                  />
-                </div>
-                {/* TODO hold off on expires for now */}
-                {/* <div class="form-group row"> */}
-                {/*   <label class="col-form-label">Expires</label> */}
-                {/*   <input type="date" class="form-control mr-2" placeholder={i18n.t('expires')} value={this.state.removeExpires} onInput={linkEvent(this, this.handleModRemoveExpiresChange)} /> */}
-                {/* </div> */}
-                <div class="form-group row">
-                  <button type="submit" class="btn btn-secondary">
-                    {i18n.t('remove_community')}
-                  </button>
-                </div>
-              </form>
-            )}
-            <ul class="my-1 list-inline">
-              {/*
-              <li className="list-inline-item badge badge-secondary">
-                {i18n.t('number_online', { count: this.props.online })}
-              </li>
-              */}
-              <li className="list-inline-item badge badge-secondary">
-                {i18n.t('number_of_subscribers', {
-                  count: community.number_of_subscribers,
-                })}
-              </li>
-              <li className="list-inline-item badge badge-secondary">
-                {i18n.t('number_of_posts', {
-                  count: community.number_of_posts,
-                })}
-              </li>
-              <li className="list-inline-item badge badge-secondary">
-                {i18n.t('number_of_comments', {
-                  count: community.number_of_comments,
-                })}
-              </li>
-              <li className="list-inline-item">
-                <Link className="badge badge-secondary" to="/communities">
-                  {community.category_name}
-                </Link>
-              </li>
-              <li className="list-inline-item">
-                <Link
-                  className="badge badge-secondary"
-                  to={`/modlog/community/${this.props.community.id}`}
+            </>
+          )}
+          {this.canAdmin && (
+            <li className="list-inline-item">
+              {!this.props.community.removed ? (
+                <span
+                  class="pointer"
+                  onClick={linkEvent(this, this.handleModRemoveShow)}
                 >
-                  {i18n.t('modlog')}
-                </Link>
-              </li>
-            </ul>
-            <ul class="list-inline small">
-              <li class="list-inline-item">{i18n.t('mods')}: </li>
-              {this.props.moderators.map(mod => (
-                <li class="list-inline-item">
-                  <UserListing
-                    user={{
-                      name: mod.user_name,
-                      avatar: mod.avatar,
-                      id: mod.user_id,
-                      local: mod.user_local,
-                      actor_id: mod.user_actor_id,
-                    }}
-                  />
-                </li>
-              ))}
-            </ul>
-            {/* TODO the to= needs to be able to handle community_ids as well, since they're federated */}
-            <Link
-              class={`btn btn-sm btn-secondary btn-block mb-3 ${
-                (community.deleted || community.removed) && 'no-click'
-              }`}
-              to={`/create_post?community=${community.name}`}
-            >
-              {i18n.t('create_a_post')}
-            </Link>
-            <div>
-              {community.subscribed ? (
-                <button
-                  class="btn btn-sm btn-secondary btn-block"
-                  onClick={linkEvent(community.id, this.handleUnsubscribe)}
-                >
-                  {i18n.t('unsubscribe')}
-                </button>
+                  {i18n.t('remove')}
+                </span>
               ) : (
-                <button
-                  class="btn btn-sm btn-secondary btn-block"
-                  onClick={linkEvent(community.id, this.handleSubscribe)}
+                <span
+                  class="pointer"
+                  onClick={linkEvent(this, this.handleModRemoveSubmit)}
                 >
-                  {i18n.t('subscribe')}
-                </button>
+                  {i18n.t('restore')}
+                </span>
               )}
-            </div>
-          </div>
-        </div>
-        {community.description && (
-          <div class="card border-secondary">
-            <div class="card-body">
-              <div
-                className="md-div"
-                dangerouslySetInnerHTML={mdToHtml(community.description)}
+            </li>
+          )}
+        </ul>
+        {this.state.showRemoveDialog && (
+          <form onSubmit={linkEvent(this, this.handleModRemoveSubmit)}>
+            <div class="form-group row">
+              <label class="col-form-label" htmlFor="remove-reason">
+                {i18n.t('reason')}
+              </label>
+              <input
+                type="text"
+                id="remove-reason"
+                class="form-control mr-2"
+                placeholder={i18n.t('optional')}
+                value={this.state.removeReason}
+                onInput={linkEvent(this, this.handleModRemoveReasonChange)}
               />
             </div>
-          </div>
+            {/* TODO hold off on expires for now */}
+            {/* <div class="form-group row"> */}
+            {/*   <label class="col-form-label">Expires</label> */}
+            {/*   <input type="date" class="form-control mr-2" placeholder={i18n.t('expires')} value={this.state.removeExpires} onInput={linkEvent(this, this.handleModRemoveExpiresChange)} /> */}
+            {/* </div> */}
+            <div class="form-group row">
+              <button type="submit" class="btn btn-secondary">
+                {i18n.t('remove_community')}
+              </button>
+            </div>
+          </form>
         )}
-      </div>
+      </>
     );
   }
 
@@ -284,19 +379,36 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
 
   handleDeleteClick(i: Sidebar) {
     event.preventDefault();
-    let deleteForm: CommunityFormI = {
-      name: i.props.community.name,
-      title: i.props.community.title,
-      category_id: i.props.community.category_id,
+    let deleteForm: DeleteCommunityForm = {
       edit_id: i.props.community.id,
       deleted: !i.props.community.deleted,
-      nsfw: i.props.community.nsfw,
-      auth: null,
     };
-    WebSocketService.Instance.editCommunity(deleteForm);
+    WebSocketService.Instance.deleteCommunity(deleteForm);
+  }
+
+  handleShowConfirmLeaveModTeamClick(i: Sidebar) {
+    i.state.showConfirmLeaveModTeam = true;
+    i.setState(i.state);
+  }
+
+  handleLeaveModTeamClick(i: Sidebar) {
+    let form: AddModToCommunityForm = {
+      user_id: UserService.Instance.user.id,
+      community_id: i.props.community.id,
+      added: false,
+    };
+    WebSocketService.Instance.addModToCommunity(form);
+    i.state.showConfirmLeaveModTeam = false;
+    i.setState(i.state);
+  }
+
+  handleCancelLeaveModTeamClick(i: Sidebar) {
+    i.state.showConfirmLeaveModTeam = false;
+    i.setState(i.state);
   }
 
   handleUnsubscribe(communityId: number) {
+    event.preventDefault();
     let form: FollowCommunityForm = {
       community_id: communityId,
       follow: false,
@@ -305,6 +417,7 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
   }
 
   handleSubscribe(communityId: number) {
+    event.preventDefault();
     let form: FollowCommunityForm = {
       community_id: communityId,
       follow: true,
@@ -350,18 +463,13 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
 
   handleModRemoveSubmit(i: Sidebar) {
     event.preventDefault();
-    let deleteForm: CommunityFormI = {
-      name: i.props.community.name,
-      title: i.props.community.title,
-      category_id: i.props.community.category_id,
+    let removeForm: RemoveCommunityForm = {
       edit_id: i.props.community.id,
       removed: !i.props.community.removed,
       reason: i.state.removeReason,
       expires: getUnixTime(i.state.removeExpires),
-      nsfw: i.props.community.nsfw,
-      auth: null,
     };
-    WebSocketService.Instance.editCommunity(deleteForm);
+    WebSocketService.Instance.removeCommunity(removeForm);
 
     i.state.showRemoveDialog = false;
     i.setState(i.state);
index 291251d3304f2161a3a27b480b0ccd92d30e0401..9b572f57ecd0ed406dbf4498d3198c58e047e5fa 100644 (file)
@@ -1,10 +1,10 @@
 import { Component, linkEvent } from 'inferno';
 import { Prompt } from 'inferno-router';
-import { Site, SiteForm as SiteFormI } from '../interfaces';
+import { MarkdownTextArea } from './markdown-textarea';
+import { ImageUploadForm } from './image-upload-form';
+import { Site, SiteForm as SiteFormI } from 'lemmy-js-client';
 import { WebSocketService } from '../services';
-import { capitalizeFirstLetter, randomStr, setupTribute } from '../utils';
-import autosize from 'autosize';
-import Tribute from 'tributejs/src/Tribute.js';
+import { capitalizeFirstLetter, randomStr } from '../utils';
 import { i18n } from '../i18next';
 
 interface SiteFormProps {
@@ -19,13 +19,14 @@ interface SiteFormState {
 
 export class SiteForm extends Component<SiteFormProps, SiteFormState> {
   private id = `site-form-${randomStr()}`;
-  private tribute: Tribute;
   private emptyState: SiteFormState = {
     siteForm: {
       enable_downvotes: true,
       open_registration: true,
       enable_nsfw: true,
       name: null,
+      icon: null,
+      banner: null,
     },
     loading: false,
   };
@@ -33,8 +34,16 @@ export class SiteForm extends Component<SiteFormProps, SiteFormState> {
   constructor(props: any, context: any) {
     super(props, context);
 
-    this.tribute = setupTribute();
     this.state = this.emptyState;
+    this.handleSiteDescriptionChange = this.handleSiteDescriptionChange.bind(
+      this
+    );
+
+    this.handleIconUpload = this.handleIconUpload.bind(this);
+    this.handleIconRemove = this.handleIconRemove.bind(this);
+
+    this.handleBannerUpload = this.handleBannerUpload.bind(this);
+    this.handleBannerRemove = this.handleBannerRemove.bind(this);
 
     if (this.props.site) {
       this.state.siteForm = {
@@ -43,21 +52,12 @@ export class SiteForm extends Component<SiteFormProps, SiteFormState> {
         enable_downvotes: this.props.site.enable_downvotes,
         open_registration: this.props.site.open_registration,
         enable_nsfw: this.props.site.enable_nsfw,
+        icon: this.props.site.icon,
+        banner: this.props.site.banner,
       };
     }
   }
 
-  componentDidMount() {
-    var textarea: any = document.getElementById(this.id);
-    autosize(textarea);
-    this.tribute.attach(textarea);
-    textarea.addEventListener('tribute-replaced', () => {
-      this.state.siteForm.description = textarea.value;
-      this.setState(this.state);
-      autosize.update(textarea);
-    });
-  }
-
   // Necessary to stop the loading
   componentWillReceiveProps() {
     this.state.loading = false;
@@ -114,18 +114,34 @@ export class SiteForm extends Component<SiteFormProps, SiteFormState> {
               />
             </div>
           </div>
+          <div class="form-group">
+            <label>{i18n.t('icon')}</label>
+            <ImageUploadForm
+              uploadTitle={i18n.t('upload_icon')}
+              imageSrc={this.state.siteForm.icon}
+              onUpload={this.handleIconUpload}
+              onRemove={this.handleIconRemove}
+              rounded
+            />
+          </div>
+          <div class="form-group">
+            <label>{i18n.t('banner')}</label>
+            <ImageUploadForm
+              uploadTitle={i18n.t('upload_banner')}
+              imageSrc={this.state.siteForm.banner}
+              onUpload={this.handleBannerUpload}
+              onRemove={this.handleBannerRemove}
+            />
+          </div>
           <div class="form-group row">
             <label class="col-12 col-form-label" htmlFor={this.id}>
               {i18n.t('sidebar')}
             </label>
             <div class="col-12">
-              <textarea
-                id={this.id}
-                value={this.state.siteForm.description}
-                onInput={linkEvent(this, this.handleSiteDescriptionChange)}
-                class="form-control"
-                rows={3}
-                maxLength={10000}
+              <MarkdownTextArea
+                initialContent={this.state.siteForm.description}
+                onContentChange={this.handleSiteDescriptionChange}
+                hideNavigationWarnings
               />
             </div>
           </div>
@@ -238,9 +254,9 @@ export class SiteForm extends Component<SiteFormProps, SiteFormState> {
     i.setState(i.state);
   }
 
-  handleSiteDescriptionChange(i: SiteForm, event: any) {
-    i.state.siteForm.description = event.target.value;
-    i.setState(i.state);
+  handleSiteDescriptionChange(val: string) {
+    this.state.siteForm.description = val;
+    this.setState(this.state);
   }
 
   handleSiteEnableNsfwChange(i: SiteForm, event: any) {
@@ -261,4 +277,24 @@ export class SiteForm extends Component<SiteFormProps, SiteFormState> {
   handleCancel(i: SiteForm) {
     i.props.onCancel();
   }
+
+  handleIconUpload(url: string) {
+    this.state.siteForm.icon = url;
+    this.setState(this.state);
+  }
+
+  handleIconRemove() {
+    this.state.siteForm.icon = '';
+    this.setState(this.state);
+  }
+
+  handleBannerUpload(url: string) {
+    this.state.siteForm.banner = url;
+    this.setState(this.state);
+  }
+
+  handleBannerRemove() {
+    this.state.siteForm.banner = '';
+    this.setState(this.state);
+  }
 }
index 33d6581991dc4375541b5496520d9bd583f56693..1f0fb055716a0d2202c24a2345ee9e014355875a 100644 (file)
@@ -1,6 +1,6 @@
 import { Component, linkEvent } from 'inferno';
-import { SortType } from '../interfaces';
-import { sortingHelpUrl } from '../utils';
+import { SortType } from 'lemmy-js-client';
+import { sortingHelpUrl, randomStr } from '../utils';
 import { i18n } from '../i18next';
 
 interface SortSelectProps {
@@ -14,6 +14,7 @@ interface SortSelectState {
 }
 
 export class SortSelect extends Component<SortSelectProps, SortSelectState> {
+  private id = `sort-select-${randomStr()}`;
   private emptyState: SortSelectState = {
     sort: this.props.sort,
   };
@@ -33,13 +34,18 @@ export class SortSelect extends Component<SortSelectProps, SortSelectState> {
     return (
       <>
         <select
+          id={this.id}
+          name={this.id}
           value={this.state.sort}
           onChange={linkEvent(this, this.handleSortChange)}
-          class="custom-select custom-select-sm w-auto mr-2"
+          class="custom-select w-auto mr-2 mb-2"
         >
           <option disabled>{i18n.t('sort_type')}</option>
           {!this.props.hideHot && (
-            <option value={SortType.Hot}>{i18n.t('hot')}</option>
+            <>
+              <option value={SortType.Active}>{i18n.t('active')}</option>
+              <option value={SortType.Hot}>{i18n.t('hot')}</option>
+            </>
           )}
           <option value={SortType.New}>{i18n.t('new')}</option>
           <option disabled>─────</option>
index 6ff9b2caa6c961a98544990b91852d75b87f94b3..3da9fc2be9f830f1bef602694375a84b316f1aaa 100644 (file)
@@ -1,12 +1,14 @@
 import { Component } from 'inferno';
+import { Helmet } from 'inferno-helmet';
 import { Subscription } from 'rxjs';
 import { retryWhen, delay, take } from 'rxjs/operators';
 import { WebSocketService } from '../services';
 import {
   GetSiteResponse,
+  Site,
   WebSocketJsonResponse,
   UserOperation,
-} from '../interfaces';
+} from 'lemmy-js-client';
 import { i18n } from '../i18next';
 import { T } from 'inferno-i18next';
 import { repoUrl, wsJsonToRes, toast } from '../utils';
@@ -17,6 +19,11 @@ interface SilverUser {
 }
 
 let general = [
+  'Brendan',
+  'mexicanhalloween',
+  'William Moore',
+  'Rachel Schmitz',
+  'comradeda',
   'ybaumy',
   'dude in phx',
   'twilight loki',
@@ -29,7 +36,7 @@ let general = [
   'Andre Vallestero',
   'NotTooHighToHack',
 ];
-let highlighted = ['DiscountFuneral', 'Oskenso Kashi', 'Alex Benishek'];
+let highlighted = ['DQW', 'DiscountFuneral', 'Oskenso Kashi', 'Alex Benishek'];
 let silver: Array<SilverUser> = [
   {
     name: 'Redjoker',
@@ -39,10 +46,18 @@ let silver: Array<SilverUser> = [
 // let gold = [];
 // let latinum = [];
 
-export class Sponsors extends Component<any, any> {
+interface SponsorsState {
+  site: Site;
+}
+
+export class Sponsors extends Component<any, SponsorsState> {
   private subscription: Subscription;
+  private emptyState: SponsorsState = {
+    site: undefined,
+  };
   constructor(props: any, context: any) {
     super(props, context);
+    this.state = this.emptyState;
     this.subscription = WebSocketService.Instance.subject
       .pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
       .subscribe(
@@ -62,9 +77,18 @@ export class Sponsors extends Component<any, any> {
     this.subscription.unsubscribe();
   }
 
+  get documentTitle(): string {
+    if (this.state.site) {
+      return `${i18n.t('sponsors')} - ${this.state.site.name}`;
+    } else {
+      return 'Lemmy';
+    }
+  }
+
   render() {
     return (
       <div class="container text-center">
+        <Helmet title={this.documentTitle} />
         {this.topMessage()}
         <hr />
         {this.sponsors()}
@@ -106,7 +130,7 @@ export class Sponsors extends Component<any, any> {
       <div class="container">
         <h5>{i18n.t('sponsors')}</h5>
         <p>{i18n.t('silver_sponsors')}</p>
-        <div class="row card-columns">
+        <div class="row justify-content-md-center card-columns">
           {silver.map(s => (
             <div class="card col-12 col-md-2">
               <div>
@@ -122,7 +146,7 @@ export class Sponsors extends Component<any, any> {
           ))}
         </div>
         <p>{i18n.t('general_sponsors')}</p>
-        <div class="row card-columns">
+        <div class="row justify-content-md-center card-columns">
           {highlighted.map(s => (
             <div class="card bg-primary col-12 col-md-2 font-weight-bold">
               <div>{s}</div>
@@ -180,7 +204,8 @@ export class Sponsors extends Component<any, any> {
       return;
     } else if (res.op == UserOperation.GetSite) {
       let data = res.data as GetSiteResponse;
-      document.title = `${i18n.t('sponsors')} - ${data.site.name}`;
+      this.state.site = data.site;
+      this.setState(this.state);
     }
   }
 }
index 3386dbe59da38d9a68defebfd456f5101321e529..327a40be8ba7512a894a6466d523de36dc17748b 100644 (file)
@@ -15,6 +15,36 @@ export class Symbols extends Component<any, any> {
         xmlnsXlink="http://www.w3.org/1999/xlink"
       >
         <defs>
+          <symbol id="icon-x" viewBox="0 0 24 24">
+            <path d="M5.293 6.707l5.293 5.293-5.293 5.293c-0.391 0.391-0.391 1.024 0 1.414s1.024 0.391 1.414 0l5.293-5.293 5.293 5.293c0.391 0.391 1.024 0.391 1.414 0s0.391-1.024 0-1.414l-5.293-5.293 5.293-5.293c0.391-0.391 0.391-1.024 0-1.414s-1.024-0.391-1.414 0l-5.293 5.293-5.293-5.293c-0.391-0.391-1.024-0.391-1.414 0s-0.391 1.024 0 1.414z"></path>
+          </symbol>
+          <symbol id="icon-refresh-cw" viewBox="0 0 24 24">
+            <path d="M4.453 9.334c0.737-2.083 2.247-3.669 4.096-4.552s4.032-1.059 6.114-0.322c1.186 0.42 2.206 1.088 2.983 1.88l2.83 2.66h-3.476c-0.552 0-1 0.448-1 1s0.448 1 1 1h5.997c0.005 0 0.009 0 0.014 0 0.137-0.001 0.268-0.031 0.386-0.082 0.119-0.051 0.229-0.126 0.324-0.225 0.012-0.013 0.024-0.026 0.036-0.039 0.075-0.087 0.133-0.183 0.173-0.285s0.064-0.211 0.069-0.326c0.001-0.015 0.001-0.029 0.001-0.043v-6c0-0.552-0.448-1-1-1s-1 0.448-1 1v3.689l-2.926-2.749c-0.992-1.010-2.271-1.843-3.743-2.364-2.603-0.921-5.335-0.699-7.643 0.402s-4.199 3.086-5.12 5.689c-0.185 0.52 0.088 1.091 0.608 1.276s1.092-0.088 1.276-0.609zM2 16.312l2.955 2.777c1.929 1.931 4.49 2.908 7.048 2.909s5.119-0.975 7.072-2.927c1.104-1.104 1.901-2.407 2.361-3.745 0.18-0.522-0.098-1.091-0.621-1.271s-1.091 0.098-1.271 0.621c-0.361 1.050-0.993 2.091-1.883 2.981-1.563 1.562-3.609 2.342-5.657 2.342s-4.094-0.782-5.679-2.366l-2.8-2.633h3.475c0.552 0 1-0.448 1-1s-0.448-1-1-1h-5.997c-0.005 0-0.009 0-0.014 0-0.137 0.001-0.268 0.031-0.386 0.082-0.119 0.051-0.229 0.126-0.324 0.225-0.012 0.013-0.024 0.026-0.036 0.039-0.075 0.087-0.133 0.183-0.173 0.285s-0.064 0.211-0.069 0.326c-0.001 0.015-0.001 0.029-0.001 0.043v6c0 0.552 0.448 1 1 1s1-0.448 1-1z"></path>
+          </symbol>
+          <symbol id="icon-play" viewBox="0 0 24 24">
+            <path d="M5.541 2.159c-0.153-0.1-0.34-0.159-0.541-0.159-0.552 0-1 0.448-1 1v18c-0.001 0.182 0.050 0.372 0.159 0.541 0.299 0.465 0.917 0.599 1.382 0.3l14-9c0.114-0.072 0.219-0.174 0.3-0.3 0.299-0.465 0.164-1.083-0.3-1.382zM6 4.832l11.151 7.168-11.151 7.168z"></path>
+          </symbol>
+          <symbol id="icon-strikethrough" viewBox="0 0 28 28">
+            <path d="M27.5 14c0.281 0 0.5 0.219 0.5 0.5v1c0 0.281-0.219 0.5-0.5 0.5h-27c-0.281 0-0.5-0.219-0.5-0.5v-1c0-0.281 0.219-0.5 0.5-0.5h27zM7.547 13c-0.297-0.375-0.562-0.797-0.797-1.25-0.5-1.016-0.75-2-0.75-2.938 0-1.906 0.703-3.5 2.094-4.828s3.437-1.984 6.141-1.984c0.594 0 1.453 0.109 2.609 0.297 0.688 0.125 1.609 0.375 2.766 0.75 0.109 0.406 0.219 1.031 0.328 1.844 0.141 1.234 0.219 2.187 0.219 2.859 0 0.219-0.031 0.453-0.078 0.703l-0.187 0.047-1.313-0.094-0.219-0.031c-0.531-1.578-1.078-2.641-1.609-3.203-0.922-0.953-2.031-1.422-3.281-1.422-1.188 0-2.141 0.313-2.844 0.922s-1.047 1.375-1.047 2.281c0 0.766 0.344 1.484 1.031 2.188s2.141 1.375 4.359 2.016c0.75 0.219 1.641 0.562 2.703 1.031 0.562 0.266 1.062 0.531 1.484 0.812h-11.609zM15.469 17h6.422c0.078 0.438 0.109 0.922 0.109 1.437 0 1.125-0.203 2.234-0.641 3.313-0.234 0.578-0.594 1.109-1.109 1.625-0.375 0.359-0.938 0.781-1.703 1.266-0.781 0.469-1.563 0.828-2.391 1.031-0.828 0.219-1.875 0.328-3.172 0.328-0.859 0-1.891-0.031-3.047-0.359l-2.188-0.625c-0.609-0.172-0.969-0.313-1.125-0.438-0.063-0.063-0.125-0.172-0.125-0.344v-0.203c0-0.125 0.031-0.938-0.031-2.438-0.031-0.781 0.031-1.328 0.031-1.641v-0.688l1.594-0.031c0.578 1.328 0.844 2.125 1.016 2.406 0.375 0.609 0.797 1.094 1.25 1.469s1 0.672 1.641 0.891c0.625 0.234 1.328 0.344 2.063 0.344 0.656 0 1.391-0.141 2.172-0.422 0.797-0.266 1.437-0.719 1.906-1.344 0.484-0.625 0.734-1.297 0.734-2.016 0-0.875-0.422-1.687-1.266-2.453-0.344-0.297-1.062-0.672-2.141-1.109z"></path>
+          </symbol>
+          <symbol id="icon-header" viewBox="0 0 28 28">
+            <path d="M26.281 26c-1.375 0-2.766-0.109-4.156-0.109-1.375 0-2.75 0.109-4.125 0.109-0.531 0-0.781-0.578-0.781-1.031 0-1.391 1.563-0.797 2.375-1.328 0.516-0.328 0.516-1.641 0.516-2.188l-0.016-6.109c0-0.172 0-0.328-0.016-0.484-0.25-0.078-0.531-0.063-0.781-0.063h-10.547c-0.266 0-0.547-0.016-0.797 0.063-0.016 0.156-0.016 0.313-0.016 0.484l-0.016 5.797c0 0.594 0 2.219 0.578 2.562 0.812 0.5 2.656-0.203 2.656 1.203 0 0.469-0.219 1.094-0.766 1.094-1.453 0-2.906-0.109-4.344-0.109-1.328 0-2.656 0.109-3.984 0.109-0.516 0-0.75-0.594-0.75-1.031 0-1.359 1.437-0.797 2.203-1.328 0.5-0.344 0.516-1.687 0.516-2.234l-0.016-0.891v-12.703c0-0.75 0.109-3.156-0.594-3.578-0.781-0.484-2.453 0.266-2.453-1.141 0-0.453 0.203-1.094 0.75-1.094 1.437 0 2.891 0.109 4.328 0.109 1.313 0 2.641-0.109 3.953-0.109 0.562 0 0.781 0.625 0.781 1.094 0 1.344-1.547 0.688-2.312 1.172-0.547 0.328-0.547 1.937-0.547 2.5l0.016 5c0 0.172 0 0.328 0.016 0.5 0.203 0.047 0.406 0.047 0.609 0.047h10.922c0.187 0 0.391 0 0.594-0.047 0.016-0.172 0.016-0.328 0.016-0.5l0.016-5c0-0.578 0-2.172-0.547-2.5-0.781-0.469-2.344 0.156-2.344-1.172 0-0.469 0.219-1.094 0.781-1.094 1.375 0 2.75 0.109 4.125 0.109 1.344 0 2.688-0.109 4.031-0.109 0.562 0 0.781 0.625 0.781 1.094 0 1.359-1.609 0.672-2.391 1.156-0.531 0.344-0.547 1.953-0.547 2.516l0.016 14.734c0 0.516 0.031 1.875 0.531 2.188 0.797 0.5 2.484-0.141 2.484 1.219 0 0.453-0.203 1.094-0.75 1.094z"></path>
+          </symbol>
+          <symbol id="icon-list" viewBox="0 0 24 24">
+            <path d="M8 7h13c0.552 0 1-0.448 1-1s-0.448-1-1-1h-13c-0.552 0-1 0.448-1 1s0.448 1 1 1zM8 13h13c0.552 0 1-0.448 1-1s-0.448-1-1-1h-13c-0.552 0-1 0.448-1 1s0.448 1 1 1zM8 19h13c0.552 0 1-0.448 1-1s-0.448-1-1-1h-13c-0.552 0-1 0.448-1 1s0.448 1 1 1zM3 7c0.552 0 1-0.448 1-1s-0.448-1-1-1-1 0.448-1 1 0.448 1 1 1zM3 13c0.552 0 1-0.448 1-1s-0.448-1-1-1-1 0.448-1 1 0.448 1 1 1zM3 19c0.552 0 1-0.448 1-1s-0.448-1-1-1-1 0.448-1 1 0.448 1 1 1z"></path>
+          </symbol>
+          <symbol id="icon-italic" viewBox="0 0 24 24">
+            <path d="M13.557 5l-5.25 14h-3.307c-0.552 0-1 0.448-1 1s0.448 1 1 1h9c0.552 0 1-0.448 1-1s-0.448-1-1-1h-3.557l5.25-14h3.307c0.552 0 1-0.448 1-1s-0.448-1-1-1h-9c-0.552 0-1 0.448-1 1s0.448 1 1 1z"></path>
+          </symbol>
+          <symbol id="icon-code" viewBox="0 0 24 24">
+            <path d="M16.707 18.707l6-6c0.391-0.391 0.391-1.024 0-1.414l-6-6c-0.391-0.391-1.024-0.391-1.414 0s-0.391 1.024 0 1.414l5.293 5.293-5.293 5.293c-0.391 0.391-0.391 1.024 0 1.414s1.024 0.391 1.414 0zM7.293 5.293l-6 6c-0.391 0.391-0.391 1.024 0 1.414l6 6c0.391 0.391 1.024 0.391 1.414 0s0.391-1.024 0-1.414l-5.293-5.293 5.293-5.293c0.391-0.391 0.391-1.024 0-1.414s-1.024-0.391-1.414 0z"></path>
+          </symbol>
+          <symbol id="icon-bold" viewBox="0 0 24 24">
+            <path d="M7 11v-6h7c0.829 0 1.577 0.335 2.121 0.879s0.879 1.292 0.879 2.121-0.335 1.577-0.879 2.121-1.292 0.879-2.121 0.879zM5 12v8c0 0.552 0.448 1 1 1h9c1.38 0 2.632-0.561 3.536-1.464s1.464-2.156 1.464-3.536-0.561-2.632-1.464-3.536c-0.325-0.325-0.695-0.606-1.1-0.832 0.034-0.032 0.067-0.064 0.1-0.097 0.903-0.903 1.464-2.155 1.464-3.535s-0.561-2.632-1.464-3.536-2.156-1.464-3.536-1.464h-8c-0.552 0-1 0.448-1 1zM7 13h8c0.829 0 1.577 0.335 2.121 0.879s0.879 1.292 0.879 2.121-0.335 1.577-0.879 2.121-1.292 0.879-2.121 0.879h-8z"></path>
+          </symbol>
+          <symbol id="icon-format_quote" viewBox="0 0 24 24">
+            <path d="M14.016 17.016l1.969-4.031h-3v-6h6v6l-1.969 4.031h-3zM6 17.016l2.016-4.031h-3v-6h6v6l-2.016 4.031h-3z"></path>
+          </symbol>
           <symbol id="icon-settings" viewBox="0 0 24 24">
             <path d="M16 12c0-1.104-0.449-2.106-1.172-2.828s-1.724-1.172-2.828-1.172-2.106 0.449-2.828 1.172-1.172 1.724-1.172 2.828 0.449 2.106 1.172 2.828 1.724 1.172 2.828 1.172 2.106-0.449 2.828-1.172 1.172-1.724 1.172-2.828zM14 12c0 0.553-0.223 1.051-0.586 1.414s-0.861 0.586-1.414 0.586-1.051-0.223-1.414-0.586-0.586-0.861-0.586-1.414 0.223-1.051 0.586-1.414 0.861-0.586 1.414-0.586 1.051 0.223 1.414 0.586 0.586 0.861 0.586 1.414zM20.315 15.404c0.046-0.105 0.112-0.191 0.192-0.257 0.112-0.092 0.251-0.146 0.403-0.147h0.090c0.828 0 1.58-0.337 2.121-0.879s0.879-1.293 0.879-2.121-0.337-1.58-0.879-2.121-1.293-0.879-2.121-0.879h-0.159c-0.11-0.001-0.215-0.028-0.308-0.076-0.127-0.066-0.23-0.172-0.292-0.312-0.003-0.029-0.004-0.059-0.004-0.089-0.024-0.055-0.040-0.111-0.049-0.168 0.020-0.334 0.077-0.454 0.168-0.547l0.062-0.062c0.585-0.586 0.878-1.356 0.877-2.122s-0.294-1.536-0.881-2.122c-0.586-0.585-1.356-0.878-2.122-0.877s-1.536 0.294-2.12 0.879l-0.046 0.046c-0.083 0.080-0.183 0.136-0.288 0.166-0.14 0.039-0.291 0.032-0.438-0.033-0.101-0.044-0.187-0.11-0.253-0.19-0.092-0.112-0.146-0.251-0.147-0.403v-0.090c0-0.828-0.337-1.58-0.879-2.121s-1.293-0.879-2.121-0.879-1.58 0.337-2.121 0.879-0.879 1.293-0.879 2.121v0.159c-0.001 0.11-0.028 0.215-0.076 0.308-0.066 0.127-0.172 0.23-0.312 0.292-0.029 0.003-0.059 0.004-0.089 0.004-0.055 0.024-0.111 0.040-0.168 0.049-0.335-0.021-0.455-0.078-0.548-0.169l-0.062-0.062c-0.586-0.585-1.355-0.878-2.122-0.878s-1.535 0.294-2.122 0.882c-0.585 0.586-0.878 1.355-0.878 2.122s0.294 1.536 0.879 2.12l0.048 0.047c0.080 0.083 0.136 0.183 0.166 0.288 0.039 0.14 0.032 0.291-0.031 0.434-0.006 0.016-0.013 0.034-0.021 0.052-0.041 0.109-0.108 0.203-0.191 0.275-0.11 0.095-0.25 0.153-0.383 0.156h-0.090c-0.828 0-1.58 0.337-2.121 0.879s-0.879 1.294-0.879 2.122 0.337 1.58 0.879 2.121 1.293 0.879 2.121 0.879h0.159c0.11 0.001 0.215 0.028 0.308 0.076 0.128 0.067 0.233 0.174 0.296 0.321 0.024 0.055 0.040 0.111 0.049 0.168-0.020 0.334-0.077 0.454-0.168 0.547l-0.062 0.062c-0.585 0.586-0.878 1.356-0.877 2.122s0.294 1.536 0.881 2.122c0.586 0.585 1.356 0.878 2.122 0.877s1.536-0.294 2.12-0.879l0.047-0.048c0.083-0.080 0.183-0.136 0.288-0.166 0.14-0.039 0.291-0.032 0.434 0.031 0.016 0.006 0.034 0.013 0.052 0.021 0.109 0.041 0.203 0.108 0.275 0.191 0.095 0.11 0.153 0.25 0.156 0.383v0.092c0 0.828 0.337 1.58 0.879 2.121s1.293 0.879 2.121 0.879 1.58-0.337 2.121-0.879 0.879-1.293 0.879-2.121v-0.159c0.001-0.11 0.028-0.215 0.076-0.308 0.067-0.128 0.174-0.233 0.321-0.296 0.055-0.024 0.111-0.040 0.168-0.049 0.334 0.020 0.454 0.077 0.547 0.168l0.062 0.062c0.586 0.585 1.356 0.878 2.122 0.877s1.536-0.294 2.122-0.881c0.585-0.586 0.878-1.356 0.877-2.122s-0.294-1.536-0.879-2.12l-0.048-0.047c-0.080-0.083-0.136-0.183-0.166-0.288-0.039-0.14-0.032-0.291 0.031-0.434zM18.396 9.302c-0.012-0.201-0.038-0.297-0.076-0.382v0.080c0 0.043 0.003 0.084 0.008 0.125 0.021 0.060 0.043 0.119 0.068 0.177 0.004 0.090 0.005 0.091 0.005 0.092 0.249 0.581 0.684 1.030 1.208 1.303 0.371 0.193 0.785 0.298 1.211 0.303h0.18c0.276 0 0.525 0.111 0.707 0.293s0.293 0.431 0.293 0.707-0.111 0.525-0.293 0.707-0.431 0.293-0.707 0.293h-0.090c-0.637 0.003-1.22 0.228-1.675 0.603-0.323 0.266-0.581 0.607-0.75 0.993-0.257 0.582-0.288 1.21-0.127 1.782 0.119 0.423 0.341 0.814 0.652 1.136l0.072 0.073c0.196 0.196 0.294 0.45 0.294 0.707s-0.097 0.512-0.292 0.707c-0.197 0.197-0.451 0.295-0.709 0.295s-0.512-0.097-0.707-0.292l-0.061-0.061c-0.463-0.453-1.040-0.702-1.632-0.752-0.437-0.037-0.882 0.034-1.293 0.212-0.578 0.248-1.027 0.683-1.3 1.206-0.193 0.371-0.298 0.785-0.303 1.211v0.181c0 0.276-0.111 0.525-0.293 0.707s-0.43 0.292-0.706 0.292-0.525-0.111-0.707-0.293-0.293-0.431-0.293-0.707v-0.090c-0.015-0.66-0.255-1.242-0.644-1.692-0.284-0.328-0.646-0.585-1.058-0.744-0.575-0.247-1.193-0.274-1.756-0.116-0.423 0.119-0.814 0.341-1.136 0.652l-0.073 0.072c-0.196 0.196-0.45 0.294-0.707 0.294s-0.512-0.097-0.707-0.292c-0.197-0.197-0.295-0.451-0.295-0.709s0.097-0.512 0.292-0.707l0.061-0.061c0.453-0.463 0.702-1.040 0.752-1.632 0.037-0.437-0.034-0.882-0.212-1.293-0.248-0.578-0.683-1.027-1.206-1.3-0.371-0.193-0.785-0.298-1.211-0.303l-0.18 0.001c-0.276 0-0.525-0.111-0.707-0.293s-0.293-0.431-0.293-0.707 0.111-0.525 0.293-0.707 0.431-0.293 0.707-0.293h0.090c0.66-0.015 1.242-0.255 1.692-0.644 0.328-0.284 0.585-0.646 0.744-1.058 0.247-0.575 0.274-1.193 0.116-1.756-0.119-0.423-0.341-0.814-0.652-1.136l-0.073-0.073c-0.196-0.196-0.294-0.45-0.294-0.707s0.097-0.512 0.292-0.707c0.197-0.197 0.451-0.295 0.709-0.295s0.512 0.097 0.707 0.292l0.061 0.061c0.463 0.453 1.040 0.702 1.632 0.752 0.37 0.032 0.745-0.014 1.101-0.137 0.096-0.012 0.186-0.036 0.266-0.072-0.031 0.001-0.061 0.003-0.089 0.004-0.201 0.012-0.297 0.038-0.382 0.076h0.080c0.043 0 0.084-0.003 0.125-0.008 0.060-0.021 0.119-0.043 0.177-0.068 0.090-0.004 0.091-0.005 0.092-0.005 0.581-0.249 1.030-0.684 1.303-1.208 0.193-0.37 0.298-0.785 0.303-1.21v-0.181c0-0.276 0.111-0.525 0.293-0.707s0.431-0.293 0.707-0.293 0.525 0.111 0.707 0.293 0.293 0.431 0.293 0.707v0.090c0.003 0.637 0.228 1.22 0.603 1.675 0.266 0.323 0.607 0.581 0.996 0.751 0.578 0.255 1.206 0.286 1.778 0.125 0.423-0.119 0.814-0.341 1.136-0.652l0.073-0.072c0.196-0.196 0.45-0.294 0.707-0.294s0.512 0.097 0.707 0.292c0.197 0.197 0.295 0.451 0.295 0.709s-0.097 0.512-0.292 0.707l-0.061 0.061c-0.453 0.463-0.702 1.040-0.752 1.632-0.032 0.37 0.014 0.745 0.137 1.101 0.012 0.095 0.037 0.185 0.072 0.266-0.001-0.032-0.002-0.062-0.004-0.089z"></path>
           </symbol>
@@ -171,6 +201,12 @@ export class Symbols extends Component<any, any> {
           <symbol id="icon-cake" viewBox="0 0 24 24">
             <path d="M 23.296875 22.394531 L 22.082031 22.394531 L 22.082031 17.007812 C 22.453125 16.699219 22.664062 16.261719 22.664062 15.796875 L 22.664062 13.984375 C 22.664062 12.996094 21.785156 12.191406 20.703125 12.191406 L 19.785156 12.191406 L 19.785156 7.785156 C 19.785156 7.050781 19.1875 6.449219 18.449219 6.449219 L 18.367188 6.449219 L 18.367188 5.96875 C 19.199219 5.675781 19.796875 4.882812 19.796875 3.957031 C 19.796875 3.644531 19.703125 3.117188 18.996094 1.800781 C 18.632812 1.121094 18.273438 0.550781 18.257812 0.527344 C 18.128906 0.320312 17.90625 0.199219 17.664062 0.199219 C 17.421875 0.199219 17.199219 0.320312 17.070312 0.527344 C 17.054688 0.550781 16.695312 1.121094 16.332031 1.800781 C 15.621094 3.117188 15.53125 3.644531 15.53125 3.957031 C 15.53125 4.882812 16.128906 5.675781 16.960938 5.96875 L 16.960938 6.449219 L 16.878906 6.449219 C 16.140625 6.449219 15.542969 7.050781 15.542969 7.785156 L 15.542969 12.191406 L 14.121094 12.191406 L 14.121094 7.785156 C 14.121094 7.050781 13.523438 6.449219 12.785156 6.449219 L 12.703125 6.449219 L 12.703125 5.96875 C 13.535156 5.675781 14.132812 4.882812 14.132812 3.957031 C 14.132812 3.644531 14.039062 3.117188 13.332031 1.800781 C 12.96875 1.121094 12.609375 0.550781 12.59375 0.527344 C 12.464844 0.320312 12.242188 0.199219 12 0.199219 C 11.757812 0.199219 11.535156 0.320312 11.40625 0.527344 C 11.390625 0.550781 11.03125 1.121094 10.667969 1.800781 C 9.960938 3.117188 9.867188 3.644531 9.867188 3.957031 C 9.867188 4.882812 10.464844 5.675781 11.296875 5.96875 L 11.296875 6.449219 L 11.214844 6.449219 C 10.476562 6.449219 9.878906 7.050781 9.878906 7.785156 L 9.878906 12.191406 L 8.457031 12.191406 L 8.457031 7.785156 C 8.457031 7.050781 7.859375 6.449219 7.121094 6.449219 L 7.039062 6.449219 L 7.039062 5.96875 C 7.871094 5.675781 8.46875 4.882812 8.46875 3.957031 C 8.46875 3.644531 8.378906 3.117188 7.667969 1.800781 C 7.304688 1.121094 6.945312 0.550781 6.929688 0.527344 C 6.800781 0.320312 6.578125 0.199219 6.335938 0.199219 C 6.09375 0.199219 5.871094 0.320312 5.742188 0.527344 C 5.726562 0.550781 5.367188 1.121094 5.003906 1.800781 C 4.296875 3.117188 4.203125 3.644531 4.203125 3.957031 C 4.203125 4.882812 4.800781 5.675781 5.632812 5.96875 L 5.632812 6.449219 L 5.550781 6.449219 C 4.8125 6.449219 4.214844 7.050781 4.214844 7.785156 L 4.214844 12.191406 L 3.296875 12.191406 C 2.214844 12.191406 1.335938 12.996094 1.335938 13.984375 L 1.335938 15.796875 C 1.335938 16.261719 1.546875 16.699219 1.917969 17.007812 L 1.917969 22.394531 L 0.703125 22.394531 C 0.316406 22.394531 0 22.710938 0 23.097656 C 0 23.488281 0.316406 23.800781 0.703125 23.800781 L 23.296875 23.800781 C 23.683594 23.800781 24 23.488281 24 23.097656 C 24 22.710938 23.683594 22.394531 23.296875 22.394531 Z M 16.9375 3.957031 C 16.941406 3.730469 17.246094 3.054688 17.664062 2.289062 C 18.082031 3.054688 18.382812 3.730469 18.390625 3.957031 C 18.390625 4.355469 18.0625 4.679688 17.664062 4.679688 C 17.265625 4.679688 16.9375 4.355469 16.9375 3.957031 Z M 16.949219 7.855469 L 18.378906 7.855469 L 18.378906 12.1875 L 16.949219 12.1875 Z M 11.273438 3.957031 C 11.277344 3.730469 11.582031 3.054688 12 2.289062 C 12.417969 3.054688 12.722656 3.730469 12.726562 3.957031 C 12.726562 4.355469 12.398438 4.679688 12 4.679688 C 11.601562 4.679688 11.273438 4.355469 11.273438 3.957031 Z M 11.285156 7.855469 L 12.714844 7.855469 L 12.714844 12.1875 L 11.285156 12.1875 Z M 5.609375 3.957031 C 5.613281 3.730469 5.917969 3.054688 6.335938 2.289062 C 6.753906 3.054688 7.058594 3.730469 7.0625 3.957031 C 7.0625 4.355469 6.734375 4.679688 6.335938 4.679688 C 5.9375 4.679688 5.609375 4.355469 5.609375 3.957031 Z M 5.621094 7.855469 L 7.050781 7.855469 L 7.050781 12.1875 L 5.621094 12.1875 Z M 20.675781 22.394531 L 3.324219 22.394531 L 3.324219 17.414062 C 3.433594 17.398438 3.546875 17.378906 3.652344 17.347656 L 5.429688 16.820312 C 6.453125 16.515625 7.582031 16.515625 8.609375 16.820312 L 10.011719 17.234375 C 10.652344 17.425781 11.324219 17.519531 12 17.519531 C 12.675781 17.519531 13.347656 17.425781 13.988281 17.234375 L 15.390625 16.820312 C 16.417969 16.515625 17.546875 16.515625 18.570312 16.820312 L 20.347656 17.347656 C 20.453125 17.378906 20.5625 17.398438 20.675781 17.414062 Z M 21.257812 15.796875 C 21.257812 15.855469 21.210938 15.902344 21.171875 15.933594 C 21.082031 16 20.925781 16.050781 20.746094 15.996094 L 18.972656 15.472656 C 17.6875 15.09375 16.273438 15.09375 14.992188 15.472656 L 13.589844 15.886719 C 12.566406 16.191406 11.433594 16.191406 10.410156 15.886719 L 9.007812 15.472656 C 8.367188 15.28125 7.691406 15.1875 7.019531 15.1875 C 6.34375 15.1875 5.671875 15.28125 5.027344 15.472656 L 3.253906 15.996094 C 3.074219 16.050781 2.917969 16 2.828125 15.933594 C 2.789062 15.902344 2.742188 15.855469 2.742188 15.796875 L 2.742188 13.984375 C 2.742188 13.800781 2.96875 13.597656 3.296875 13.597656 L 20.703125 13.597656 C 21.03125 13.597656 21.257812 13.800781 21.257812 13.984375 Z M 21.257812 15.796875 " />
           </symbol>
+          <symbol id="icon-subscript" viewBox="0 0 20 20">
+            <path d="M13.68 16h-2.42a.67.67 0 0 1-.46-.15 1.33 1.33 0 0 1-.28-.34l-2.77-4.44a2.65 2.65 0 0 1-.28.69L5 15.51a2.22 2.22 0 0 1-.29.34.58.58 0 0 1-.42.15H2l4.15-6.19L2.17 4h2.42a.81.81 0 0 1 .41.09.8.8 0 0 1 .24.26L8 8.59a2.71 2.71 0 0 1 .33-.74L10.6 4.4a.69.69 0 0 1 .6-.4h2.32l-4 5.71zm3.82-4h.5v-1h-.5a1.49 1.49 0 0 0-1 .39 1.49 1.49 0 0 0-1-.39H15v1h.5a.5.5 0 0 1 .5.5v6a.5.5 0 0 1-.5.5H15v1h.5a1.49 1.49 0 0 0 1-.39 1.49 1.49 0 0 0 1 .39h.5v-1h-.5a.5.5 0 0 1-.5-.5v-6a.5.5 0 0 1 .5-.5z" />
+          </symbol>
+          <symbol id="icon-superscript" viewBox="0 0 20 20">
+            <path d="M17.5 1h.5V0h-.5a1.49 1.49 0 0 0-1 .39 1.49 1.49 0 0 0-1-.39H15v1h.5a.5.5 0 0 1 .5.5v6a.5.5 0 0 1-.5.5H15v1h.5a1.49 1.49 0 0 0 1-.39 1.49 1.49 0 0 0 1 .39h.5V8h-.5a.5.5 0 0 1-.5-.5v-6a.5.5 0 0 1 .5-.5zm-3.82 15h-2.42a.67.67 0 0 1-.46-.15 1.33 1.33 0 0 1-.28-.34l-2.77-4.44a2.65 2.65 0 0 1-.28.69L5 15.51a2.22 2.22 0 0 1-.29.34.58.58 0 0 1-.42.15H2l4.15-6.19L2.17 4h2.42a.81.81 0 0 1 .41.09.8.8 0 0 1 .24.26L8 8.59a2.71 2.71 0 0 1 .33-.74L10.6 4.4a.69.69 0 0 1 .6-.4h2.32l-4 5.71z" />
+          </symbol>
         </defs>
       </svg>
     );
index e4b4b24a35a4a9e1a935dcc6fb23788d3e01461c..8b6247ca359cbe812d81108ac2199a7ebb1e3856 100644 (file)
@@ -1,7 +1,7 @@
 import { Component, linkEvent } from 'inferno';
 import { WebSocketService, UserService } from '../services';
 import { Subscription } from 'rxjs';
-import { retryWhen, delay, take, last } from 'rxjs/operators';
+import { retryWhen, delay, take } from 'rxjs/operators';
 import { i18n } from '../i18next';
 import {
   UserOperation,
@@ -12,12 +12,11 @@ import {
   UserDetailsResponse,
   UserView,
   WebSocketJsonResponse,
-  UserDetailsView,
   CommentResponse,
   BanUserResponse,
   PostResponse,
-  AddAdminResponse,
-} from '../interfaces';
+} from 'lemmy-js-client';
+import { UserDetailsView } from '../interfaces';
 import {
   wsJsonToRes,
   toast,
@@ -36,11 +35,12 @@ interface UserDetailsProps {
   user_id?: number;
   page: number;
   limit: number;
-  sort: string;
+  sort: SortType;
   enableDownvotes: boolean;
   enableNsfw: boolean;
   view: UserDetailsView;
   onPageChange(page: number): number | any;
+  admins: Array<UserView>;
 }
 
 interface UserDetailsState {
@@ -49,7 +49,6 @@ interface UserDetailsState {
   comments: Array<Comment>;
   posts: Array<Post>;
   saved?: Array<Post>;
-  admins: Array<UserView>;
 }
 
 export class UserDetails extends Component<UserDetailsProps, UserDetailsState> {
@@ -63,7 +62,6 @@ export class UserDetails extends Component<UserDetailsProps, UserDetailsState> {
       comments: [],
       posts: [],
       saved: [],
-      admins: [],
     };
 
     this.subscription = WebSocketService.Instance.subject
@@ -81,6 +79,7 @@ export class UserDetails extends Component<UserDetailsProps, UserDetailsState> {
 
   componentDidMount() {
     this.fetchUserData();
+    setupTippy();
   }
 
   componentDidUpdate(lastProps: UserDetailsProps) {
@@ -90,7 +89,6 @@ export class UserDetails extends Component<UserDetailsProps, UserDetailsState> {
         break;
       }
     }
-    setupTippy();
   }
 
   fetchUserData() {
@@ -139,7 +137,7 @@ export class UserDetails extends Component<UserDetailsProps, UserDetailsState> {
     ];
 
     // Sort it
-    if (SortType[this.props.sort] === SortType.New) {
+    if (this.props.sort === SortType.New) {
       combined.sort((a, b) => b.data.published.localeCompare(a.data.published));
     } else {
       combined.sort((a, b) => b.data.score - a.data.score);
@@ -148,25 +146,32 @@ export class UserDetails extends Component<UserDetailsProps, UserDetailsState> {
     return (
       <div>
         {combined.map(i => (
-          <div>
-            {i.type === 'posts' ? (
-              <PostListing
-                post={i.data as Post}
-                admins={this.state.admins}
-                showCommunity
-                enableDownvotes={this.props.enableDownvotes}
-                enableNsfw={this.props.enableNsfw}
-              />
-            ) : (
-              <CommentNodes
-                nodes={[{ comment: i.data as Comment }]}
-                admins={this.state.admins}
-                noIndent
-                showContext
-                enableDownvotes={this.props.enableDownvotes}
-              />
-            )}
-          </div>
+          <>
+            <div>
+              {i.type === 'posts' ? (
+                <PostListing
+                  key={(i.data as Post).id}
+                  post={i.data as Post}
+                  admins={this.props.admins}
+                  showCommunity
+                  enableDownvotes={this.props.enableDownvotes}
+                  enableNsfw={this.props.enableNsfw}
+                />
+              ) : (
+                <CommentNodes
+                  key={(i.data as Comment).id}
+                  nodes={[{ comment: i.data as Comment }]}
+                  admins={this.props.admins}
+                  noBorder
+                  noIndent
+                  showCommunity
+                  showContext
+                  enableDownvotes={this.props.enableDownvotes}
+                />
+              )}
+            </div>
+            <hr class="my-3" />
+          </>
         ))}
       </div>
     );
@@ -177,8 +182,9 @@ export class UserDetails extends Component<UserDetailsProps, UserDetailsState> {
       <div>
         <CommentNodes
           nodes={commentsToFlatNodes(this.state.comments)}
-          admins={this.state.admins}
+          admins={this.props.admins}
           noIndent
+          showCommunity
           showContext
           enableDownvotes={this.props.enableDownvotes}
         />
@@ -190,13 +196,16 @@ export class UserDetails extends Component<UserDetailsProps, UserDetailsState> {
     return (
       <div>
         {this.state.posts.map(post => (
-          <PostListing
-            post={post}
-            admins={this.state.admins}
-            showCommunity
-            enableDownvotes={this.props.enableDownvotes}
-            enableNsfw={this.props.enableNsfw}
-          />
+          <>
+            <PostListing
+              post={post}
+              admins={this.props.admins}
+              showCommunity
+              enableDownvotes={this.props.enableDownvotes}
+              enableNsfw={this.props.enableNsfw}
+            />
+            <hr class="my-3" />
+          </>
         ))}
       </div>
     );
@@ -207,7 +216,7 @@ export class UserDetails extends Component<UserDetailsProps, UserDetailsState> {
       <div class="my-2">
         {this.props.page > 1 && (
           <button
-            class="btn btn-sm btn-secondary mr-1"
+            class="btn btn-secondary mr-1"
             onClick={linkEvent(this, this.prevPage)}
           >
             {i18n.t('prev')}
@@ -215,7 +224,7 @@ export class UserDetails extends Component<UserDetailsProps, UserDetailsState> {
         )}
         {this.state.comments.length + this.state.posts.length > 0 && (
           <button
-            class="btn btn-sm btn-secondary"
+            class="btn btn-secondary"
             onClick={linkEvent(this, this.nextPage)}
           >
             {i18n.t('next')}
@@ -234,6 +243,7 @@ export class UserDetails extends Component<UserDetailsProps, UserDetailsState> {
   }
 
   parseMessage(msg: WebSocketJsonResponse) {
+    console.log(msg);
     const res = wsJsonToRes(msg);
 
     if (msg.error) {
@@ -251,7 +261,6 @@ export class UserDetails extends Component<UserDetailsProps, UserDetailsState> {
         follows: data.follows,
         moderates: data.moderates,
         posts: data.posts,
-        admins: data.admins,
       });
     } else if (res.op == UserOperation.CreateCommentLike) {
       const data = res.data as CommentResponse;
@@ -259,7 +268,11 @@ export class UserDetails extends Component<UserDetailsProps, UserDetailsState> {
       this.setState({
         comments: this.state.comments,
       });
-    } else if (res.op == UserOperation.EditComment) {
+    } else if (
+      res.op == UserOperation.EditComment ||
+      res.op == UserOperation.DeleteComment ||
+      res.op == UserOperation.RemoveComment
+    ) {
       const data = res.data as CommentResponse;
       editCommentRes(data, this.state.comments);
       this.setState({
@@ -297,11 +310,6 @@ export class UserDetails extends Component<UserDetailsProps, UserDetailsState> {
         posts: this.state.posts,
         comments: this.state.comments,
       });
-    } else if (res.op == UserOperation.AddAdmin) {
-      const data = res.data as AddAdminResponse;
-      this.setState({
-        admins: data.admins,
-      });
     }
   }
 }
index 58475d3e94127996cdc516d898745cf6b5e4ccec..fd296faddce8a2b019a830ed0c8769810404b3a9 100644 (file)
@@ -1,6 +1,6 @@
 import { Component } from 'inferno';
 import { Link } from 'inferno-router';
-import { UserView } from '../interfaces';
+import { UserView } from 'lemmy-js-client';
 import {
   pictrsAvatarThumbnail,
   showAvatars,
@@ -9,8 +9,9 @@ import {
 } from '../utils';
 import { CakeDay } from './cake-day';
 
-interface UserOther {
+export interface UserOther {
   name: string;
+  preferred_username?: string;
   id?: number; // Necessary if its federated
   avatar?: string;
   local?: boolean;
@@ -21,6 +22,9 @@ interface UserOther {
 interface UserListingProps {
   user: UserView | UserOther;
   realLink?: boolean;
+  useApubName?: boolean;
+  muted?: boolean;
+  hideAvatar?: boolean;
 }
 
 export class UserListing extends Component<UserListingProps, any> {
@@ -31,31 +35,40 @@ export class UserListing extends Component<UserListingProps, any> {
   render() {
     let user = this.props.user;
     let local = user.local == null ? true : user.local;
-    let name_: string, link: string;
+    let apubName: string, link: string;
 
     if (local) {
-      name_ = user.name;
+      apubName = `@${user.name}`;
       link = `/u/${user.name}`;
     } else {
-      name_ = `${user.name}@${hostname(user.actor_id)}`;
+      apubName = `@${user.name}@${hostname(user.actor_id)}`;
       link = !this.props.realLink ? `/user/${user.id}` : user.actor_id;
     }
 
+    let displayName = this.props.useApubName
+      ? apubName
+      : user.preferred_username
+      ? user.preferred_username
+      : apubName;
+
     return (
       <>
-        <Link className="text-body font-weight-bold" to={link}>
-          {user.avatar && showAvatars() && (
+        <Link
+          title={apubName}
+          className={this.props.muted ? 'text-muted' : 'text-info'}
+          to={link}
+        >
+          {!this.props.hideAvatar && user.avatar && showAvatars() && (
             <img
-              height="32"
-              width="32"
+              style="width: 2rem; height: 2rem;"
               src={pictrsAvatarThumbnail(user.avatar)}
               class="rounded-circle mr-2"
             />
           )}
-          <span>{name_}</span>
+          <span>{displayName}</span>
         </Link>
 
-        {isCakeDay(user.published) && <CakeDay creatorName={name_} />}
+        {isCakeDay(user.published) && <CakeDay creatorName={apubName} />}
       </>
     );
   }
index 945206c1de67202c3166516cd4541fd01d83f44d..e4b6c750b570a74c6f5dc0032a5a7f3e78b44f89 100644 (file)
@@ -1,4 +1,5 @@
 import { Component, linkEvent } from 'inferno';
+import { Helmet } from 'inferno-helmet';
 import { Link } from 'inferno-router';
 import { Subscription } from 'rxjs';
 import { retryWhen, delay, take } from 'rxjs/operators';
@@ -13,10 +14,10 @@ import {
   DeleteAccountForm,
   WebSocketJsonResponse,
   GetSiteResponse,
-  Site,
-  UserDetailsView,
   UserDetailsResponse,
-} from '../interfaces';
+  AddAdminResponse,
+} from 'lemmy-js-client';
+import { UserDetailsView } from '../interfaces';
 import { WebSocketService, UserService } from '../services';
 import {
   wsJsonToRes,
@@ -26,9 +27,12 @@ import {
   themes,
   setTheme,
   languages,
-  showAvatars,
   toast,
   setupTippy,
+  getLanguage,
+  mdToHtml,
+  elementUrl,
+  favIconUrl,
 } from '../utils';
 import { UserListing } from './user-listing';
 import { SortSelect } from './sort-select';
@@ -37,6 +41,9 @@ import { MomentTime } from './moment-time';
 import { i18n } from '../i18next';
 import moment from 'moment';
 import { UserDetails } from './user-details';
+import { MarkdownTextArea } from './markdown-textarea';
+import { ImageUploadForm } from './image-upload-form';
+import { BannerIconHeader } from './banner-icon-header';
 
 interface UserState {
   user: UserView;
@@ -48,13 +55,12 @@ interface UserState {
   sort: SortType;
   page: number;
   loading: boolean;
-  avatarLoading: boolean;
   userSettingsForm: UserSettingsForm;
   userSettingsLoading: boolean;
   deleteAccountLoading: boolean;
   deleteAccountShowConfirm: boolean;
   deleteAccountForm: DeleteAccountForm;
-  site: Site;
+  siteRes: GetSiteResponse;
 }
 
 interface UserProps {
@@ -67,7 +73,7 @@ interface UserProps {
 
 interface UrlParams {
   view?: string;
-  sort?: string;
+  sort?: SortType;
   page?: number;
 }
 
@@ -84,8 +90,6 @@ export class User extends Component<any, UserState> {
       comment_score: null,
       banned: null,
       avatar: null,
-      show_avatars: null,
-      send_notifications_to_email: null,
       actor_id: null,
       local: null,
     },
@@ -93,8 +97,7 @@ export class User extends Component<any, UserState> {
     username: null,
     follows: [],
     moderates: [],
-    loading: false,
-    avatarLoading: false,
+    loading: true,
     view: User.getViewFromProps(this.props.match.view),
     sort: User.getSortTypeFromProps(this.props.match.sort),
     page: User.getPageFromProps(this.props.match.page),
@@ -107,6 +110,8 @@ export class User extends Component<any, UserState> {
       show_avatars: null,
       send_notifications_to_email: null,
       auth: null,
+      bio: null,
+      preferred_username: null,
     },
     userSettingsLoading: null,
     deleteAccountLoading: null,
@@ -114,19 +119,30 @@ export class User extends Component<any, UserState> {
     deleteAccountForm: {
       password: null,
     },
-    site: {
-      id: undefined,
-      name: undefined,
-      creator_id: undefined,
-      published: undefined,
-      creator_name: undefined,
-      number_of_users: undefined,
-      number_of_posts: undefined,
-      number_of_comments: undefined,
-      number_of_communities: undefined,
-      enable_downvotes: undefined,
-      open_registration: undefined,
-      enable_nsfw: undefined,
+    siteRes: {
+      admins: [],
+      banned: [],
+      online: undefined,
+      site: {
+        id: undefined,
+        name: undefined,
+        creator_id: undefined,
+        published: undefined,
+        creator_name: undefined,
+        number_of_users: undefined,
+        number_of_posts: undefined,
+        number_of_comments: undefined,
+        number_of_communities: undefined,
+        enable_downvotes: undefined,
+        open_registration: undefined,
+        enable_nsfw: undefined,
+        icon: undefined,
+        banner: undefined,
+        creator_preferred_username: undefined,
+      },
+      version: undefined,
+      my_user: undefined,
+      federated_instances: undefined,
     },
   };
 
@@ -142,6 +158,15 @@ export class User extends Component<any, UserState> {
       this
     );
     this.handlePageChange = this.handlePageChange.bind(this);
+    this.handleUserSettingsBioChange = this.handleUserSettingsBioChange.bind(
+      this
+    );
+
+    this.handleAvatarUpload = this.handleAvatarUpload.bind(this);
+    this.handleAvatarRemove = this.handleAvatarRemove.bind(this);
+
+    this.handleBannerUpload = this.handleBannerUpload.bind(this);
+    this.handleBannerRemove = this.handleBannerRemove.bind(this);
 
     this.state.user_id = Number(this.props.match.params.id) || null;
     this.state.username = this.props.match.params.username;
@@ -155,6 +180,7 @@ export class User extends Component<any, UserState> {
       );
 
     WebSocketService.Instance.getSite();
+    setupTippy();
   }
 
   get isCurrentUser() {
@@ -164,17 +190,15 @@ export class User extends Component<any, UserState> {
     );
   }
 
-  static getViewFromProps(view: any): UserDetailsView {
-    return view
-      ? UserDetailsView[capitalizeFirstLetter(view)]
-      : UserDetailsView.Overview;
+  static getViewFromProps(view: string): UserDetailsView {
+    return view ? UserDetailsView[view] : UserDetailsView.Overview;
   }
 
-  static getSortTypeFromProps(sort: any): SortType {
+  static getSortTypeFromProps(sort: string): SortType {
     return sort ? routeSortTypeToEnum(sort) : SortType.New;
   }
 
-  static getPageFromProps(page: any): number {
+  static getPageFromProps(page: number): number {
     return page ? Number(page) : 1;
   }
 
@@ -201,63 +225,79 @@ export class User extends Component<any, UserState> {
       // Couldnt get a refresh working. This does for now.
       location.reload();
     }
-    document.title = `/u/${this.state.username} - ${this.state.site.name}`;
-    setupTippy();
+  }
+
+  get documentTitle(): string {
+    if (this.state.siteRes.site.name) {
+      return `@${this.state.username} - ${this.state.siteRes.site.name}`;
+    } else {
+      return 'Lemmy';
+    }
+  }
+
+  get favIcon(): string {
+    return this.state.siteRes.site.icon
+      ? this.state.siteRes.site.icon
+      : favIconUrl;
   }
 
   render() {
     return (
       <div class="container">
-        {this.state.loading ? (
-          <h5>
-            <svg class="icon icon-spinner spin">
-              <use xlinkHref="#icon-spinner"></use>
-            </svg>
-          </h5>
-        ) : (
-          <div class="row">
-            <div class="col-12 col-md-8">
+        <Helmet title={this.documentTitle}>
+          <link
+            id="favicon"
+            rel="icon"
+            type="image/x-icon"
+            href={this.favIcon}
+          />
+        </Helmet>
+        <div class="row">
+          <div class="col-12 col-md-8">
+            {this.state.loading ? (
               <h5>
-                {this.state.user.avatar && showAvatars() && (
-                  <img
-                    height="80"
-                    width="80"
-                    src={this.state.user.avatar}
-                    class="rounded-circle mr-2"
-                  />
-                )}
-                <span>/u/{this.state.username}</span>
+                <svg class="icon icon-spinner spin">
+                  <use xlinkHref="#icon-spinner"></use>
+                </svg>
               </h5>
-              {this.selects()}
-              <UserDetails
-                user_id={this.state.user_id}
-                username={this.state.username}
-                sort={SortType[this.state.sort]}
-                page={this.state.page}
-                limit={fetchLimit}
-                enableDownvotes={this.state.site.enable_downvotes}
-                enableNsfw={this.state.site.enable_nsfw}
-                view={this.state.view}
-                onPageChange={this.handlePageChange}
-              />
-            </div>
+            ) : (
+              <>
+                {this.userInfo()}
+                <hr />
+              </>
+            )}
+            {!this.state.loading && this.selects()}
+            <UserDetails
+              user_id={this.state.user_id}
+              username={this.state.username}
+              sort={this.state.sort}
+              page={this.state.page}
+              limit={fetchLimit}
+              enableDownvotes={this.state.siteRes.site.enable_downvotes}
+              enableNsfw={this.state.siteRes.site.enable_nsfw}
+              admins={this.state.siteRes.admins}
+              view={this.state.view}
+              onPageChange={this.handlePageChange}
+            />
+          </div>
+
+          {!this.state.loading && (
             <div class="col-12 col-md-4">
-              {this.userInfo()}
               {this.isCurrentUser && this.userSettings()}
               {this.moderates()}
               {this.follows()}
             </div>
-          </div>
-        )}
+          )}
+        </div>
       </div>
     );
   }
 
   viewRadios() {
     return (
-      <div class="btn-group btn-group-toggle">
+      <div class="btn-group btn-group-toggle flex-wrap mb-2">
         <label
-          className={`btn btn-sm btn-secondary pointer btn-outline-light
+          className={`btn btn-outline-secondary pointer
             ${this.state.view == UserDetailsView.Overview && 'active'}
           `}
         >
@@ -270,7 +310,7 @@ export class User extends Component<any, UserState> {
           {i18n.t('overview')}
         </label>
         <label
-          className={`btn btn-sm btn-secondary pointer btn-outline-light
+          className={`btn btn-outline-secondary pointer
             ${this.state.view == UserDetailsView.Comments && 'active'}
           `}
         >
@@ -283,7 +323,7 @@ export class User extends Component<any, UserState> {
           {i18n.t('comments')}
         </label>
         <label
-          className={`btn btn-sm btn-secondary pointer btn-outline-light
+          className={`btn btn-outline-secondary pointer
             ${this.state.view == UserDetailsView.Posts && 'active'}
           `}
         >
@@ -296,7 +336,7 @@ export class User extends Component<any, UserState> {
           {i18n.t('posts')}
         </label>
         <label
-          className={`btn btn-sm btn-secondary pointer btn-outline-light
+          className={`btn btn-outline-secondary pointer
             ${this.state.view == UserDetailsView.Saved && 'active'}
           `}
         >
@@ -322,9 +362,7 @@ export class User extends Component<any, UserState> {
           hideHot
         />
         <a
-          href={`/feeds/u/${this.state.username}.xml?sort=${
-            SortType[this.state.sort]
-          }`}
+          href={`/feeds/u/${this.state.username}.xml?sort=${this.state.sort}`}
           target="_blank"
           rel="noopener"
           title="RSS"
@@ -339,23 +377,90 @@ export class User extends Component<any, UserState> {
 
   userInfo() {
     let user = this.state.user;
+
     return (
       <div>
-        <div class="card border-secondary mb-3">
-          <div class="card-body">
-            <h5>
-              <ul class="list-inline mb-0">
-                <li className="list-inline-item">
-                  <UserListing user={user} realLink />
-                </li>
-                {user.banned && (
-                  <li className="list-inline-item badge badge-danger">
-                    {i18n.t('banned')}
-                  </li>
+        <BannerIconHeader
+          banner={this.state.user.banner}
+          icon={this.state.user.avatar}
+        />
+        <div class="mb-3">
+          <div class="">
+            <div class="mb-0 d-flex flex-wrap">
+              <div>
+                {user.preferred_username && (
+                  <h5 class="mb-0">{user.preferred_username}</h5>
                 )}
+                <ul class="list-inline mb-2">
+                  <li className="list-inline-item">
+                    <UserListing
+                      user={user}
+                      realLink
+                      useApubName
+                      muted
+                      hideAvatar
+                    />
+                  </li>
+                  {user.banned && (
+                    <li className="list-inline-item badge badge-danger">
+                      {i18n.t('banned')}
+                    </li>
+                  )}
+                </ul>
+              </div>
+              <div className="flex-grow-1 unselectable pointer mx-2"></div>
+              {this.isCurrentUser ? (
+                <button
+                  class="d-flex align-self-start btn btn-secondary ml-2"
+                  onClick={linkEvent(this, this.handleLogoutClick)}
+                >
+                  {i18n.t('logout')}
+                </button>
+              ) : (
+                <>
+                  <a
+                    className={`d-flex align-self-start btn btn-secondary ml-2 ${
+                      !this.state.user.matrix_user_id && 'invisible'
+                    }`}
+                    target="_blank"
+                    rel="noopener"
+                    href={`https://matrix.to/#/${this.state.user.matrix_user_id}`}
+                  >
+                    {i18n.t('send_secure_message')}
+                  </a>
+                  <Link
+                    class="d-flex align-self-start btn btn-secondary ml-2"
+                    to={`/create_private_message?recipient_id=${this.state.user.id}`}
+                  >
+                    {i18n.t('send_message')}
+                  </Link>
+                </>
+              )}
+            </div>
+            {user.bio && (
+              <div className="d-flex align-items-center mb-2">
+                <div
+                  className="md-div"
+                  dangerouslySetInnerHTML={mdToHtml(user.bio)}
+                />
+              </div>
+            )}
+            <div>
+              <ul class="list-inline mb-2">
+                <li className="list-inline-item badge badge-light">
+                  {i18n.t('number_of_posts', { count: user.number_of_posts })}
+                </li>
+                <li className="list-inline-item badge badge-light">
+                  {i18n.t('number_of_comments', {
+                    count: user.number_of_comments,
+                  })}
+                </li>
               </ul>
-            </h5>
-            <div className="d-flex align-items-center mb-2">
+            </div>
+            <div class="text-muted">
+              {i18n.t('joined')} <MomentTime data={user} showAgo />
+            </div>
+            <div className="d-flex align-items-center text-muted mb-2">
               <svg class="icon">
                 <use xlinkHref="#icon-cake"></use>
               </svg>
@@ -364,71 +469,6 @@ export class User extends Component<any, UserState> {
                 {moment.utc(user.published).local().format('MMM DD, YYYY')}
               </span>
             </div>
-            <div>
-              {i18n.t('joined')} <MomentTime data={user} showAgo />
-            </div>
-            <div class="table-responsive mt-1">
-              <table class="table table-bordered table-sm mt-2 mb-0">
-                {/*
-                <tr>
-                  <td class="text-center" colSpan={2}>
-                    {i18n.t('number_of_points', {
-                      count: user.post_score + user.comment_score,
-                    })}
-                  </td>
-                </tr>
-                */}
-                <tr>
-                  {/* 
-                  <td>
-                    {i18n.t('number_of_points', { count: user.post_score })}
-                  </td>
-                  */}
-                  <td>
-                    {i18n.t('number_of_posts', { count: user.number_of_posts })}
-                  </td>
-                  {/* 
-                </tr>
-                <tr>
-                  <td>
-                    {i18n.t('number_of_points', { count: user.comment_score })}
-                  </td>
-                  */}
-                  <td>
-                    {i18n.t('number_of_comments', {
-                      count: user.number_of_comments,
-                    })}
-                  </td>
-                </tr>
-              </table>
-            </div>
-            {this.isCurrentUser ? (
-              <button
-                class="btn btn-block btn-secondary mt-3"
-                onClick={linkEvent(this, this.handleLogoutClick)}
-              >
-                {i18n.t('logout')}
-              </button>
-            ) : (
-              <>
-                <a
-                  className={`btn btn-block btn-secondary mt-3 ${
-                    !this.state.user.matrix_user_id && 'disabled'
-                  }`}
-                  target="_blank"
-                  rel="noopener"
-                  href={`https://matrix.to/#/${this.state.user.matrix_user_id}`}
-                >
-                  {i18n.t('send_secure_message')}
-                </a>
-                <Link
-                  class="btn btn-block btn-secondary mt-3"
-                  to={`/create_private_message?recipient_id=${this.state.user.id}`}
-                >
-                  {i18n.t('send_message')}
-                </Link>
-              </>
-            )}
           </div>
         </div>
       </div>
@@ -438,59 +478,35 @@ export class User extends Component<any, UserState> {
   userSettings() {
     return (
       <div>
-        <div class="card border-secondary mb-3">
+        <div class="card bg-transparent border-secondary mb-3">
           <div class="card-body">
             <h5>{i18n.t('settings')}</h5>
             <form onSubmit={linkEvent(this, this.handleUserSettingsSubmit)}>
               <div class="form-group">
                 <label>{i18n.t('avatar')}</label>
-                <form class="d-inline">
-                  <label
-                    htmlFor="file-upload"
-                    class="pointer ml-4 text-muted small font-weight-bold"
-                  >
-                    {!this.checkSettingsAvatar ? (
-                      <span class="btn btn-sm btn-secondary">
-                        {i18n.t('upload_avatar')}
-                      </span>
-                    ) : (
-                      <img
-                        height="80"
-                        width="80"
-                        src={this.state.userSettingsForm.avatar}
-                        class="rounded-circle"
-                      />
-                    )}
-                  </label>
-                  <input
-                    id="file-upload"
-                    type="file"
-                    accept="image/*,video/*"
-                    name="file"
-                    class="d-none"
-                    disabled={!UserService.Instance.user}
-                    onChange={linkEvent(this, this.handleImageUpload)}
-                  />
-                </form>
+                <ImageUploadForm
+                  uploadTitle={i18n.t('upload_avatar')}
+                  imageSrc={this.state.userSettingsForm.avatar}
+                  onUpload={this.handleAvatarUpload}
+                  onRemove={this.handleAvatarRemove}
+                  rounded
+                />
+              </div>
+              <div class="form-group">
+                <label>{i18n.t('banner')}</label>
+                <ImageUploadForm
+                  uploadTitle={i18n.t('upload_banner')}
+                  imageSrc={this.state.userSettingsForm.banner}
+                  onUpload={this.handleBannerUpload}
+                  onRemove={this.handleBannerRemove}
+                />
               </div>
-              {this.checkSettingsAvatar && (
-                <div class="form-group">
-                  <button
-                    class="btn btn-secondary btn-block"
-                    onClick={linkEvent(this, this.removeAvatar)}
-                  >
-                    {`${capitalizeFirstLetter(i18n.t('remove'))} ${i18n.t(
-                      'avatar'
-                    )}`}
-                  </button>
-                </div>
-              )}
               <div class="form-group">
                 <label>{i18n.t('language')}</label>
                 <select
                   value={this.state.userSettingsForm.lang}
                   onChange={linkEvent(this, this.handleUserSettingsLangChange)}
-                  class="ml-2 custom-select custom-select-sm w-auto"
+                  class="ml-2 custom-select w-auto"
                 >
                   <option disabled>{i18n.t('language')}</option>
                   <option value="browser">{i18n.t('browser_default')}</option>
@@ -505,7 +521,7 @@ export class User extends Component<any, UserState> {
                 <select
                   value={this.state.userSettingsForm.theme}
                   onChange={linkEvent(this, this.handleUserSettingsThemeChange)}
-                  class="ml-2 custom-select custom-select-sm w-auto"
+                  class="ml-2 custom-select w-auto"
                 >
                   <option disabled>{i18n.t('theme')}</option>
                   {themes.map(theme => (
@@ -518,7 +534,11 @@ export class User extends Component<any, UserState> {
                   <div class="mr-2">{i18n.t('sort_type')}</div>
                 </label>
                 <ListingTypeSelect
-                  type_={this.state.userSettingsForm.default_listing_type}
+                  type_={
+                    Object.values(ListingType)[
+                      this.state.userSettingsForm.default_listing_type
+                    ]
+                  }
                   onChange={this.handleUserSettingsListingTypeChange}
                 />
               </form>
@@ -527,10 +547,47 @@ export class User extends Component<any, UserState> {
                   <div class="mr-2">{i18n.t('type')}</div>
                 </label>
                 <SortSelect
-                  sort={this.state.userSettingsForm.default_sort_type}
+                  sort={
+                    Object.values(SortType)[
+                      this.state.userSettingsForm.default_sort_type
+                    ]
+                  }
                   onChange={this.handleUserSettingsSortTypeChange}
                 />
               </form>
+              <div class="form-group row">
+                <label class="col-lg-5 col-form-label">
+                  {i18n.t('display_name')}
+                </label>
+                <div class="col-lg-7">
+                  <input
+                    type="text"
+                    class="form-control"
+                    placeholder={i18n.t('optional')}
+                    value={this.state.userSettingsForm.preferred_username}
+                    onInput={linkEvent(
+                      this,
+                      this.handleUserSettingsPreferredUsernameChange
+                    )}
+                    pattern="^(?!@)(.+)$"
+                    minLength={3}
+                    maxLength={20}
+                  />
+                </div>
+              </div>
+              <div class="form-group row">
+                <label class="col-lg-3 col-form-label" htmlFor="user-bio">
+                  {i18n.t('bio')}
+                </label>
+                <div class="col-lg-9">
+                  <MarkdownTextArea
+                    initialContent={this.state.userSettingsForm.bio}
+                    onContentChange={this.handleUserSettingsBioChange}
+                    maxLength={300}
+                    hideNavigationWarnings
+                  />
+                </div>
+              </div>
               <div class="form-group row">
                 <label class="col-lg-3 col-form-label" htmlFor="user-email">
                   {i18n.t('email')}
@@ -552,11 +609,7 @@ export class User extends Component<any, UserState> {
               </div>
               <div class="form-group row">
                 <label class="col-lg-5 col-form-label">
-                  <a
-                    href="https://about.riot.im/"
-                    target="_blank"
-                    rel="noopener"
-                  >
+                  <a href={elementUrl} target="_blank" rel="noopener">
                     {i18n.t('matrix_user_id')}
                   </a>
                 </label>
@@ -634,7 +687,7 @@ export class User extends Component<any, UserState> {
                   />
                 </div>
               </div>
-              {this.state.site.enable_nsfw && (
+              {this.state.siteRes.site.enable_nsfw && (
                 <div class="form-group">
                   <div class="form-check">
                     <input
@@ -676,7 +729,7 @@ export class User extends Component<any, UserState> {
                     class="form-check-input"
                     id="user-send-notifications-to-email"
                     type="checkbox"
-                    disabled={!this.state.user.email}
+                    disabled={!this.state.userSettingsForm.email}
                     checked={
                       this.state.userSettingsForm.send_notifications_to_email
                     }
@@ -766,7 +819,7 @@ export class User extends Component<any, UserState> {
     return (
       <div>
         {this.state.moderates.length > 0 && (
-          <div class="card border-secondary mb-3">
+          <div class="card bg-transparent border-secondary mb-3">
             <div class="card-body">
               <h5>{i18n.t('moderates')}</h5>
               <ul class="list-unstyled mb-0">
@@ -789,7 +842,7 @@ export class User extends Component<any, UserState> {
     return (
       <div>
         {this.state.follows.length > 0 && (
-          <div class="card border-secondary mb-3">
+          <div class="card bg-transparent border-secondary mb-3">
             <div class="card-body">
               <h5>{i18n.t('subscribed')}</h5>
               <ul class="list-unstyled mb-0">
@@ -810,10 +863,8 @@ export class User extends Component<any, UserState> {
 
   updateUrl(paramUpdates: UrlParams) {
     const page = paramUpdates.page || this.state.page;
-    const viewStr =
-      paramUpdates.view || UserDetailsView[this.state.view].toLowerCase();
-    const sortStr =
-      paramUpdates.sort || SortType[this.state.sort].toLowerCase();
+    const viewStr = paramUpdates.view || UserDetailsView[this.state.view];
+    const sortStr = paramUpdates.sort || this.state.sort;
     this.props.history.push(
       `/u/${this.state.username}/view/${viewStr}/sort/${sortStr}/page/${page}`
     );
@@ -824,12 +875,12 @@ export class User extends Component<any, UserState> {
   }
 
   handleSortChange(val: SortType) {
-    this.updateUrl({ sort: SortType[val].toLowerCase(), page: 1 });
+    this.updateUrl({ sort: val, page: 1 });
   }
 
   handleViewChange(i: User, event: any) {
     i.updateUrl({
-      view: UserDetailsView[Number(event.target.value)].toLowerCase(),
+      view: UserDetailsView[Number(event.target.value)],
       page: 1,
     });
   }
@@ -858,25 +909,56 @@ export class User extends Component<any, UserState> {
 
   handleUserSettingsLangChange(i: User, event: any) {
     i.state.userSettingsForm.lang = event.target.value;
-    i18n.changeLanguage(i.state.userSettingsForm.lang);
+    i18n.changeLanguage(getLanguage(i.state.userSettingsForm.lang));
     i.setState(i.state);
   }
 
   handleUserSettingsSortTypeChange(val: SortType) {
-    this.state.userSettingsForm.default_sort_type = val;
+    this.state.userSettingsForm.default_sort_type = Object.keys(
+      SortType
+    ).indexOf(val);
     this.setState(this.state);
   }
 
   handleUserSettingsListingTypeChange(val: ListingType) {
-    this.state.userSettingsForm.default_listing_type = val;
+    this.state.userSettingsForm.default_listing_type = Object.keys(
+      ListingType
+    ).indexOf(val);
     this.setState(this.state);
   }
 
   handleUserSettingsEmailChange(i: User, event: any) {
     i.state.userSettingsForm.email = event.target.value;
-    if (i.state.userSettingsForm.email == '' && !i.state.user.email) {
-      i.state.userSettingsForm.email = undefined;
-    }
+    i.setState(i.state);
+  }
+
+  handleUserSettingsBioChange(val: string) {
+    this.state.userSettingsForm.bio = val;
+    this.setState(this.state);
+  }
+
+  handleAvatarUpload(url: string) {
+    this.state.userSettingsForm.avatar = url;
+    this.setState(this.state);
+  }
+
+  handleAvatarRemove() {
+    this.state.userSettingsForm.avatar = '';
+    this.setState(this.state);
+  }
+
+  handleBannerUpload(url: string) {
+    this.state.userSettingsForm.banner = url;
+    this.setState(this.state);
+  }
+
+  handleBannerRemove() {
+    this.state.userSettingsForm.banner = '';
+    this.setState(this.state);
+  }
+
+  handleUserSettingsPreferredUsernameChange(i: User, event: any) {
+    i.state.userSettingsForm.preferred_username = event.target.value;
     i.setState(i.state);
   }
 
@@ -915,59 +997,6 @@ export class User extends Component<any, UserState> {
     i.setState(i.state);
   }
 
-  handleImageUpload(i: User, event: any) {
-    event.preventDefault();
-    let file = event.target.files[0];
-    const imageUploadUrl = `/pictrs/image`;
-    const formData = new FormData();
-    formData.append('images[]', file);
-
-    i.state.avatarLoading = true;
-    i.setState(i.state);
-
-    fetch(imageUploadUrl, {
-      method: 'POST',
-      body: formData,
-    })
-      .then(res => res.json())
-      .then(res => {
-        console.log('pictrs upload:');
-        console.log(res);
-        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.avatarLoading = false;
-          i.setState(i.state);
-        } else {
-          i.state.avatarLoading = false;
-          i.setState(i.state);
-          toast(JSON.stringify(res), 'danger');
-        }
-      })
-      .catch(error => {
-        i.state.avatarLoading = false;
-        i.setState(i.state);
-        toast(error, 'danger');
-      });
-  }
-
-  removeAvatar(i: User, event: any) {
-    event.preventDefault();
-    i.state.userSettingsLoading = true;
-    i.state.userSettingsForm.avatar = '';
-    i.setState(i.state);
-
-    WebSocketService.Instance.saveUserSettings(i.state.userSettingsForm);
-  }
-
-  get checkSettingsAvatar(): boolean {
-    return (
-      this.state.userSettingsForm.avatar &&
-      this.state.userSettingsForm.avatar != ''
-    );
-  }
-
   handleUserSettingsSubmit(i: User, event: any) {
     event.preventDefault();
     i.state.userSettingsLoading = true;
@@ -1001,6 +1030,7 @@ export class User extends Component<any, UserState> {
   }
 
   parseMessage(msg: WebSocketJsonResponse) {
+    console.log(msg);
     const res = wsJsonToRes(msg);
     if (msg.error) {
       toast(i18n.t(msg.error), 'danger');
@@ -1009,7 +1039,6 @@ export class User extends Component<any, UserState> {
       }
       this.setState({
         deleteAccountLoading: false,
-        avatarLoading: false,
         userSettingsLoading: false,
       });
       return;
@@ -1035,20 +1064,31 @@ export class User extends Component<any, UserState> {
             UserService.Instance.user.default_listing_type;
           this.state.userSettingsForm.lang = UserService.Instance.user.lang;
           this.state.userSettingsForm.avatar = UserService.Instance.user.avatar;
-          this.state.userSettingsForm.email = this.state.user.email;
-          this.state.userSettingsForm.send_notifications_to_email = this.state.user.send_notifications_to_email;
+          this.state.userSettingsForm.banner = UserService.Instance.user.banner;
+          this.state.userSettingsForm.preferred_username =
+            UserService.Instance.user.preferred_username;
           this.state.userSettingsForm.show_avatars =
             UserService.Instance.user.show_avatars;
-          this.state.userSettingsForm.matrix_user_id = this.state.user.matrix_user_id;
+          this.state.userSettingsForm.email = UserService.Instance.user.email;
+          this.state.userSettingsForm.bio = UserService.Instance.user.bio;
+          this.state.userSettingsForm.send_notifications_to_email =
+            UserService.Instance.user.send_notifications_to_email;
+          this.state.userSettingsForm.matrix_user_id =
+            UserService.Instance.user.matrix_user_id;
         }
+        this.state.loading = false;
         this.setState(this.state);
       }
     } else if (res.op == UserOperation.SaveUserSettings) {
       const data = res.data as LoginResponse;
       UserService.Instance.login(data);
-      this.setState({
-        userSettingsLoading: false,
-      });
+      this.state.user.bio = this.state.userSettingsForm.bio;
+      this.state.user.preferred_username = this.state.userSettingsForm.preferred_username;
+      this.state.user.banner = this.state.userSettingsForm.banner;
+      this.state.user.avatar = this.state.userSettingsForm.avatar;
+      this.state.userSettingsLoading = false;
+      this.setState(this.state);
+
       window.scrollTo(0, 0);
     } else if (res.op == UserOperation.DeleteAccount) {
       this.setState({
@@ -1058,9 +1098,12 @@ export class User extends Component<any, UserState> {
       this.context.router.history.push('/');
     } else if (res.op == UserOperation.GetSite) {
       const data = res.data as GetSiteResponse;
-      this.setState({
-        site: data.site,
-      });
+      this.state.siteRes = data;
+      this.setState(this.state);
+    } else if (res.op == UserOperation.AddAdmin) {
+      const data = res.data as AddAdminResponse;
+      this.state.siteRes.admins = data.admins;
+      this.setState(this.state);
     }
   }
 }
index 3657da33b10c044ed9d749bad335022ec3c6d04b..39f7d65ccc08ef481847287aae7a522bb910651e 100644 (file)
@@ -28,6 +28,7 @@ import { sq } from './translations/sq';
 import { km } from './translations/km';
 import { ga } from './translations/ga';
 import { sr_Latn } from './translations/sr_Latn';
+import { ko } from './translations/ko';
 
 // https://github.com/nimbusec-oss/inferno-i18next/blob/master/tests/T.test.js#L66
 const resources = {
@@ -59,6 +60,7 @@ const resources = {
   km,
   ga,
   sr_Latn,
+  ko,
 };
 
 function format(value: any, format: any, lng: any): any {
index 8e49df9fbb09c5d88b99834a59a4527a4f699a33..aa373de4a302bf4dc847677e32339d0307d05ddd 100644 (file)
@@ -19,6 +19,7 @@ import { AdminSettings } from './components/admin-settings';
 import { Inbox } from './components/inbox';
 import { Search } from './components/search';
 import { Sponsors } from './components/sponsors';
+import { Instances } from './components/instances';
 import { Symbols } from './components/symbols';
 import { i18n } from './i18next';
 
@@ -89,6 +90,7 @@ class Index extends Component<any, any> {
                   path={`/password_change/:token`}
                   component={PasswordChange}
                 />
+                <Route path={`/instances`} component={Instances} />
               </Switch>
               <Symbols />
             </div>
index dc860e0684acb03b3a2b9635a272772ee6eb83ab..3b081183d7f16a4425bce3b9944206492bab3e3d 100644 (file)
@@ -1,52 +1,3 @@
-export enum UserOperation {
-  Login,
-  Register,
-  CreateCommunity,
-  CreatePost,
-  ListCommunities,
-  ListCategories,
-  GetPost,
-  GetCommunity,
-  CreateComment,
-  EditComment,
-  SaveComment,
-  CreateCommentLike,
-  GetPosts,
-  CreatePostLike,
-  EditPost,
-  SavePost,
-  EditCommunity,
-  FollowCommunity,
-  GetFollowedCommunities,
-  GetUserDetails,
-  GetReplies,
-  GetUserMentions,
-  EditUserMention,
-  GetModlog,
-  BanFromCommunity,
-  AddModToCommunity,
-  CreateSite,
-  EditSite,
-  GetSite,
-  AddAdmin,
-  BanUser,
-  Search,
-  MarkAllAsRead,
-  SaveUserSettings,
-  TransferCommunity,
-  TransferSite,
-  DeleteAccount,
-  PasswordReset,
-  PasswordChange,
-  CreatePrivateMessage,
-  EditPrivateMessage,
-  GetPrivateMessages,
-  UserJoin,
-  GetComments,
-  GetSiteConfig,
-  SaveSiteConfig,
-}
-
 export enum CommentSortType {
   Hot,
   Top,
@@ -59,885 +10,16 @@ export enum CommentViewType {
   Chat,
 }
 
-export enum ListingType {
-  All,
-  Subscribed,
-  Community,
-}
-
 export enum DataType {
   Post,
   Comment,
 }
 
-export enum SortType {
-  Hot,
-  New,
-  TopDay,
-  TopWeek,
-  TopMonth,
-  TopYear,
-  TopAll,
-}
-
-export enum SearchType {
-  All,
-  Comments,
-  Posts,
-  Communities,
-  Users,
-  Url,
-}
-
-export interface User {
-  id: number;
-  iss: string;
-  username: string;
-  show_nsfw: boolean;
-  theme: string;
-  default_sort_type: SortType;
-  default_listing_type: ListingType;
-  lang: string;
-  avatar?: string;
-  show_avatars: boolean;
-  unreadCount?: number;
-}
-
-export interface UserView {
-  id: number;
-  actor_id: string;
-  name: string;
-  avatar?: string;
-  email?: string;
-  matrix_user_id?: string;
-  bio?: string;
-  local: boolean;
-  published: string;
-  number_of_posts: number;
-  post_score: number;
-  number_of_comments: number;
-  comment_score: number;
-  banned: boolean;
-  show_avatars: boolean;
-  send_notifications_to_email: boolean;
-}
-
-export interface CommunityUser {
-  id: number;
-  user_id: number;
-  user_actor_id: string;
-  user_local: boolean;
-  user_name: string;
-  avatar?: string;
-  community_id: number;
-  community_actor_id: string;
-  community_local: boolean;
-  community_name: string;
-  published: string;
-}
-
-export interface Community {
-  id: number;
-  actor_id: string;
-  local: boolean;
-  name: string;
-  title: string;
-  description?: string;
-  category_id: number;
-  creator_id: number;
-  removed: boolean;
-  deleted: boolean;
-  nsfw: boolean;
-  published: string;
-  updated?: string;
-  creator_actor_id: string;
-  creator_local: boolean;
-  last_refreshed_at: string;
-  creator_name: string;
-  creator_avatar?: string;
-  category_name: string;
-  number_of_subscribers: number;
-  number_of_posts: number;
-  number_of_comments: number;
-  user_id?: number;
-  subscribed?: boolean;
-}
-
-export interface Post {
-  id: number;
-  name: string;
-  url?: string;
-  body?: string;
-  creator_id: number;
-  community_id: number;
-  removed: boolean;
-  deleted: boolean;
-  locked: boolean;
-  stickied: boolean;
-  embed_title?: string;
-  embed_description?: string;
-  embed_html?: string;
-  thumbnail_url?: string;
-  ap_id: string;
-  local: boolean;
-  nsfw: boolean;
-  banned: boolean;
-  banned_from_community: boolean;
-  published: string;
-  updated?: string;
-  creator_actor_id: string;
-  creator_local: boolean;
-  creator_name: string;
-  creator_published: string;
-  creator_avatar?: string;
-  community_actor_id: string;
-  community_local: boolean;
-  community_name: string;
-  community_removed: boolean;
-  community_deleted: boolean;
-  community_nsfw: boolean;
-  number_of_comments: number;
-  score: number;
-  upvotes: number;
-  downvotes: number;
-  hot_rank: number;
-  newest_activity_time: string;
-  user_id?: number;
-  my_vote?: number;
-  subscribed?: boolean;
-  read?: boolean;
-  saved?: boolean;
-  duplicates?: Array<Post>;
-}
-
-export interface Comment {
-  id: number;
-  ap_id: string;
-  local: boolean;
-  creator_id: number;
-  post_id: number;
-  post_name: string;
-  parent_id?: number;
-  content: string;
-  removed: boolean;
-  deleted: boolean;
-  read: boolean;
-  published: string;
-  updated?: string;
-  community_id: number;
-  community_actor_id: string;
-  community_local: boolean;
-  community_name: string;
-  banned: boolean;
-  banned_from_community: boolean;
-  creator_actor_id: string;
-  creator_local: boolean;
-  creator_name: string;
-  creator_avatar?: string;
-  creator_published: string;
-  score: number;
-  upvotes: number;
-  downvotes: number;
-  hot_rank: number;
-  user_id?: number;
-  my_vote?: number;
-  subscribed?: number;
-  saved?: boolean;
-  user_mention_id?: number; // For mention type
-  recipient_id?: number;
-  recipient_actor_id?: string;
-  recipient_local?: boolean;
-  depth?: number;
-}
-
-export interface Category {
-  id: number;
-  name: string;
-}
-
-export interface Site {
-  id: number;
-  name: string;
-  description?: string;
-  creator_id: number;
-  published: string;
-  updated?: string;
-  creator_name: string;
-  number_of_users: number;
-  number_of_posts: number;
-  number_of_comments: number;
-  number_of_communities: number;
-  enable_downvotes: boolean;
-  open_registration: boolean;
-  enable_nsfw: boolean;
-}
-
-export interface PrivateMessage {
-  id: number;
-  creator_id: number;
-  recipient_id: number;
-  content: string;
-  deleted: boolean;
-  read: boolean;
-  published: string;
-  updated?: string;
-  ap_id: string;
-  local: boolean;
-  creator_name: string;
-  creator_avatar?: string;
-  creator_actor_id: string;
-  creator_local: boolean;
-  recipient_name: string;
-  recipient_avatar?: string;
-  recipient_actor_id: string;
-  recipient_local: boolean;
-}
-
 export enum BanType {
   Community,
   Site,
 }
 
-export interface FollowCommunityForm {
-  community_id: number;
-  follow: boolean;
-  auth?: string;
-}
-
-export interface GetFollowedCommunitiesForm {
-  auth: string;
-}
-
-export interface GetFollowedCommunitiesResponse {
-  communities: Array<CommunityUser>;
-}
-
-export interface GetUserDetailsForm {
-  user_id?: number;
-  username?: string;
-  sort: string;
-  page?: number;
-  limit?: number;
-  community_id?: number;
-  saved_only: boolean;
-}
-
-export interface UserDetailsResponse {
-  user: UserView;
-  follows: Array<CommunityUser>;
-  moderates: Array<CommunityUser>;
-  comments: Array<Comment>;
-  posts: Array<Post>;
-  admins: Array<UserView>;
-}
-
-export interface GetRepliesForm {
-  sort: string;
-  page?: number;
-  limit?: number;
-  unread_only: boolean;
-  auth?: string;
-}
-
-export interface GetRepliesResponse {
-  replies: Array<Comment>;
-}
-
-export interface GetUserMentionsForm {
-  sort: string;
-  page?: number;
-  limit?: number;
-  unread_only: boolean;
-  auth?: string;
-}
-
-export interface GetUserMentionsResponse {
-  mentions: Array<Comment>;
-}
-
-export interface EditUserMentionForm {
-  user_mention_id: number;
-  read?: boolean;
-  auth?: string;
-}
-
-export interface UserMentionResponse {
-  mention: Comment;
-}
-
-export interface BanFromCommunityForm {
-  community_id: number;
-  user_id: number;
-  ban: boolean;
-  reason?: string;
-  expires?: number;
-  auth?: string;
-}
-
-export interface BanFromCommunityResponse {
-  user: UserView;
-  banned: boolean;
-}
-
-export interface AddModToCommunityForm {
-  community_id: number;
-  user_id: number;
-  added: boolean;
-  auth?: string;
-}
-
-export interface TransferCommunityForm {
-  community_id: number;
-  user_id: number;
-  auth?: string;
-}
-
-export interface TransferSiteForm {
-  user_id: number;
-  auth?: string;
-}
-
-export interface AddModToCommunityResponse {
-  moderators: Array<CommunityUser>;
-}
-
-export interface GetModlogForm {
-  mod_user_id?: number;
-  community_id?: number;
-  page?: number;
-  limit?: number;
-}
-
-export interface GetModlogResponse {
-  removed_posts: Array<ModRemovePost>;
-  locked_posts: Array<ModLockPost>;
-  stickied_posts: Array<ModStickyPost>;
-  removed_comments: Array<ModRemoveComment>;
-  removed_communities: Array<ModRemoveCommunity>;
-  banned_from_community: Array<ModBanFromCommunity>;
-  banned: Array<ModBan>;
-  added_to_community: Array<ModAddCommunity>;
-  added: Array<ModAdd>;
-}
-
-export interface ModRemovePost {
-  id: number;
-  mod_user_id: number;
-  post_id: number;
-  reason?: string;
-  removed?: boolean;
-  when_: string;
-  mod_user_name: string;
-  post_name: string;
-  community_id: number;
-  community_name: string;
-}
-
-export interface ModLockPost {
-  id: number;
-  mod_user_id: number;
-  post_id: number;
-  locked?: boolean;
-  when_: string;
-  mod_user_name: string;
-  post_name: string;
-  community_id: number;
-  community_name: string;
-}
-
-export interface ModStickyPost {
-  id: number;
-  mod_user_id: number;
-  post_id: number;
-  stickied?: boolean;
-  when_: string;
-  mod_user_name: string;
-  post_name: string;
-  community_id: number;
-  community_name: string;
-}
-
-export interface ModRemoveComment {
-  id: number;
-  mod_user_id: number;
-  comment_id: number;
-  reason?: string;
-  removed?: boolean;
-  when_: string;
-  mod_user_name: string;
-  comment_user_id: number;
-  comment_user_name: string;
-  comment_content: string;
-  post_id: number;
-  post_name: string;
-  community_id: number;
-  community_name: string;
-}
-
-export interface ModRemoveCommunity {
-  id: number;
-  mod_user_id: number;
-  community_id: number;
-  reason?: string;
-  removed?: boolean;
-  expires?: number;
-  when_: string;
-  mod_user_name: string;
-  community_name: string;
-}
-
-export interface ModBanFromCommunity {
-  id: number;
-  mod_user_id: number;
-  other_user_id: number;
-  community_id: number;
-  reason?: string;
-  banned?: boolean;
-  expires?: number;
-  when_: string;
-  mod_user_name: string;
-  other_user_name: string;
-  community_name: string;
-}
-
-export interface ModBan {
-  id: number;
-  mod_user_id: number;
-  other_user_id: number;
-  reason?: string;
-  banned?: boolean;
-  expires?: number;
-  when_: string;
-  mod_user_name: string;
-  other_user_name: string;
-}
-
-export interface ModAddCommunity {
-  id: number;
-  mod_user_id: number;
-  other_user_id: number;
-  community_id: number;
-  removed?: boolean;
-  when_: string;
-  mod_user_name: string;
-  other_user_name: string;
-  community_name: string;
-}
-
-export interface ModAdd {
-  id: number;
-  mod_user_id: number;
-  other_user_id: number;
-  removed?: boolean;
-  when_: string;
-  mod_user_name: string;
-  other_user_name: string;
-}
-
-export interface LoginForm {
-  username_or_email: string;
-  password: string;
-}
-
-export interface RegisterForm {
-  username: string;
-  email?: string;
-  password: string;
-  password_verify: string;
-  admin: boolean;
-  show_nsfw: boolean;
-}
-
-export interface LoginResponse {
-  jwt: string;
-}
-
-export interface UserSettingsForm {
-  show_nsfw: boolean;
-  theme: string;
-  default_sort_type: SortType;
-  default_listing_type: ListingType;
-  lang: string;
-  avatar?: string;
-  email?: string;
-  matrix_user_id?: string;
-  new_password?: string;
-  new_password_verify?: string;
-  old_password?: string;
-  show_avatars: boolean;
-  send_notifications_to_email: boolean;
-  auth: string;
-}
-
-export interface CommunityForm {
-  name: string;
-  title: string;
-  description?: string;
-  category_id: number;
-  edit_id?: number;
-  removed?: boolean;
-  deleted?: boolean;
-  nsfw: boolean;
-  reason?: string;
-  expires?: number;
-  auth?: string;
-}
-
-export interface GetCommunityForm {
-  id?: number;
-  name?: string;
-  auth?: string;
-}
-
-export interface GetCommunityResponse {
-  community: Community;
-  moderators: Array<CommunityUser>;
-  admins: Array<UserView>;
-  online: number;
-}
-
-export interface CommunityResponse {
-  community: Community;
-}
-
-export interface ListCommunitiesForm {
-  sort: string;
-  page?: number;
-  limit?: number;
-  auth?: string;
-}
-
-export interface ListCommunitiesResponse {
-  communities: Array<Community>;
-}
-
-export interface ListCategoriesResponse {
-  categories: Array<Category>;
-}
-
-export interface PostForm {
-  name: string;
-  url?: string;
-  body?: string;
-  community_id: number;
-  updated?: number;
-  edit_id?: number;
-  creator_id: number;
-  removed?: boolean;
-  deleted?: boolean;
-  nsfw: boolean;
-  locked?: boolean;
-  stickied?: boolean;
-  reason?: string;
-  auth: string;
-}
-
-export interface PostFormParams {
-  name: string;
-  url?: string;
-  body?: string;
-  community?: string;
-}
-
-export interface GetPostForm {
-  id: number;
-  auth?: string;
-}
-
-export interface GetPostResponse {
-  post: Post;
-  comments: Array<Comment>;
-  community: Community;
-  moderators: Array<CommunityUser>;
-  admins: Array<UserView>;
-  online: number;
-}
-
-export interface SavePostForm {
-  post_id: number;
-  save: boolean;
-  auth?: string;
-}
-
-export interface PostResponse {
-  post: Post;
-}
-
-export interface CommentForm {
-  content: string;
-  post_id: number;
-  parent_id?: number;
-  edit_id?: number;
-  creator_id?: number;
-  removed?: boolean;
-  deleted?: boolean;
-  reason?: string;
-  read?: boolean;
-  auth: string;
-}
-
-export interface SaveCommentForm {
-  comment_id: number;
-  save: boolean;
-  auth?: string;
-}
-
-export interface CommentResponse {
-  comment: Comment;
-  recipient_ids: Array<number>;
-}
-
-export interface CommentLikeForm {
-  comment_id: number;
-  post_id: number;
-  score: number;
-  auth?: string;
-}
-
-export interface CommentNode {
-  comment: Comment;
-  children?: Array<CommentNode>;
-}
-
-export interface GetPostsForm {
-  type_: string;
-  sort: string;
-  page?: number;
-  limit?: number;
-  community_id?: number;
-  auth?: string;
-}
-
-export interface GetPostsResponse {
-  posts: Array<Post>;
-}
-
-export interface GetCommentsForm {
-  type_: string;
-  sort: string;
-  page?: number;
-  limit: number;
-  community_id?: number;
-  auth?: string;
-}
-
-export interface GetCommentsResponse {
-  comments: Array<Comment>;
-}
-
-export interface CreatePostLikeForm {
-  post_id: number;
-  score: number;
-  auth?: string;
-}
-
-export interface SiteForm {
-  name: string;
-  description?: string;
-  enable_downvotes: boolean;
-  open_registration: boolean;
-  enable_nsfw: boolean;
-  auth?: string;
-}
-
-export interface GetSiteConfig {
-  auth?: string;
-}
-
-export interface GetSiteConfigResponse {
-  config_hjson: string;
-}
-
-export interface SiteConfigForm {
-  config_hjson: string;
-  auth?: string;
-}
-
-export interface GetSiteResponse {
-  site: Site;
-  admins: Array<UserView>;
-  banned: Array<UserView>;
-  online: number;
-}
-
-export interface SiteResponse {
-  site: Site;
-}
-
-export interface BanUserForm {
-  user_id: number;
-  ban: boolean;
-  reason?: string;
-  expires?: number;
-  auth?: string;
-}
-
-export interface BanUserResponse {
-  user: UserView;
-  banned: boolean;
-}
-
-export interface AddAdminForm {
-  user_id: number;
-  added: boolean;
-  auth?: string;
-}
-
-export interface AddAdminResponse {
-  admins: Array<UserView>;
-}
-
-export interface SearchForm {
-  q: string;
-  type_: string;
-  community_id?: number;
-  sort: string;
-  page?: number;
-  limit?: number;
-  auth?: string;
-}
-
-export interface SearchResponse {
-  type_: string;
-  posts?: Array<Post>;
-  comments?: Array<Comment>;
-  communities: Array<Community>;
-  users: Array<UserView>;
-}
-
-export interface DeleteAccountForm {
-  password: string;
-}
-
-export interface PasswordResetForm {
-  email: string;
-}
-
-// export interface PasswordResetResponse {
-// }
-
-export interface PasswordChangeForm {
-  token: string;
-  password: string;
-  password_verify: string;
-}
-
-export interface PrivateMessageForm {
-  content: string;
-  recipient_id: number;
-  auth?: string;
-}
-
-export interface PrivateMessageFormParams {
-  recipient_id: number;
-}
-
-export interface EditPrivateMessageForm {
-  edit_id: number;
-  content?: string;
-  deleted?: boolean;
-  read?: boolean;
-  auth?: string;
-}
-
-export interface GetPrivateMessagesForm {
-  unread_only: boolean;
-  page?: number;
-  limit?: number;
-  auth?: string;
-}
-
-export interface PrivateMessagesResponse {
-  messages: Array<PrivateMessage>;
-}
-
-export interface PrivateMessageResponse {
-  message: PrivateMessage;
-}
-
-export interface UserJoinForm {
-  auth: string;
-}
-
-export interface UserJoinResponse {
-  user_id: number;
-}
-
-export type MessageType =
-  | EditPrivateMessageForm
-  | LoginForm
-  | RegisterForm
-  | CommunityForm
-  | FollowCommunityForm
-  | ListCommunitiesForm
-  | GetFollowedCommunitiesForm
-  | PostForm
-  | GetPostForm
-  | GetPostsForm
-  | GetCommunityForm
-  | CommentForm
-  | CommentLikeForm
-  | SaveCommentForm
-  | CreatePostLikeForm
-  | BanFromCommunityForm
-  | AddAdminForm
-  | AddModToCommunityForm
-  | TransferCommunityForm
-  | TransferSiteForm
-  | SaveCommentForm
-  | BanUserForm
-  | AddAdminForm
-  | GetUserDetailsForm
-  | GetRepliesForm
-  | GetUserMentionsForm
-  | EditUserMentionForm
-  | GetModlogForm
-  | SiteForm
-  | SearchForm
-  | UserSettingsForm
-  | DeleteAccountForm
-  | PasswordResetForm
-  | PasswordChangeForm
-  | PrivateMessageForm
-  | EditPrivateMessageForm
-  | GetPrivateMessagesForm
-  | SiteConfigForm;
-
-type ResponseType =
-  | SiteResponse
-  | GetFollowedCommunitiesResponse
-  | ListCommunitiesResponse
-  | GetPostsResponse
-  | PostResponse
-  | GetRepliesResponse
-  | GetUserMentionsResponse
-  | ListCategoriesResponse
-  | CommunityResponse
-  | CommentResponse
-  | UserMentionResponse
-  | LoginResponse
-  | GetModlogResponse
-  | SearchResponse
-  | BanFromCommunityResponse
-  | AddModToCommunityResponse
-  | BanUserResponse
-  | AddAdminResponse
-  | PrivateMessageResponse
-  | PrivateMessagesResponse
-  | GetSiteConfigResponse;
-
-export interface WebSocketResponse {
-  op: UserOperation;
-  data: ResponseType;
-}
-
-export interface WebSocketJsonResponse {
-  op?: string;
-  data?: ResponseType;
-  error?: string;
-  reconnect?: boolean;
-}
-
 export enum UserDetailsView {
   Overview,
   Comments,
diff --git a/ui/src/service-worker.ts b/ui/src/service-worker.ts
new file mode 100644 (file)
index 0000000..a7e1bd5
--- /dev/null
@@ -0,0 +1,28 @@
+import { register } from 'register-service-worker';
+
+register('/service-worker.js', {
+  registrationOptions: { scope: './' },
+  ready(registration) {
+    console.log('Service worker is active.');
+  },
+  registered(registration) {
+    console.log('Service worker has been registered.');
+  },
+  cached(registration) {
+    console.log('Content has been cached for offline use.');
+  },
+  updatefound(registration) {
+    console.log('New content is downloading.');
+  },
+  updated(registration) {
+    console.log('New content is available; please refresh.');
+  },
+  offline() {
+    console.log(
+      'No internet connection found. App is running in offline mode.'
+    );
+  },
+  error(error) {
+    console.error('Error during service worker registration:', error);
+  },
+});
index 786d5d07ebdb03e8419e56b652f03fb941503c72..513ba608079ea29e115b562141cb0d8e32d84879 100644 (file)
@@ -1,20 +1,27 @@
 import Cookies from 'js-cookie';
-import { User, LoginResponse } from '../interfaces';
+import { User, LoginResponse } from 'lemmy-js-client';
 import { setTheme } from '../utils';
 import jwt_decode from 'jwt-decode';
-import { Subject } from 'rxjs';
+import { Subject, BehaviorSubject } from 'rxjs';
+
+interface Claims {
+  id: number;
+  iss: string;
+}
 
 export class UserService {
   private static _instance: UserService;
   public user: User;
-  public sub: Subject<{ user: User }> = new Subject<{
-    user: User;
-  }>();
+  public claims: Claims;
+  public jwtSub: Subject<string> = new Subject<string>();
+  public unreadCountSub: BehaviorSubject<number> = new BehaviorSubject<number>(
+    0
+  );
 
   private constructor() {
     let jwt = Cookies.get('jwt');
     if (jwt) {
-      this.setUser(jwt);
+      this.setClaims(jwt);
     } else {
       setTheme();
       console.log('No JWT cookie found.');
@@ -22,16 +29,17 @@ export class UserService {
   }
 
   public login(res: LoginResponse) {
-    this.setUser(res.jwt);
+    this.setClaims(res.jwt);
     Cookies.set('jwt', res.jwt, { expires: 365 });
     console.log('jwt cookie set');
   }
 
   public logout() {
+    this.claims = undefined;
     this.user = undefined;
     Cookies.remove('jwt');
     setTheme();
-    this.sub.next({ user: undefined });
+    this.jwtSub.next();
     console.log('Logged out.');
   }
 
@@ -39,11 +47,9 @@ export class UserService {
     return Cookies.get('jwt');
   }
 
-  private setUser(jwt: string) {
-    this.user = jwt_decode(jwt);
-    setTheme(this.user.theme, true);
-    this.sub.next({ user: this.user });
-    console.log(this.user);
+  private setClaims(jwt: string) {
+    this.claims = jwt_decode(jwt);
+    this.jwtSub.next(jwt);
   }
 
   public static get Instance() {
index 8e4364d2d496c3bb739d34bda37ffb483a4e3045..93587c9901a6ed617e41ed95ea8f7a278a052e2f 100644 (file)
@@ -1,12 +1,21 @@
 import { wsUri } from '../env';
 import {
+  LemmyWebsocket,
   LoginForm,
   RegisterForm,
-  UserOperation,
   CommunityForm,
+  DeleteCommunityForm,
+  RemoveCommunityForm,
   PostForm,
+  DeletePostForm,
+  RemovePostForm,
+  LockPostForm,
+  StickyPostForm,
   SavePostForm,
   CommentForm,
+  DeleteCommentForm,
+  RemoveCommentForm,
+  MarkCommentAsReadForm,
   SaveCommentForm,
   CommentLikeForm,
   GetPostForm,
@@ -28,7 +37,7 @@ import {
   UserView,
   GetRepliesForm,
   GetUserMentionsForm,
-  EditUserMentionForm,
+  MarkUserMentionAsReadForm,
   SearchForm,
   UserSettingsForm,
   DeleteAccountForm,
@@ -36,14 +45,17 @@ import {
   PasswordChangeForm,
   PrivateMessageForm,
   EditPrivateMessageForm,
+  DeletePrivateMessageForm,
+  MarkPrivateMessageAsReadForm,
   GetPrivateMessagesForm,
   GetCommentsForm,
   UserJoinForm,
   GetSiteConfig,
+  GetSiteForm,
   SiteConfigForm,
-  MessageType,
+  MarkAllAsReadForm,
   WebSocketJsonResponse,
-} from '../interfaces';
+} from 'lemmy-js-client';
 import { UserService } from './';
 import { i18n } from '../i18next';
 import { toast } from '../utils';
@@ -58,6 +70,7 @@ export class WebSocketService {
 
   public admins: Array<UserView>;
   public banned: Array<UserView>;
+  private client = new LemmyWebsocket();
 
   private constructor() {
     this.ws = new ReconnectingWebSocket(wsUri);
@@ -70,10 +83,6 @@ export class WebSocketService {
       this.ws.onopen = () => {
         console.log(`Connected to ${wsUri}`);
 
-        if (UserService.Instance.user) {
-          this.userJoin();
-        }
-
         if (!firstConnect) {
           let res: WebSocketJsonResponse = {
             reconnect: true,
@@ -92,243 +101,287 @@ export class WebSocketService {
 
   public userJoin() {
     let form: UserJoinForm = { auth: UserService.Instance.auth };
-    this.ws.send(this.wsSendWrapper(UserOperation.UserJoin, form));
+    this.ws.send(this.client.userJoin(form));
   }
 
-  public login(loginForm: LoginForm) {
-    this.ws.send(this.wsSendWrapper(UserOperation.Login, loginForm));
+  public login(form: LoginForm) {
+    this.ws.send(this.client.login(form));
   }
 
-  public register(registerForm: RegisterForm) {
-    this.ws.send(this.wsSendWrapper(UserOperation.Register, registerForm));
+  public register(form: RegisterForm) {
+    this.ws.send(this.client.register(form));
   }
 
-  public createCommunity(communityForm: CommunityForm) {
-    this.setAuth(communityForm);
-    this.ws.send(
-      this.wsSendWrapper(UserOperation.CreateCommunity, communityForm)
-    );
+  public getCaptcha() {
+    this.ws.send(this.client.getCaptcha());
   }
 
-  public editCommunity(communityForm: CommunityForm) {
-    this.setAuth(communityForm);
-    this.ws.send(
-      this.wsSendWrapper(UserOperation.EditCommunity, communityForm)
-    );
+  public createCommunity(form: CommunityForm) {
+    this.setAuth(form); // TODO all these setauths at some point would be good to make required
+    this.ws.send(this.client.createCommunity(form));
   }
 
-  public followCommunity(followCommunityForm: FollowCommunityForm) {
-    this.setAuth(followCommunityForm);
-    this.ws.send(
-      this.wsSendWrapper(UserOperation.FollowCommunity, followCommunityForm)
-    );
+  public editCommunity(form: CommunityForm) {
+    this.setAuth(form);
+    this.ws.send(this.client.editCommunity(form));
+  }
+
+  public deleteCommunity(form: DeleteCommunityForm) {
+    this.setAuth(form);
+    this.ws.send(this.client.deleteCommunity(form));
+  }
+
+  public removeCommunity(form: RemoveCommunityForm) {
+    this.setAuth(form);
+    this.ws.send(this.client.removeCommunity(form));
+  }
+
+  public followCommunity(form: FollowCommunityForm) {
+    this.setAuth(form);
+    this.ws.send(this.client.followCommunity(form));
   }
 
   public listCommunities(form: ListCommunitiesForm) {
     this.setAuth(form, false);
-    this.ws.send(this.wsSendWrapper(UserOperation.ListCommunities, form));
+    this.ws.send(this.client.listCommunities(form));
   }
 
   public getFollowedCommunities() {
     let form: GetFollowedCommunitiesForm = { auth: UserService.Instance.auth };
-    this.ws.send(
-      this.wsSendWrapper(UserOperation.GetFollowedCommunities, form)
-    );
+    this.ws.send(this.client.getFollowedCommunities(form));
   }
 
   public listCategories() {
-    this.ws.send(this.wsSendWrapper(UserOperation.ListCategories, {}));
+    this.ws.send(this.client.listCategories());
   }
 
-  public createPost(postForm: PostForm) {
-    this.setAuth(postForm);
-    this.ws.send(this.wsSendWrapper(UserOperation.CreatePost, postForm));
+  public createPost(form: PostForm) {
+    this.setAuth(form);
+    this.ws.send(this.client.createPost(form));
   }
 
   public getPost(form: GetPostForm) {
     this.setAuth(form, false);
-    this.ws.send(this.wsSendWrapper(UserOperation.GetPost, form));
+    this.ws.send(this.client.getPost(form));
   }
 
   public getCommunity(form: GetCommunityForm) {
     this.setAuth(form, false);
-    this.ws.send(this.wsSendWrapper(UserOperation.GetCommunity, form));
+    this.ws.send(this.client.getCommunity(form));
   }
 
-  public createComment(commentForm: CommentForm) {
-    this.setAuth(commentForm);
-    this.ws.send(this.wsSendWrapper(UserOperation.CreateComment, commentForm));
+  public createComment(form: CommentForm) {
+    this.setAuth(form);
+    this.ws.send(this.client.createComment(form));
+  }
+
+  public editComment(form: CommentForm) {
+    this.setAuth(form);
+    this.ws.send(this.client.editComment(form));
+  }
+
+  public deleteComment(form: DeleteCommentForm) {
+    this.setAuth(form);
+    this.ws.send(this.client.deleteComment(form));
+  }
+
+  public removeComment(form: RemoveCommentForm) {
+    this.setAuth(form);
+    this.ws.send(this.client.removeComment(form));
   }
 
-  public editComment(commentForm: CommentForm) {
-    this.setAuth(commentForm);
-    this.ws.send(this.wsSendWrapper(UserOperation.EditComment, commentForm));
+  public markCommentAsRead(form: MarkCommentAsReadForm) {
+    this.setAuth(form);
+    this.ws.send(this.client.markCommentAsRead(form));
   }
 
   public likeComment(form: CommentLikeForm) {
     this.setAuth(form);
-    this.ws.send(this.wsSendWrapper(UserOperation.CreateCommentLike, form));
+    this.ws.send(this.client.likeComment(form));
   }
 
   public saveComment(form: SaveCommentForm) {
     this.setAuth(form);
-    this.ws.send(this.wsSendWrapper(UserOperation.SaveComment, form));
+    this.ws.send(this.client.saveComment(form));
   }
 
   public getPosts(form: GetPostsForm) {
     this.setAuth(form, false);
-    this.ws.send(this.wsSendWrapper(UserOperation.GetPosts, form));
+    this.ws.send(this.client.getPosts(form));
   }
 
   public getComments(form: GetCommentsForm) {
     this.setAuth(form, false);
-    this.ws.send(this.wsSendWrapper(UserOperation.GetComments, form));
+    this.ws.send(this.client.getComments(form));
   }
 
   public likePost(form: CreatePostLikeForm) {
     this.setAuth(form);
-    this.ws.send(this.wsSendWrapper(UserOperation.CreatePostLike, form));
+    this.ws.send(this.client.likePost(form));
+  }
+
+  public editPost(form: PostForm) {
+    this.setAuth(form);
+    this.ws.send(this.client.editPost(form));
   }
 
-  public editPost(postForm: PostForm) {
-    this.setAuth(postForm);
-    this.ws.send(this.wsSendWrapper(UserOperation.EditPost, postForm));
+  public deletePost(form: DeletePostForm) {
+    this.setAuth(form);
+    this.ws.send(this.client.deletePost(form));
+  }
+
+  public removePost(form: RemovePostForm) {
+    this.setAuth(form);
+    this.ws.send(this.client.removePost(form));
+  }
+
+  public lockPost(form: LockPostForm) {
+    this.setAuth(form);
+    this.ws.send(this.client.lockPost(form));
+  }
+
+  public stickyPost(form: StickyPostForm) {
+    this.setAuth(form);
+    this.ws.send(this.client.stickyPost(form));
   }
 
   public savePost(form: SavePostForm) {
     this.setAuth(form);
-    this.ws.send(this.wsSendWrapper(UserOperation.SavePost, form));
+    this.ws.send(this.client.savePost(form));
   }
 
   public banFromCommunity(form: BanFromCommunityForm) {
     this.setAuth(form);
-    this.ws.send(this.wsSendWrapper(UserOperation.BanFromCommunity, form));
+    this.ws.send(this.client.banFromCommunity(form));
   }
 
   public addModToCommunity(form: AddModToCommunityForm) {
     this.setAuth(form);
-    this.ws.send(this.wsSendWrapper(UserOperation.AddModToCommunity, form));
+    this.ws.send(this.client.addModToCommunity(form));
   }
 
   public transferCommunity(form: TransferCommunityForm) {
     this.setAuth(form);
-    this.ws.send(this.wsSendWrapper(UserOperation.TransferCommunity, form));
+    this.ws.send(this.client.transferCommunity(form));
   }
 
   public transferSite(form: TransferSiteForm) {
     this.setAuth(form);
-    this.ws.send(this.wsSendWrapper(UserOperation.TransferSite, form));
+    this.ws.send(this.client.transferSite(form));
   }
 
   public banUser(form: BanUserForm) {
     this.setAuth(form);
-    this.ws.send(this.wsSendWrapper(UserOperation.BanUser, form));
+    this.ws.send(this.client.banUser(form));
   }
 
   public addAdmin(form: AddAdminForm) {
     this.setAuth(form);
-    this.ws.send(this.wsSendWrapper(UserOperation.AddAdmin, form));
+    this.ws.send(this.client.addAdmin(form));
   }
 
   public getUserDetails(form: GetUserDetailsForm) {
     this.setAuth(form, false);
-    this.ws.send(this.wsSendWrapper(UserOperation.GetUserDetails, form));
+    this.ws.send(this.client.getUserDetails(form));
   }
 
   public getReplies(form: GetRepliesForm) {
     this.setAuth(form);
-    this.ws.send(this.wsSendWrapper(UserOperation.GetReplies, form));
+    this.ws.send(this.client.getReplies(form));
   }
 
   public getUserMentions(form: GetUserMentionsForm) {
     this.setAuth(form);
-    this.ws.send(this.wsSendWrapper(UserOperation.GetUserMentions, form));
+    this.ws.send(this.client.getUserMentions(form));
   }
 
-  public editUserMention(form: EditUserMentionForm) {
+  public markUserMentionAsRead(form: MarkUserMentionAsReadForm) {
     this.setAuth(form);
-    this.ws.send(this.wsSendWrapper(UserOperation.EditUserMention, form));
+    this.ws.send(this.client.markUserMentionAsRead(form));
   }
 
   public getModlog(form: GetModlogForm) {
-    this.ws.send(this.wsSendWrapper(UserOperation.GetModlog, form));
+    this.ws.send(this.client.getModlog(form));
   }
 
-  public createSite(siteForm: SiteForm) {
-    this.setAuth(siteForm);
-    this.ws.send(this.wsSendWrapper(UserOperation.CreateSite, siteForm));
+  public createSite(form: SiteForm) {
+    this.setAuth(form);
+    this.ws.send(this.client.createSite(form));
   }
 
-  public editSite(siteForm: SiteForm) {
-    this.setAuth(siteForm);
-    this.ws.send(this.wsSendWrapper(UserOperation.EditSite, siteForm));
+  public editSite(form: SiteForm) {
+    this.setAuth(form);
+    this.ws.send(this.client.editSite(form));
   }
 
-  public getSite() {
-    this.ws.send(this.wsSendWrapper(UserOperation.GetSite, {}));
+  public getSite(form: GetSiteForm = {}) {
+    this.setAuth(form, false);
+    this.ws.send(this.client.getSite(form));
   }
 
   public getSiteConfig() {
-    let siteConfig: GetSiteConfig = {};
-    this.setAuth(siteConfig);
-    this.ws.send(this.wsSendWrapper(UserOperation.GetSiteConfig, siteConfig));
+    let form: GetSiteConfig = {};
+    this.setAuth(form);
+    this.ws.send(this.client.getSiteConfig(form));
   }
 
   public search(form: SearchForm) {
     this.setAuth(form, false);
-    this.ws.send(this.wsSendWrapper(UserOperation.Search, form));
+    this.ws.send(this.client.search(form));
   }
 
   public markAllAsRead() {
-    let form = {};
+    let form: MarkAllAsReadForm;
     this.setAuth(form);
-    this.ws.send(this.wsSendWrapper(UserOperation.MarkAllAsRead, form));
+    this.ws.send(this.client.markAllAsRead(form));
   }
 
-  public saveUserSettings(userSettingsForm: UserSettingsForm) {
-    this.setAuth(userSettingsForm);
-    this.ws.send(
-      this.wsSendWrapper(UserOperation.SaveUserSettings, userSettingsForm)
-    );
+  public saveUserSettings(form: UserSettingsForm) {
+    this.setAuth(form);
+    this.ws.send(this.client.saveUserSettings(form));
   }
 
   public deleteAccount(form: DeleteAccountForm) {
     this.setAuth(form);
-    this.ws.send(this.wsSendWrapper(UserOperation.DeleteAccount, form));
+    this.ws.send(this.client.deleteAccount(form));
   }
 
   public passwordReset(form: PasswordResetForm) {
-    this.ws.send(this.wsSendWrapper(UserOperation.PasswordReset, form));
+    this.ws.send(this.client.passwordReset(form));
   }
 
   public passwordChange(form: PasswordChangeForm) {
-    this.ws.send(this.wsSendWrapper(UserOperation.PasswordChange, form));
+    this.ws.send(this.client.passwordChange(form));
   }
 
   public createPrivateMessage(form: PrivateMessageForm) {
     this.setAuth(form);
-    this.ws.send(this.wsSendWrapper(UserOperation.CreatePrivateMessage, form));
+    this.ws.send(this.client.createPrivateMessage(form));
   }
 
   public editPrivateMessage(form: EditPrivateMessageForm) {
     this.setAuth(form);
-    this.ws.send(this.wsSendWrapper(UserOperation.EditPrivateMessage, form));
+    this.ws.send(this.client.editPrivateMessage(form));
   }
 
-  public getPrivateMessages(form: GetPrivateMessagesForm) {
+  public deletePrivateMessage(form: DeletePrivateMessageForm) {
     this.setAuth(form);
-    this.ws.send(this.wsSendWrapper(UserOperation.GetPrivateMessages, form));
+    this.ws.send(this.client.deletePrivateMessage(form));
   }
 
-  public saveSiteConfig(form: SiteConfigForm) {
+  public markPrivateMessageAsRead(form: MarkPrivateMessageAsReadForm) {
     this.setAuth(form);
-    this.ws.send(this.wsSendWrapper(UserOperation.SaveSiteConfig, form));
+    this.ws.send(this.client.markPrivateMessageAsRead(form));
   }
 
-  private wsSendWrapper(op: UserOperation, data: MessageType) {
-    let send = { op: UserOperation[op], data: data };
-    console.log(send);
-    return JSON.stringify(send);
+  public getPrivateMessages(form: GetPrivateMessagesForm) {
+    this.setAuth(form);
+    this.ws.send(this.client.getPrivateMessages(form));
+  }
+
+  public saveSiteConfig(form: SiteConfigForm) {
+    this.setAuth(form);
+    this.ws.send(this.client.saveSiteConfig(form));
   }
 
   private setAuth(obj: any, throwErr: boolean = true) {
index 2bede77704aeee5553a48df68453a2ac810ea8ad..a312a663c5ced1fc8e59d5420502a2c5be65330d 100644 (file)
@@ -25,6 +25,7 @@ import 'moment/locale/sq';
 import 'moment/locale/km';
 import 'moment/locale/ga';
 import 'moment/locale/sr';
+import 'moment/locale/ko';
 
 import {
   UserOperation,
@@ -34,9 +35,7 @@ import {
   PrivateMessage,
   User,
   SortType,
-  CommentSortType,
   ListingType,
-  DataType,
   SearchType,
   WebSocketResponse,
   WebSocketJsonResponse,
@@ -44,11 +43,15 @@ import {
   SearchResponse,
   CommentResponse,
   PostResponse,
-} from './interfaces';
+} from 'lemmy-js-client';
+
+import { CommentSortType, DataType } from './interfaces';
 import { UserService, WebSocketService } from './services';
 
 import Tribute from 'tributejs/src/Tribute.js';
 import markdown_it from 'markdown-it';
+import markdown_it_sub from 'markdown-it-sub';
+import markdown_it_sup from 'markdown-it-sup';
 import markdownitEmoji from 'markdown-it-emoji/light';
 import markdown_it_container from 'markdown-it-container';
 import emojiShortName from 'emoji-short-name';
@@ -56,11 +59,15 @@ import Toastify from 'toastify-js';
 import tippy from 'tippy.js';
 import moment from 'moment';
 
+export const favIconUrl = '/static/assets/favicon.svg';
+export const favIconPngUrl = '/static/assets/apple-touch-icon.png';
+export const defaultFavIcon = `${window.location.protocol}//${window.location.host}${favIconPngUrl}`;
 export const repoUrl = 'https://github.com/LemmyNet/lemmy';
 export const helpGuideUrl = '/docs/about_guide.html';
 export const markdownHelpUrl = `${helpGuideUrl}#markdown-guide`;
 export const sortingHelpUrl = `${helpGuideUrl}#sorting`;
 export const archiveUrl = 'https://archive.is';
+export const elementUrl = 'https://element.io/';
 
 export const postRefetchSeconds: number = 60 * 1000;
 export const fetchLimit: number = 20;
@@ -78,6 +85,7 @@ export const languages = [
   { code: 'gl', name: 'Galego' },
   { code: 'hu', name: 'Magyar Nyelv' },
   { code: 'ka', name: 'ქართული ენა' },
+  { code: 'ko', name: '한국어' },
   { code: 'km', name: 'ភាសាខ្មែរ' },
   { code: 'hi', name: 'मानक हिन्दी' },
   { code: 'fa', name: 'فارسی' },
@@ -148,6 +156,8 @@ export const md = new markdown_it({
   linkify: true,
   typographer: true,
 })
+  .use(markdown_it_sub)
+  .use(markdown_it_sup)
   .use(markdown_it_container, 'spoiler', {
     validate: function (params: any) {
       return params.trim().match(/^spoiler\s+(.*)$/);
@@ -265,25 +275,11 @@ export function capitalizeFirstLetter(str: string): string {
 }
 
 export function routeSortTypeToEnum(sort: string): SortType {
-  if (sort == 'new') {
-    return SortType.New;
-  } else if (sort == 'hot') {
-    return SortType.Hot;
-  } else if (sort == 'topday') {
-    return SortType.TopDay;
-  } else if (sort == 'topweek') {
-    return SortType.TopWeek;
-  } else if (sort == 'topmonth') {
-    return SortType.TopMonth;
-  } else if (sort == 'topyear') {
-    return SortType.TopYear;
-  } else if (sort == 'topall') {
-    return SortType.TopAll;
-  }
+  return SortType[sort];
 }
 
 export function routeListingTypeToEnum(type: string): ListingType {
-  return ListingType[capitalizeFirstLetter(type)];
+  return ListingType[type];
 }
 
 export function routeDataTypeToEnum(type: string): DataType {
@@ -346,9 +342,9 @@ export function debounce(
   };
 }
 
-export function getLanguage(): string {
+export function getLanguage(override?: string): string {
   let user = UserService.Instance.user;
-  let lang = user && user.lang ? user.lang : 'browser';
+  let lang = override || (user && user.lang ? user.lang : 'browser');
 
   if (lang == 'browser') {
     return getBrowserLanguage();
@@ -417,6 +413,8 @@ export function getMomentLanguage(): string {
     lang = 'ga';
   } else if (lang.startsWith('sr')) {
     lang = 'sr';
+  } else if (lang.startsWith('ko')) {
+    lang = 'ko';
   } else {
     lang = 'en';
   }
@@ -518,8 +516,19 @@ export function pictrsImage(hash: string, thumbnail: boolean = false): string {
   return out;
 }
 
-export function isCommentType(item: Comment | PrivateMessage): item is Comment {
-  return (item as Comment).community_id !== undefined;
+export function isCommentType(
+  item: Comment | PrivateMessage | Post
+): item is Comment {
+  return (
+    (item as Comment).community_id !== undefined &&
+    (item as Comment).content !== undefined
+  );
+}
+
+export function isPostType(
+  item: Comment | PrivateMessage | Post
+): item is Post {
+  return (item as Post).stickied !== undefined;
 }
 
 export function toast(text: string, background: string = 'success') {
@@ -555,18 +564,20 @@ export function pictrsDeleteToast(
   }).showToast();
 }
 
-export function messageToastify(
-  creator: string,
-  avatar: string,
-  body: string,
-  link: string,
-  router: any
-) {
+interface NotifyInfo {
+  name: string;
+  icon: string;
+  link: string;
+  body: string;
+}
+
+export function messageToastify(info: NotifyInfo, router: any) {
+  let htmlBody = info.body ? md.render(info.body) : '';
   let backgroundColor = `var(--light)`;
 
   let toast = Toastify({
-    text: `${body}<br />${creator}`,
-    avatar: avatar,
+    text: `${htmlBody}<br />${info.name}`,
+    avatar: info.icon,
     backgroundColor: backgroundColor,
     className: 'text-dark',
     close: true,
@@ -576,14 +587,64 @@ export function messageToastify(
     onClick: () => {
       if (toast) {
         toast.hideToast();
-        router.history.push(link);
+        router.history.push(info.link);
       }
     },
   }).showToast();
 }
 
+export function notifyPost(post: Post, router: any) {
+  let info: NotifyInfo = {
+    name: post.community_name,
+    icon: post.community_icon ? post.community_icon : defaultFavIcon,
+    link: `/post/${post.id}`,
+    body: post.name,
+  };
+  notify(info, router);
+}
+
+export function notifyComment(comment: Comment, router: any) {
+  let info: NotifyInfo = {
+    name: comment.creator_name,
+    icon: comment.creator_avatar ? comment.creator_avatar : defaultFavIcon,
+    link: `/post/${comment.post_id}/comment/${comment.id}`,
+    body: comment.content,
+  };
+  notify(info, router);
+}
+
+export function notifyPrivateMessage(pm: PrivateMessage, router: any) {
+  let info: NotifyInfo = {
+    name: pm.creator_name,
+    icon: pm.creator_avatar ? pm.creator_avatar : defaultFavIcon,
+    link: `/inbox`,
+    body: pm.content,
+  };
+  notify(info, router);
+}
+
+function notify(info: NotifyInfo, router: any) {
+  messageToastify(info, router);
+
+  if (Notification.permission !== 'granted') Notification.requestPermission();
+  else {
+    var notification = new Notification(info.name, {
+      icon: info.icon,
+      body: info.body,
+    });
+
+    notification.onclick = () => {
+      event.preventDefault();
+      router.history.push(info.link);
+    };
+  }
+}
+
 export function setupTribute(): Tribute {
   return new Tribute({
+    noMatchTemplate: function () {
+      return '';
+    },
     collection: [
       // Emojis
       {
@@ -657,15 +718,15 @@ function userSearch(text: string, cb: any) {
   if (text) {
     let form: SearchForm = {
       q: text,
-      type_: SearchType[SearchType.Users],
-      sort: SortType[SortType.TopAll],
+      type_: SearchType.Users,
+      sort: SortType.TopAll,
       page: 1,
       limit: mentionDropdownFetchLimit,
     };
 
     WebSocketService.Instance.search(form);
 
-    this.userSub = WebSocketService.Instance.subject.subscribe(
+    let userSub = WebSocketService.Instance.subject.subscribe(
       msg => {
         let res = wsJsonToRes(msg);
         if (res.op == UserOperation.Search) {
@@ -679,7 +740,7 @@ function userSearch(text: string, cb: any) {
             };
           });
           cb(users);
-          this.userSub.unsubscribe();
+          userSub.unsubscribe();
         }
       },
       err => console.error(err),
@@ -694,15 +755,15 @@ function communitySearch(text: string, cb: any) {
   if (text) {
     let form: SearchForm = {
       q: text,
-      type_: SearchType[SearchType.Communities],
-      sort: SortType[SortType.TopAll],
+      type_: SearchType.Communities,
+      sort: SortType.TopAll,
       page: 1,
       limit: mentionDropdownFetchLimit,
     };
 
     WebSocketService.Instance.search(form);
 
-    this.communitySub = WebSocketService.Instance.subject.subscribe(
+    let communitySub = WebSocketService.Instance.subject.subscribe(
       msg => {
         let res = wsJsonToRes(msg);
         if (res.op == UserOperation.Search) {
@@ -716,7 +777,7 @@ function communitySearch(text: string, cb: any) {
             };
           });
           cb(communities);
-          this.communitySub.unsubscribe();
+          communitySub.unsubscribe();
         }
       },
       err => console.error(err),
@@ -731,7 +792,7 @@ export function getListingTypeFromProps(props: any): ListingType {
   return props.match.params.listing_type
     ? routeListingTypeToEnum(props.match.params.listing_type)
     : UserService.Instance.user
-    ? UserService.Instance.user.default_listing_type
+    ? Object.values(ListingType)[UserService.Instance.user.default_listing_type]
     : ListingType.All;
 }
 
@@ -746,8 +807,8 @@ export function getSortTypeFromProps(props: any): SortType {
   return props.match.params.sort
     ? routeSortTypeToEnum(props.match.params.sort)
     : UserService.Instance.user
-    ? UserService.Instance.user.default_sort_type
-    : SortType.Hot;
+    ? Object.values(SortType)[UserService.Instance.user.default_sort_type]
+    : SortType.Active;
 }
 
 export function getPageFromProps(props: any): number {
@@ -825,6 +886,11 @@ export function editPostRes(data: PostResponse, post: Post) {
     post.url = data.post.url;
     post.name = data.post.name;
     post.nsfw = data.post.nsfw;
+    post.deleted = data.post.deleted;
+    post.removed = data.post.removed;
+    post.stickied = data.post.stickied;
+    post.body = data.post.body;
+    post.locked = data.post.locked;
   }
 }
 
@@ -893,7 +959,7 @@ function convertCommentSortType(sort: SortType): CommentSortType {
     return CommentSortType.Top;
   } else if (sort == SortType.New) {
     return CommentSortType.New;
-  } else if (sort == SortType.Hot) {
+  } else if (sort == SortType.Hot || sort == SortType.Active) {
     return CommentSortType.Hot;
   } else {
     return CommentSortType.Hot;
@@ -936,6 +1002,14 @@ export function postSort(
         (communityType && +b.stickied - +a.stickied) ||
         b.hot_rank - a.hot_rank
     );
+  } else if (sort == SortType.Active) {
+    posts.sort(
+      (a, b) =>
+        +a.removed - +b.removed ||
+        +a.deleted - +b.deleted ||
+        (communityType && +b.stickied - +a.stickied) ||
+        b.hot_rank_active - a.hot_rank_active
+    );
   }
 }
 
@@ -956,12 +1030,19 @@ function randomHsl() {
   return `hsla(${Math.random() * 360}, 100%, 50%, 1)`;
 }
 
-export function previewLines(text: string, lines: number = 3): string {
-  // Use lines * 2 because markdown requires 2 lines
-  return text
-    .split('\n')
-    .slice(0, lines * 2)
-    .join('\n');
+export function previewLines(
+  text: string,
+  maxChars: number = 300,
+  maxLines: number = 1
+): string {
+  return (
+    text
+      .slice(0, maxChars)
+      .split('\n')
+      // Use lines * 2 because markdown requires 2 lines
+      .slice(0, maxLines * 2)
+      .join('\n') + '...'
+  );
 }
 
 export function hostname(url: string): string {
@@ -996,3 +1077,16 @@ export function validTitle(title?: string): boolean {
 
   return regex.test(title);
 }
+
+export function siteBannerCss(banner: string): string {
+  return ` \
+    background-image: linear-gradient( rgba(0, 0, 0, 0.8), rgba(0, 0, 0, 0.8) ) ,url("${banner}"); \
+    background-attachment: fixed; \
+    background-position: top; \
+    background-repeat: no-repeat; \
+    background-size: 100% cover; \
+
+    width: 100%; \
+    max-height: 100vh; \
+    `;
+}
diff --git a/ui/src/version.ts b/ui/src/version.ts
deleted file mode 100644 (file)
index bfbd20a..0000000
+++ /dev/null
@@ -1 +0,0 @@
-export const version: string = 'v0.7.21';
index 3199bc7adce0a6e237d1921167be55d496aefd7c..597a8bb92a9dc4cd6f5e50ea0e425435f4dadc5b 100644 (file)
@@ -14,7 +14,7 @@
     "number_of_comments": "{{count}} Kommentar",
     "number_of_comments_plural": "{{count}} Kommentare",
     "remove_comment": "Kommentar löschen",
-    "communities": "Communitys",
+    "communities": "Communities",
     "users": "Benutzer",
     "create_a_community": "Eine Community anlegen",
     "create_community": "Community erstellen",
     "yes": "Ja",
     "no": "Nein",
     "powered_by": "Bereitgestellt durch",
-    "landing_0": "Lemmy ist ein <1>Link-Aggregator</1> / Reddit Alternative im <2>Fediverse</2>.<3></3>Es ist selbst-hostbar, hat live-updates von Kommentar-threads und ist winzig (<4>~80kB</4>). Federation in das ActivityPub Netzwerk ist geplant. <5></5>Dies ist eine <6>sehr frühe Beta Version</6>, und viele Features funktionieren zurzeit nicht richtig oder fehlen. <7></7>Schlage neue Features vor oder melde Bugs <8>hier.</8><9></9>Gebaut mit <10>Rust</10>, <11>Actix</11>, <12>Inferno</12>, <13>Typescript</13>.",
+    "landing": "Lemmy ist ein <1>Link-Aggregator</1> / Reddit Alternative im <2>Fediverse</2>.<3></3>Es ist selbst-hostbar, hat live-updates von Kommentar-threads und ist winzig (<4>~80kB</4>). Federation in das ActivityPub Netzwerk ist geplant. <5></5>Dies ist eine <6>sehr frühe Beta Version</6>, und viele Features funktionieren zurzeit nicht richtig oder fehlen. <7></7>Schlage neue Features vor oder melde Bugs <8>hier.</8><9></9>Gebaut mit <10>Rust</10>, <11>Actix</11>, <12>Inferno</12>, <13>Typescript</13>. <14></14> <15>Vielen Dank an unsere Mitwirkenden: </15> dessalines, Nutomic, asonix, zacanger, and iav.",
     "not_logged_in": "Nicht eingeloggt.",
     "community_ban": "Du wurdest von dieser Community gebannt.",
     "site_ban": "Du wurdest von dieser Seite gebannt",
     "messages": "Nachrichten",
     "old_password": "Letztes Passwort",
     "matrix_user_id": "Matrix Benutzer",
-    "private_message_disclaimer": "Achtung: Private Nachrichten sind in Lemmy nicht sicher. Bitte erstelle einen <1>Riot.im</1> Account für sicheren Nachrichtenverkehr.",
+    "private_message_disclaimer": "Achtung: Private Nachrichten sind in Lemmy nicht verschlüsselt. Bitte erstelle einen<1>Element.io</1> Account für sicheren Nachrichtenverkehr.",
     "send_notifications_to_email": "Sende Benachrichtigungen per Email",
     "downvotes_disabled": "Downvotes deaktiviert",
     "enable_downvotes": "Aktiviere Downvotes",
     "click_to_delete_picture": "Klicke, um das Bild zu löschen.",
     "picture_deleted": "Bild gelöscht.",
     "select_a_community": "Wähle eine Community aus",
-    "invalid_username": "Ungültiger Benutzername."
+    "invalid_username": "Ungültiger Benutzername.",
+    "bold": "fett",
+    "italic": "kursiv",
+    "subscript": "Tiefzeichen",
+    "superscript": "Hochzeichen",
+    "header": "Header",
+    "strikethrough": "durchgestrichen",
+    "quote": "Zitat",
+    "spoiler": "Spoiler",
+    "list": "Liste",
+    "not_a_moderator": "Kein Moderator.",
+    "invalid_url": "Ungültige URL.",
+    "must_login": "Du musst <1>eingeloggt oder registriert</1> sein um zu Kommentieren.",
+    "no_password_reset": "Du kannst dein Passwort ohne E-Mail nicht zurücksetzen.",
+    "cake_day_info": "Heute ist {{ creator_name }}'s cake day!",
+    "invalid_post_title": "Ungültiger Post Titel",
+    "cake_day_title": "Cake day:",
+    "what_is": "Was ist"
 }
index 4dab7c8841b9421ecd996618b24a1140a5082329..45940ccf77bdfd7db738f6d46277fc3cf9af0d9d 100644 (file)
     "verify_password": "Επαλήθευση κωδικού",
     "old_password": "Παλιός κωδικός",
     "forgot_password": "ξέχασα τον κωδικό μου",
-    "reset_password_mail_sent": "Î\9cÏ\8cλιÏ\82 Ï\83Ï\84άλθηκε Î­Î½Î± Î¼Î®Î½Ï\85μα Î·Î»ÎµÎºÏ\84Ï\81ονικοÏ\8d Ï\84αÏ\87Ï\85δÏ\81ομείοÏ\85 για την επαναφορά του κωδικού σας.",
+    "reset_password_mail_sent": "ΣÏ\84άλθηκε Î¼Î®Î½Ï\85μα Ï\83Ï\84ο Î·Î»ÎµÎºÏ\84Ï\81ονικÏ\8c Ï\84αÏ\87Ï\85δÏ\81ομείο για την επαναφορά του κωδικού σας.",
     "password_change": "Αλλαγή κωδικού",
     "new_password": "Νέος κωδικός",
-    "no_email_setup": "Αυτός ο διακομιστής δεν έχει εγκαταστήσει σωστά το email.",
+    "no_email_setup": "Αυτός ο διακομιστής δεν έχει στήσει σωστά το email.",
     "email": "Email",
     "matrix_user_id": "Χρήστης Matrix",
-    "private_message_disclaimer": "Προσοχή: τα προσωπικά μηνύματα στο Lemmy δεν είναι ασφαλή. Παρακαλούμε δημιουργήστε έναν λογαριασμό στο <1>Riot.im</1> για ασφαλή επικοινωνία.",
+    "private_message_disclaimer": "Προσοχή: τα προσωπικά μηνύματα στο Lemmy δεν είναι ασφαλή. Παρακαλούμε δημιουργήστε έναν λογαριασμό στο <1>Element.io</1> για ασφαλή επικοινωνία.",
     "send_notifications_to_email": "Αποστολή ειδοποιήσεων στη διεύθυνση ηλεκτρονικού ταχυδρομείου",
     "optional": "Προαιρετικό",
     "expires": "Λήγει",
     "transfer_site": "μεταφορά ιστότοπου",
     "are_you_sure": "είστε σίγουρος;",
     "powered_by": "Τροφοδοτείται από",
-    "landing": "Το Lemmy είναι μια <1>ιστοσελίδα συγκέντρωσης συνδέσμων</1> / εναλλακτική του reddit, προορισμένη να δουλέψει μέσα στο <2>fediverse</2>.<3></3>Μπορεί να φιλοξενηθεί σε διακομιστή οποιουδήποτε, ανανεώνει ζωντανά (live) τα σχόλια, και είναι μικροσκοπικό σε μέγεθος (<4>~80kB</4>). Η ομοσπονδίωση με το ActivityPub δίκτυο βρίσκεται υπό εξέλιξη<5></5>Αυτή είναι μια <6>πολύ πρώιμη έκδοση beta</6>, συνεπώς πολλές λειτουργίες είναι προς το παρόν αναξιόπιστες ή ανύπαρκτες. <7></7>Προτείνετε καινούριες λειτουργίες ή αναφέρετε σφάλματα <8>εδώ.</8><9></9>Γραμμένο με <10>Rust</10>, <11>Actix</11>, <12>Inferno</12>, <13>Typescript</13>.",
-    "not_logged_in": "Î\9cη Ï\83Ï\85νδεμένος.",
+    "landing": "Το Lemmy είναι μια <1>ιστοσελίδα συγκέντρωσης συνδέσμων</1> / εναλλακτική του reddit, προορισμένη να δουλέψει μέσα στο <2>fediverse</2>.<3></3>Μπορεί να φιλοξενηθεί σε διακομιστή οποιουδήποτε, ανανεώνει ζωντανά (live) τα σχόλια, και είναι μικροσκοπικό σε μέγεθος (<4>~80kB</4>). Η ομοσπονδίωση με το ActivityPub δίκτυο βρίσκεται υπό εξέλιξη<5></5>Αυτή είναι μια <6>πολύ πρώιμη έκδοση beta</6>, συνεπώς πολλές λειτουργίες είναι προς το παρόν αναξιόπιστες ή ανύπαρκτες. <7></7>Προτείνετε καινούριες λειτουργίες ή αναφέρετε σφάλματα <8>εδώ.</8><9></9>Γραμμένο με <10>Rust</10>, <11>Actix</11>, <12>Inferno</12>, <13>Typescript</13>. <14></14> <15>Ευχαριστούμε τους συνεργάτες μας: </15> dessalines, Nutomic, asonix, zacanger, και iav.",
+    "not_logged_in": "Î\91Ï\80οÏ\83Ï\85νδεδέμενος.",
     "logged_in": "Συνδεμένος.",
     "site_saved": "Ο ιστότοπος αποθηκεύτηκε.",
     "community_ban": "Έχετε αποβληθεί από αυτή την κοινότητα.",
     "site_ban": "Έχετε αποβληθεί από τον ιστότοπο",
-    "couldnt_create_comment": "Î\94εν Î¼Ï\80Ï\8cÏ\81εÏ\83ε Î½Î± Î´Î·Î¼Î¹Î¿Ï\85Ï\81γηθεί Ï\84ο Ï\83Ï\87Ï\8cλιο.",
+    "couldnt_create_comment": "Î\91δÏ\85ναμία Î´Î·Î¼Î¹Î¿Ï\85Ï\81γίαÏ\82 Ï\83Ï\87Ï\8cλιοÏ\85.",
     "couldnt_like_comment": "Δεν μπόρεσε να ψηφισθεί θετικά το σχόλιο.",
     "couldnt_update_comment": "Δεν μπόρεσε να ενημερωθεί το σχόλιο.",
     "couldnt_save_comment": "Δεν μπόρεσε να αποθηκευτεί το σχόλιο.",
     "no": "όχι",
     "top_day": "Κορυφαία σήμερα",
     "joined": "Μέλος από",
-    "couldnt_get_posts": "Î\94εν Î¼Ï\80Ï\8cÏ\81εÏ\83αν Î½Î± Î²Ï\81εθοÏ\8dν Î¿Î¹ Î´Î·Î¼Î¿Ï\83ιεÏ\8dÏ\83ειÏ\82",
-    "couldnt_update_post": "Î\94εν Î¼Ï\80Ï\8cÏ\81εÏ\83ε Î½Î± ÎµÎ½Î·Î¼ÎµÏ\81Ï\89θεί Î· Î´Î·Î¼Î¿Ï\83ίεÏ\85Ï\83η",
+    "couldnt_get_posts": "Î\91δÏ\85ναμία ÎµÏ\8dÏ\81εÏ\83ηÏ\82 Î´Î·Î¼Î¿Ï\83ιεÏ\8dÏ\83Ï\89ν",
+    "couldnt_update_post": "Î\91δÏ\85ναμία ÎµÎ½Î·Î¼Î­Ï\81Ï\89Ï\83ηÏ\82 Î´Î·Î¼Î¿Ï\83ιεÏ\8dÏ\83ηÏ\82",
     "couldnt_save_post": "Δεν μπόρεσε να αποθηκευτεί η δημοσίευση.",
     "no_slurs": "Όχι προσβολές.",
     "not_an_admin": "Ο χρήστης δεν είναι διαχειριστής.",
     "site_already_exists": "Ο ιστότοπος υπάρχει ήδη.",
-    "couldnt_update_site": "Î\94εν Î¼Ï\80Ï\8cÏ\81εÏ\83ε Î½Î± ÎµÎ½Î·Î¼ÎµÏ\81Ï\89θεί Î¿ Î¹Ï\83Ï\84Ï\8cÏ\84οÏ\80οÏ\82.",
-    "couldnt_find_that_username_or_email": "Î\94εν Î¼Ï\80Ï\8cÏ\81εÏ\83ε Î½Î± Î²Ï\81εθεί Î±Ï\85Ï\84Ï\8c Ï\84ο Ï\8cνομα Ï\87Ï\81ήÏ\83Ï\84η Î® Î· Î´Î¹ÎµÏ\8dθÏ\85νÏ\83η ηλεκτρονικού ταχυδρομείου.",
+    "couldnt_update_site": "Î\91δÏ\85ναμία ÎµÎ½Î·Î¼Î­Ï\81Ï\89Ï\83ηÏ\82 Î¹Ï\83Ï\84Ï\8cÏ\84οÏ\80οÏ\85.",
+    "couldnt_find_that_username_or_email": "Î\91δÏ\85ναμία ÎµÏ\8dÏ\81εÏ\83ηÏ\82 Ï\87Ï\81ήÏ\83Ï\84η Î® Î´Î¹ÎµÏ\8dθÏ\85νÏ\83ηÏ\82 ηλεκτρονικού ταχυδρομείου.",
     "password_incorrect": "Λάθος κωδικός.",
-    "passwords_dont_match": "Οι κωδικοί δεν ταυτίζονται.",
+    "passwords_dont_match": "Οι κωδικοί δεν ταιριάζουν.",
     "admin_already_created": "Δυστυχώς υπάρχει ήδη διαχειριστής.",
     "user_already_exists": "Ο χρήστης υπάρχει ήδη.",
     "email_already_exists": "Η διεύθυνση ηλεκτρονικού ταχυδρομείου υπάρχει ήδη.",
-    "couldnt_update_user": "Î\94εν Î¼Ï\80Ï\8cÏ\81εÏ\83ε Î½Î± ÎµÎ½Î·Î¼ÎµÏ\81Ï\89θεί Î¿ Ï\87Ï\81ήÏ\83Ï\84ηÏ\82.",
+    "couldnt_update_user": "Î\91δÏ\85ναμία ÎµÎ½Î·Î¼Î­Ï\81Ï\89Ï\83ηÏ\82 Ï\87Ï\81ήÏ\83Ï\84η.",
     "system_err_login": "Σφάλμα στο σύστημα. Προσπαθήστε να αποσυνδεθείτε και να συνδεθείτε ξανά.",
-    "couldnt_create_private_message": "Î\94εν Î¼Ï\80Ï\8cÏ\81εÏ\83ε Î½Î± Î´Î·Î¼Î¹Î¿Ï\85Ï\81γηθεί Ï\80Ï\81οÏ\83Ï\89Ï\80ικÏ\8c Î¼Î®Î½Ï\85μα.",
+    "couldnt_create_private_message": "Î\91δÏ\85ναμία Î´Î·Î¼Î¹Î¿Ï\85Ï\81γίαÏ\82 Ï\80Ï\81οÏ\83Ï\89Ï\80ικοÏ\8d Î¼Î·Î½Ï\8dμαÏ\84οÏ\82.",
     "no_private_message_edit_allowed": "Δεν επιτρέπεται η επεξεργασία του προσωπικού μηνύματος.",
     "time": "Χρόνος",
-    "couldnt_update_private_message": "Î\94εν Î¼Ï\80Ï\8cÏ\81εÏ\83ε Î½Î± ÎµÎ½Î·Î¼ÎµÏ\81Ï\89θεί Ï\84ο Ï\80Ï\81οÏ\83Ï\89Ï\80ικÏ\8c Î¼Î®Î½Ï\85μα.",
+    "couldnt_update_private_message": "Î\91δÏ\85ναμία ÎµÎ½Î·Î¼Î­Ï\81Ï\89Ï\83ηÏ\82 Ï\80Ï\81οÏ\83Ï\89Ï\80ικοÏ\8d Î¼Î·Î½Ï\8dμαÏ\84οÏ\82.",
     "action": "Δράση",
     "emoji_picker": "Διαλογέας emoji",
     "block_leaving": "Είστε σίγουρος ότι θέλετε να φύγετε;",
-    "invalid_username": "Λάθος όνομα χρήστη."
+    "invalid_username": "Λάθος όνομα χρήστη.",
+    "bold": "έμφαση",
+    "italic": "πλάγια",
+    "subscript": "δείκτης",
+    "superscript": "εκθέτης",
+    "header": "επικεφαλίδα",
+    "strikethrough": "διαγράμμιση",
+    "quote": "παράθεση",
+    "spoiler": "σπόιλερ",
+    "list": "λίστα",
+    "not_a_moderator": "Δεν είναι συντονιστής.",
+    "invalid_url": "Μη έγκυρο URL.",
+    "must_login": "Πρέπει να <1>συνδεθείτε ή να κάνετε εγγραφή</1> για να σχολιάσετε.",
+    "no_password_reset": "Δεν θα μπορέσετε να κάνετε επαναφορά του κωδικού σας χωρίς διεύθυνση ηλεκτρονικού ταχυδρομείου.",
+    "cake_day_title": "Ημέρα cake:",
+    "cake_day_info": "Σήμερα είναι ημέρα cake του {{ creator_name }}!",
+    "invalid_post_title": "Μη έγκυρη επικεφαλίδα δημοσίευσης",
+    "what_is": "Τι είναι"
 }
index 90c4a9959cb58a82560511ac2ebd465cbde28154..de1fc8268baba5bec10e0d93ae7460c3a0810b0e 100644 (file)
@@ -15,6 +15,7 @@
     "number_of_comments": "{{count}} Comment",
     "number_of_comments_plural": "{{count}} Comments",
     "remove_comment": "Remove Comment",
+    "remove_posts_comments": "Remove Posts and Comments",
     "communities": "Communities",
     "users": "Users",
     "create_a_community": "Create a community",
     "upload_image": "upload image",
     "avatar": "Avatar",
     "upload_avatar": "Upload Avatar",
+    "banner": "Banner",
+    "upload_banner": "Upload Banner",
+    "icon": "Icon",
+    "upload_icon": "Upload Icon",
     "show_avatars": "Show Avatars",
     "show_context": "Show context",
     "formatting_help": "formatting help",
     "unsticky": "unsticky",
     "link": "link",
     "archive_link": "archive link",
+    "bold": "bold",
+    "italic": "italic",
+    "subscript": "subscript",
+    "superscript": "superscript",
+    "header": "header",
+    "strikethrough": "strikethrough",
+    "quote": "quote",
+    "spoiler": "spoiler",
+    "list": "list",
     "mod": "mod",
     "mods": "mods",
     "moderates": "Moderates",
@@ -59,6 +73,7 @@
     "site_config": "Site Configuration",
     "remove_as_mod": "remove as mod",
     "appoint_as_mod": "appoint as mod",
+    "leave_mod_team": "leave mod team",
     "modlog": "Modlog",
     "admin": "admin",
     "admins": "admins",
     "number_online": "{{count}} User Online",
     "number_online_plural": "{{count}} Users Online",
     "name": "Name",
+    "name_explain": "Name – used as the identifier for the community, cannot be changed.",
+    "display_name": "Display name",
+    "display_name_explain": "Display name — shown as the title on the community's page, can be changed.",
     "title": "Title",
     "category": "Category",
     "subscribers": "Subscribers",
     "sidebar": "Sidebar",
     "sort_type": "Sort type",
     "hot": "Hot",
+    "active": "Active",
     "new": "New",
     "old": "Old",
     "top_day": "Top day",
     "email": "Email",
     "matrix_user_id": "Matrix User",
     "private_message_disclaimer":
-      "Warning: Private messages in Lemmy are not secure. Please create an account on <1>Riot.im</1> for secure messaging.",
+      "Warning: Private messages in Lemmy are not secure. Please create an account on <1>Element.io</1> for secure messaging.",
     "send_notifications_to_email": "Send notifications to Email",
     "optional": "Optional",
     "expires": "Expires",
     "landing_0":
       "Lemmy is a <1>link aggregator</1> / reddit alternative, intended to work in the <2>fediverse</2>.<3></3>It's self-hostable, has live-updating comment threads, and is tiny (<4>~80kB</4>). Federation into the ActivityPub network is on the roadmap. <5></5>This is a <6>very early beta version</6>, and a lot of features are currently broken or missing. <7></7>Suggest new features or report bugs <8>here.</8><9></9>Made with <10>Rust</10>, <11>Actix</11>, <12>Inferno</12>, <13>Typescript</13>. <14></14> <15>Thank you to our contributors: </15> dessalines, Nutomic, asonix, zacanger, and iav.",
     "not_logged_in": "Not logged in.",
+    "bio_length_overflow": "User bio cannot exceed 300 characters.",
     "logged_in": "Logged in.",
     "must_login": "You must <1>log in or register</1> to comment.",
     "site_saved": "Site Saved.",
     "couldnt_save_post": "Couldn't save post.",
     "no_slurs": "No slurs.",
     "not_an_admin": "Not an admin.",
+    "not_a_moderator": "Not a moderator.",
     "site_already_exists": "Site already exists.",
     "couldnt_update_site": "Couldn't update site.",
     "couldnt_find_that_username_or_email":
     "password_incorrect": "Password incorrect.",
     "passwords_dont_match": "Passwords do not match.",
     "no_password_reset": "You will not be able to reset your password without an email.",
+    "captcha_incorrect": "Captcha incorrect.",
+    "enter_code": "Enter Code",
     "invalid_username": "Invalid username.",
     "admin_already_created": "Sorry, there's already an admin.",
     "user_already_exists": "User already exists.",
     "what_is": "What is",
     "cake_day_title": "Cake day:",
     "cake_day_info": "It's {{ creator_name }}'s cake day today!",
-    "invalid_post_title": "Invalid post title"
+    "invalid_post_title": "Invalid post title",
+    "invalid_url": "Invalid URL.",
+    "play_captcha_audio": "Play Captcha Audio",
+    "bio": "Bio",
+    "instances": "Instances",
+    "linked_instances": "Linked Instances",
+    "none_found": "None found."
 }
index 78821bf03ff98570073dbc7d53f2f0748ed027f3..6b41d6e98ca08f32b9224910ecf89521cfff101d 100644 (file)
     "unlock": "malŝlosi",
     "lock": "ŝlosi",
     "link": "ligilo",
-    "mod": "moderanto",
-    "mods": "moderantoj",
-    "moderates": "Moderigas",
+    "mod": "reguligisto",
+    "mods": "reguligistoj",
+    "moderates": "reguligas",
     "settings": "Agordoj",
-    "remove_as_mod": "forigi per moderanto",
-    "appoint_as_mod": "nomumi per moderanto",
-    "modlog": "Moderlogo",
+    "remove_as_mod": "forigi kiel reguligisto",
+    "appoint_as_mod": "nomumi reguligisto",
+    "modlog": "Protokolo de reguligado",
     "admin": "administranto",
     "admins": "administrantoj",
     "remove_as_admin": "forigi kiel administranto",
     "appoint_as_admin": "nomumi administranto",
     "remove": "forigi",
-    "removed": "fortirita",
+    "removed": "forigita de reguligisto",
     "locked": "ŝlosita",
     "reason": "Kialo",
     "mark_as_read": "marki legita",
@@ -77,7 +77,7 @@
     "next": "Pluen",
     "sidebar": "Flankobreto",
     "sort_type": "Ordigilo",
-    "hot": "Varmaj",
+    "hot": "Furoraj",
     "new": "Novaj",
     "top_day": "Supraj tagaj",
     "week": "Semajno",
     "transfer_community": "transdoni la komunumon",
     "transfer_site": "transdoni la retejon",
     "powered_by": "Konstruita per",
-    "landing": "Lemmy estas <1>amasigilo de ligiloj</1> / alternativo de Reddit, intencita funkcii en la <2>federuniverso</2>.<3></3>ĝi estas mem-gastigebla, havas tuj-ĝisdatigojn de komentaroj, kaj estas malgrandega (<4>~80kB</4>). Federado en la reto de ActivityPub estas planita. <5></5>Ĉi tio estas <6>tre frua beta-versio</6>, kaj multaj funkcioj estas nune difektaj aŭ mankaj. <7></7>Proponu novajn funkciojn aŭ raportu erarojn <8>ĉi tie.</8><9></9>Konstruita per <10>Rust</10>, <11>Actix</11>, <12>Inferno</12>, <13>Typescript</13>.",
+    "landing": "Lemmy estas <1>amasigilo de ligiloj</1> / alternativo de Reddit, intencita funkcii en la <2>federuniverso</2>.<3></3>ĝi estas mem-gastigebla, havas tuj-ĝisdatigojn de komentaroj, kaj estas malgrandega (<4>~80kB</4>). Federado en la reto de ActivityPub estas planita. <5></5>Ĉi tio estas <6>tre frua beta-versio</6>, kaj multaj funkcioj estas nune difektaj aŭ mankaj. <7></7>Proponu novajn funkciojn aŭ raportu erarojn <8>ĉi tie.</8><9></9>Konstruita per <10>Rust</10>, <11>Actix</11>, <12>Inferno</12>, <13>Typescript</13>. <14></14> <15>Dankon al niaj kontribuintoj: </15> dessalines, Nutomic, asonix, zacanger, kaj iav.",
     "not_logged_in": "Nesalutinta.",
     "community_ban": "Vi estas forbarita de la komunumo.",
     "site_ban": "Vi estas forbarita de la retejo",
     "couldnt_find_community": "Ne povis trovi la komunumon.",
     "couldnt_update_community": "Ne povis ĝisdatigi la komunumon.",
     "community_already_exists": "Komunumo jam ekzistas.",
-    "community_moderator_already_exists": "Komunuma moderanto jam ekzistas.",
+    "community_moderator_already_exists": "Reguligisto de komunumo jam ekzistas.",
     "community_follower_already_exists": "Abonanto de komunumo jam ekzistas.",
     "community_user_already_banned": "Uzanto de komunumo jam estas forbarita.",
     "couldnt_create_post": "Ne povis krei la afiŝon.",
     "number_of_upvotes_plural": "{{count}} porvoĉoj",
     "downvote": "Kontraŭvoĉi",
     "number_of_downvotes": "{{count}} kontraŭvoĉo",
-    "number_of_downvotes_plural": "{{count}} kontraŭvoĉoj"
+    "number_of_downvotes_plural": "{{count}} kontraŭvoĉoj",
+    "what_is": "Kio estas",
+    "must_login": "Vi devas <1>saluti aŭ registriĝi</1> por komenti.",
+    "no_password_reset": "Vi ne povos restarigi vian pasvorton sen retpoŝtadreso.",
+    "cake_day_title": "Tortotago:",
+    "cake_day_info": "Hodiaŭ estas tortotago de {{ creator_name }}!",
+    "invalid_post_title": "Nevalida titolo de afiŝo"
 }
index 383fcce4d563d43f91c10a6151f1b31709462885..f24c336bcd2cc0bc145ce230105a9ebda821af04 100644 (file)
     "no_email_setup": "Este servidor no ha activado correctamente el correo.",
     "email": "Correo electrónico",
     "matrix_user_id": "Usuario Matricial",
-    "private_message_disclaimer": "Aviso: Los mensajes privados en Lemmy no son seguros. Por favor cree una cuenta en <1>Riot.im</1> para mensajeria segura.",
+    "private_message_disclaimer": "Aviso: Los mensajes privados en Lemmy no son seguros. Por favor cree una cuenta en <1>Element.io</1> para mensajería segura.",
     "send_notifications_to_email": "Enviar notificaciones al correo",
     "optional": "Opcional",
     "expires": "Expira",
     "yes": "sí",
     "no": "no",
     "powered_by": "Impulsado por",
-    "landing_0": "Lemmy es un <1>agregador de links</1> / alternativa a reddit, con la intención de funcionar en el <2>fediverso</2>.<3></3>Es alojable por uno mismo (sin necesidad de grandes compañías), tiene actualización en vivo de cadenas de comentarios, y es pequeño (<4>~80kB</4>). Federar con el sistema de redes ActivityPub forma parte de los objetivos del proyecto. <5></5>Esta es una <6>version beta muy prematura</6>, y actualmente muchas de las características están rotas o faltan. <7></7>Sugiere nuevas características o reporta errores <8>aquí</8>.<9></9>Hecho con <10>Rust</10>, <11>Actix</11>, <12>Inferno</12>, <13>Typescript</13>.",
+    "landing": "Lemmy es un <1>agregador de links</1> / alternativa a reddit, con la intención de funcionar en el <2>fediverso</2>.<3></3>Es alojable por uno mismo (sin necesidad de grandes compañías), tiene actualización en vivo de cadenas de comentarios, y es pequeño (<4>~80kB</4>). Federar con el sistema de redes ActivityPub forma parte de los objetivos del proyecto. <5></5>Esta es una <6>version beta muy prematura</6>, y actualmente muchas de las características están rotas o faltan. <7></7>Sugiere nuevas características o reporta errores <8>aquí</8>.<9></9>Hecho con <10>Rust</10>, <11>Actix</11>, <12>Inferno</12>, <13>Typescript</13>. <14> </14><15>Gracias a todos los contribuyentes: </15> dessalines, Nutomic, asonix, zacanger, and iav.",
     "not_logged_in": "No has iniciado sesión.",
     "logged_in": "Has iniciado sesión.",
     "community_ban": "Has sido expulsado de esta comunidad.",
     "invalid_username": "Nombre de usuario inválido.",
     "invalid_community_name": "Nombre no válido.",
     "click_to_delete_picture": "Pulse para eliminar la imagen.",
-    "picture_deleted": "Imagen eliminada."
+    "picture_deleted": "Imagen eliminada.",
+    "italic": "cursiva",
+    "subscript": "subíndice",
+    "superscript": "superíndice",
+    "header": "título",
+    "quote": "cita",
+    "spoiler": "spoiler",
+    "list": "lista",
+    "no_password_reset": "No podrás restablecer tu contraseña si no tienes correo.",
+    "what_is": "Cuanto es",
+    "bold": "negrita",
+    "strikethrough": "tachado",
+    "must_login": "Debes <1>iniciar sesión o registrarse</1> para hacer comentarios.",
+    "cake_day_info": "¡Hoy es el cumpleaños de {{ creator_name }}!"
 }
index 0563880dab60c3529f28873f48db2290a82933d7..bedec76b86219066f89e7ca56b45aeba57426c99 100644 (file)
@@ -46,7 +46,7 @@
     "url": "URL",
     "chat": "Txata",
     "your_site": "zure gunea",
-    "nsfw": "NSFW (eduki hunkigarria)",
+    "nsfw": "NSFW (eduki hunkigarriak)",
     "block_leaving": "Ziur al zaude atera nahi duzula?",
     "bitcoin": "Bitcoin",
     "ethereum": "Ethereum",
     "remove_community": "Ezabatu komunitatea",
     "subscribed_to_communities": "<1>Komunitateetara</1> harpidetuta",
     "trending_communities": "<1>Komunitateen</1> joerak",
-    "list_of_communities": "Komunitate-zerrenda",
+    "list_of_communities": "Komunitateen zerrenda",
     "community_reqs": "Letra xehez, azpimarratuta eta hutsunerik gabe.",
     "create_private_message": "Sortu mezu pribatua",
     "cancel": "Ezeztatu",
     "stickied": "finkatuta",
     "reason": "Arrazoia",
     "mark_as_read": "markatu irakurrita gisa",
-    "deleted": "sortzaileak ezabatua",
+    "deleted": "sortzaileak ezabatu du",
     "delete_account_confirm": "Abisua: honek zure datu guztiak betirako ezabatu ditu. Sartu zure pasahitza baieztatzeko.",
     "restore": "leheneratu",
     "unban_from_site": "kendu debekua gunean",
     "reset_password_mail_sent": "Eposta bat bidali da zure pasahitza berrezarri dezazun.",
     "no_email_setup": "Zerbitzari honek ez du eposta ondo konfiguraturik.",
     "matrix_user_id": "Matrix erabiltzailea",
-    "private_message_disclaimer": "Abisua: Lemmyko mezu pribatuak ez dira seguruak. Mesedez, sortu kontu bat <1>Riot.im</1>en mezu seguruak trukatzeko.",
+    "private_message_disclaimer": "Abisua: Lemmyko mezu pribatuak ez dira seguruak. Mesedez, sortu kontu bat <1>Element.io</1>n mezu seguruak trukatzeko.",
     "send_notifications_to_email": "Bidali jakinarazpenak epostara",
     "optional": "Hautazkoa",
     "browser_default": "Nabigatzaileko lehenetsia",
     "number_of_upvotes_plural": "{{count}} aldeko bozka",
     "open_registration": "Izen-ematea irekia",
     "registration_closed": "Izen-ematea itxira",
-    "enable_nsfw": "Gaitu NSFW (eduki hunkigarria)",
+    "enable_nsfw": "Gaitu NSFW (eduki hunkigarriak)",
     "body": "Gorputza",
     "copy_suggested_title": "kopiatu iradokitako izenburua: {{title}}",
     "community": "Komunitatea",
     "lemmy_instance_setup": "Lemmy instantziaren ezarpena",
     "setup_admin": "Ezarri gunearen administratzailea",
     "modified": "aldatuta",
-    "show_nsfw": "Erakutsi eduki hunkigarria (NSFW)",
+    "show_nsfw": "Erakutsi eduki hunkigarriak (NSFW)",
     "expires": "Noiz iraungitzen da:",
     "theme": "Itxura",
     "sponsors": "Babesleak",
     "support_on_open_collective": "OpenCollective bitartez lagundu",
     "donate_to_lemmy": "Egin dohaintza bat Lemmyri",
     "donate": "Dohaintza egin",
-    "general_sponsors": "Babesle orokorrak Lemmyri 10 eta 39 dolar artean eman zizkiotenak dira.",
-    "silver_sponsors": "Zilarrezko babesleak Lemmyri 40 dolar eman zizkiotenak dira.",
+    "general_sponsors": "Babesle orokorrak Lemmyri 10 eta 39 dolar artean eman dizkiotenak dira.",
+    "silver_sponsors": "Zilarrezko babesleak Lemmyri 40 dolar eman dizkiotenak dira.",
     "crypto": "Kriptomonetak",
     "code": "Kodea",
     "joined": "Batuta",
-    "by": "egilea",
-    "to": "nori",
+    "by": "egilea:",
+    "to": "non:",
     "from": "nork",
     "transfer_community": "transferentzia-komunitatea",
     "transfer_site": "transferentzia-gunea",
     "are_you_sure": "ziur al zaude?",
-    "powered_by": "Egilea",
-    "landing": "Lemmy <1>esteka-agregatzailea</1> / reddit-en ordezkoa da, eta <2>fedibertsoan</2> lan egiteko sortua da. <3></3>Norberak ostatu dezake, iruzkin-hari eguneratuak ditu eta txikia da (<4>~80kB</4>). ActivityPub sareko federazioa bide-orrian dago. <5></5>Hau <6>beta bertsio goiztiarra</6> da eta funtzionalitate asko hautsita edo egin gabe ditu oraindik. <7></7>Iradoki itzazu funtzionalitate berriak edo jakinarazi akatsak <8>hemen</8>.<9></9><10>Rust</10>, <11>Actix</11>, <12>Inferno</12> eta <13>Typescript</13>ekin egina.",
+    "powered_by": "Egilea:",
+    "landing": "Lemmy <1>esteka-agregatzailea</1> / reddit-en ordezkoa da, eta <2>fedibertsoan</2> lan egiteko sortua da. <3></3>Norberak ostatu dezake, iruzkin-hari eguneratuak ditu eta txikia da (<4>~80kB</4>). ActivityPub sareko federazioa bide-orrian dago. <5></5>Hau <6>beta bertsio goiztiarra</6> da eta funtzionalitate asko hautsita edo egin gabe ditu oraindik. <7></7>Iradoki itzazu funtzionalitate berriak edo jakinarazi akatsak <8>hemen</8>.<9></9><10>Rust</10>, <11>Actix</11>, <12>Inferno</12> eta <13>Typescript</13>ekin egina. <14></14> <15>Eskerrak ematen dizkiegu gure laguntzaileei: </15> dessalines, Nutomic, asonix, zacanger eta iav.",
     "logged_in": "Saioa hasi duzu.",
     "not_logged_in": "Ez duzu saiorik hasi.",
     "site_saved": "Gunea gorde da.",
     "couldnt_update_private_message": "Ezin izan da mezu pribatu hori eguneratu.",
     "emoji_picker": "Emoji hautagailua",
     "invalid_username": "Erabiltzaile-izen baliogabea.",
-    "what_is": "Zer da"
+    "what_is": "Zenbat da",
+    "bold": "lodia",
+    "italic": "etzana",
+    "subscript": "Azpi-indizea",
+    "superscript": "Goi-indizea",
+    "header": "goiburua",
+    "quote": "aipua",
+    "strikethrough": "marratua",
+    "list": "zerrenda",
+    "spoiler": "spoiler",
+    "not_a_moderator": "Ez zara moderatzailea.",
+    "invalid_url": "URL baliogabea.",
+    "must_login": "<1>Saioa hasi edo izena eman</1> behar duzu iruzkinak egiteko.",
+    "no_password_reset": "Ezingo duzu zure pasahitza berrezarri epostarik ez baduzu.",
+    "invalid_post_title": "Bidalketa izenburu baliogabea",
+    "cake_day_title": "Izen-emate eguna:",
+    "cake_day_info": "{{ creator_name }}(e)ren urtebetetzea da gaur!"
 }
index f9e9bbd33eac0b8804c037a3e27ad15837d656be..26668fdcc184f5c205f5ae43f9b9640c92ffe129 100644 (file)
@@ -28,7 +28,7 @@
     "create_private_message": "Luo yksityisviesti",
     "send_secure_message": "Lähetä suojattu viesti",
     "send_message": "Lähetä viesti",
-    "message": "Viesti",
+    "message": "Lähetä",
     "edit": "muokkaa",
     "reply": "vastaa",
     "cancel": "Peru",
     "mods": "moderaattorit",
     "moderates": "Moderoi",
     "settings": "Asetukset",
-    "remove_as_mod": "Poista moderaattorina",
+    "remove_as_mod": "Poista moderaattorin asemasta",
     "appoint_as_mod": "Nimitä moderaattoriksi",
     "modlog": "Moderoinnin loki",
     "admin": "ylläpitäjä",
     "admins": "ylläpitäjät",
-    "remove_as_admin": "poista ylläpitäjänä",
+    "remove_as_admin": "poista ylläpitäjän asemasta",
     "appoint_as_admin": "nimitä ylläpitäjäksi",
     "remove": "poista",
     "removed": "poistettu",
     "login_sign_up": "Kirjaudu sisään / Rekisteröidy",
     "login": "Kirjaudu sisään",
     "sign_up": "Rekisteröidy",
-    "notifications_error": "Työpöydän ilmoitukset eivät ole saatavilla selaimellesi. Yritä Firefoxia tai Chromea.",
+    "notifications_error": "Työpöydän ilmoitukset eivät ole saatavilla selaimellesi. Kokeile Firefoxilla tai Chromella.",
     "unread_messages": "Lukemattomat viestit",
     "messages": "Viestit",
     "password": "Salasana",
     "theme": "Teema",
     "sponsors": "Sponsorit",
     "sponsors_of_lemmy": "Lemmy-sponsorit",
-    "sponsor_message": "Lemmy on vapaa, <1>avoimen lähdekoodin</1> -ohjelmisto, eli mainontaa, rahantekemistä, tai pääomasijoitusta täällä ei tule ikinä olemaan. Lahjoituksesi tukevat suoraan projektin täysipäiväistä kehitystä. Kiitokset seuraaville ihmisille:",
+    "sponsor_message": "Lemmy on vapaa, <1>avoimen lähdekoodin</1> -ohjelmisto, eli tällä ei tulla tekemään rahaa. Lahjoituksesi tukevat suoraan projektin täysipäiväistä kehitystä. Kiitokset seuraaville lahjoittajille:",
     "support_on_patreon": "Tue Patreonissa",
     "donate_to_lemmy": "Lahjoita Lemmylle",
     "donate": "Lahjoita",
-    "general_sponsors": "Yleisiä sponsoreja ovat he, jotka lupaavat 10-39 dollaria Lemmylle.",
+    "general_sponsors": "Yleiset sponsorit lupaavat 10-39 dollaria Lemmylle.",
     "crypto": "Krypto",
     "bitcoin": "Bitcoin",
     "ethereum": "Ethereum",
     "monero": "Monero",
-    "code": "Code",
+    "code": "Lähdekoodi",
     "joined": "Liittyi",
     "by": "käyttäjältä",
     "to": "yhteisössä",
     "from": "paikasta",
-    "transfer_community": "siirron yhteisö",
-    "transfer_site": "siirron määrä",
+    "transfer_community": "siirrä yhteisö",
+    "transfer_site": "siirrä sivusto",
     "are_you_sure": "oletko varma?",
     "yes": "kyllä",
     "no": "ei",
     "powered_by": "Vauhdittajana",
-    "landing_0": "Lemmy on <1>linkinkerääjä</1> / Reddit-vaihtoehto, tarkoitettu toimimaan <2>fediversessä</2>.<3></3>Sitä voi isännöidä itse, siinä on tosiaikaisesti päivittyvät kommenttiketjut, ja se on pieni (<4>~80 kilotavua</4>). Federointi ActivityPub-verkkoon on suunnittelun alla. <5></5>Tämä on <6>hyvin varhainen betaversio</6>, ja monet ominaisuudet ovat toistaiseksi rikki tai poissa. <7></7>Ehdota uusia ominaisuuksia tai raportoi bugeja <8>tänne.</8><9></9>Tehty teknologioilla <10>Rust</10>, <11>Actix</11>, <12>Inferno</12>, <13>Typescript</13>.",
+    "landing_0": "Lemmy on <2>fediversessä</2> toimiva <1>linkinkerääjä</1> / Reddit-vaihtoehto.<3></3>Sitä voi ylläpitää itse, siinä on tosiaikaisesti päivittyvät kommenttiketjut, ja se on pieni (<4>~80 kilotavua</4>). Federointi ActivityPub-verkkoon on suunnittelun alla. <5></5>Tämä on <6>hyvin varhainen betaversio</6>, ja monet ominaisuudet ovat toistaiseksi rikki tai poissa. <7></7>Ehdota uusia ominaisuuksia tai raportoi bugeja <8>tänne.</8><9></9>Tehty teknologioilla <10>Rust</10>, <11>Actix</11>, <12>Inferno</12> ja <13>Typescript</13>. Kiitoksia projektin kehitykseen osallistuneille käyttäjille dessalines, Nutomic, asonix, zacanger ja iav.",
     "not_logged_in": "Ei kirjautunut sisään.",
     "logged_in": "Kirjautunut sisään.",
     "community_ban": "Sinulle on asetettu porttikielto tähän yhteisöön.",
-    "site_ban": "Sinut on asetettu porttikieltoon tältä sivustolta",
+    "site_ban": "Sinut on asetettu porttikieltoon tällä sivustolla",
     "couldnt_create_comment": "Kommenttia ei pystytty luomaan.",
     "couldnt_like_comment": "Kommentista ei voitu tykätä.",
     "couldnt_update_comment": "Kommenttia ei voitu päivittää.",
     "couldnt_find_that_username_or_email": "Käyttäjänimeä tai sähköpostia ei onnistuttu löytämään.",
     "password_incorrect": "Salasana on väärin.",
     "passwords_dont_match": "Salasanat eivät täsmää.",
+       "no_password_reset": "Et voi nollata salasanaasi ilman sähköpostia.",
     "admin_already_created": "Anteeksi, mutta täällä on jo ylläpitäjä.",
     "user_already_exists": "Käyttäjä on jo olemassa.",
     "email_already_exists": "Sähköposti on jo olemassa.",
     "no_private_message_edit_allowed": "Sinulla ei ole oikeutta muokata yksityisviestiä.",
     "couldnt_update_private_message": "Yksityisviestiä ei voitu päivittää.",
     "more": "lisää",
-    "cross_posted_to": "ristipostattu: ",
+    "cross_posted_to": "jaettu ristiin: ",
     "sorting_help": "apua lajitteluun",
     "show_context": "Näytä yhteys",
     "admin_settings": "Ylläpitäjän asetukset",
     "block_leaving": "Haluatko varmasti poistua?",
     "silver_sponsors": "Hopeasponsoreita ovat ne, jotka lupaavat 40 dollaria Lemmylle.",
     "post_title_too_long": "Viestin otsikko on liian pitkä.",
-    "support_on_open_collective": "Tie OpenCollectivessa",
-    "site_saved": "Sivu tallennettu."
+    "support_on_open_collective": "Tue OpenCollectivessa",
+    "site_saved": "Sivu tallennettu.",
+       "what_is": "Mikä on",
+       "cake_day_title": "Kakkupäivä:",
+    "cake_day_info": "Tänään on käyttäjän {{ creator_name }} kakkupäivä!",
+       "invalid_post_title": "Väärä viestin otsikko"
 }
index 769863c1f42ef6f8b8de5954ad415278c482904b..b24f28056637f41d3217afdde83a3a1d3c96c8df 100644 (file)
     "yes": "oui",
     "no": "non",
     "powered_by": "Propulsé par",
-    "landing": "Lemmy est un <1>aggrégateur de liens</1>, similaire à Reddit et conçu pour fonctionner sur le <2>Fédivers</2>.<3></3>Il est auto-hébergeable, se met à jour en direct et est léger (<4>~80kB</4>). La fédération via ActivityPub est prévue dans sa feuille de route. <5></5>Lemmy est une <6>version beta très précoce</6> et de nombreuses fonctionnalités sont manquantes ou non fonctionnelles. <7></7>Vous pouvez signaler des bugs et suggérer de nouvelles fonctionnalités <8>ici.</8><9></9>Créé avec <10>Rust</10>, <11>Actix</11>, <12>Inferno</12>, <13>Typescript</13>.",
+    "landing": "Lemmy est un <1>aggrégateur de liens</1>, similaire à Reddit et conçu pour fonctionner sur le <2>Fédivers</2>.<3></3>Il est auto-hébergeable, se met à jour en direct et est léger (<4>~80kB</4>). La fédération via ActivityPub est prévue dans sa feuille de route. <5></5>Lemmy est une <6>version beta très précoce</6> et de nombreuses fonctionnalités sont manquantes ou non fonctionnelles. <7></7>Vous pouvez signaler des bugs et suggérer de nouvelles fonctionnalités <8>ici.</8><9></9>Créé avec <10>Rust</10>, <11>Actix</11>, <12>Inferno</12>, <13>Typescript</13>. <14></14> <15>Merci à nos contributeurs : </15> dessalines, Nutomic, asonix, zacanger, et iav.",
     "not_logged_in": "Vous n’êtes pas connecté.",
     "logged_in": "Vous êtes connecté.",
     "community_ban": "Vous avez été banni de cette communauté.",
     "invalid_username": "Nom d'utilisateur invalide.",
     "invalid_community_name": "Nom invalide.",
     "click_to_delete_picture": "Cliquer pour supprimer l'image.",
-    "picture_deleted": "Image supprimée."
+    "picture_deleted": "Image supprimée.",
+    "invalid_post_title": "Titre du post invalide",
+    "must_login": "Vous devez vous être <1>connecté ou enregistré</1> pour commenter.",
+    "no_password_reset": "Vous ne pourrez pas réinitialiser votre mot de passe sans un e-mail.",
+    "what_is": "Combien font",
+    "cake_day_title": "Lemmyversaire :"
 }
index 0967ef424bce6791893e9a57bb952f80fd536e93..943fea62f209c099ccf50258c236d406c68b0ea5 100644 (file)
@@ -1 +1,304 @@
-{}
+{
+    "bold": "trom",
+    "italic": "iodálach",
+    "header": "ceanntásc",
+    "strikethrough": "stailc tríd",
+    "quote": "ceanglófar",
+    "spoiler": "milleadh scéil",
+    "list": "liosta",
+    "admin_settings": "Socruithe Riaracháin",
+    "site_config": "Cumraíocht Suímh",
+    "remove_as_mod": "bhaint mar modhnóir",
+    "appoint_as_mod": "ceapachán mar mhodhnóir",
+    "locked": "glasáilte",
+    "stickied": "bioráin",
+    "reason": "Cúis",
+    "mark_as_read": "marc mar a léitear",
+    "mark_as_unread": "marc mar neamhléite",
+    "delete": "scriosadh",
+    "deleted": "scriosta ag cruthaitheoir",
+    "delete_account": "Scrios Cuntas",
+    "click_to_delete_picture": "Cliceáil chun pictiúr a scriosadh.",
+    "picture_deleted": "Scriosadh an pictiúr.",
+    "restore": "athchóirigh",
+    "ban": "cosc",
+    "ban_from_site": "cosc ón suíomh",
+    "unban": "Cealaigh cosc",
+    "unban_from_site": "Cealaigh cosc ón suíomh",
+    "banned": "coisceadh",
+    "banned_users": "Úsáideoirí Coisceadh",
+    "save": "sábháil",
+    "unsave": "cealaigh sábháil",
+    "create": "cruthaigh",
+    "creator": "cruthaitheoir",
+    "email_or_username": "Ríomhphost nó Ainm Úsáideora",
+    "number_online_0": "{{count}} Úsáideoir Ar Líne",
+    "number_online_1": "{{count}} Úsáideoirí Ar Líne",
+    "number_online_2": "{{count}} Úsáideoirí Ar Líne",
+    "number_online_3": "{{count}} Úsáideoirí Ar Líne",
+    "number_online_4": "{{count}} Úsáideoirí Ar Líne",
+    "name": "Ainm",
+    "title": "Teideal",
+    "subscribers": "Síntiúsóirí",
+    "both": "Araon",
+    "saved": "Coinníodh",
+    "unsubscribe": "Díliostáil",
+    "subscribe": "Liostáil",
+    "subscribed": "Suibscríofa",
+    "prev": "Roimhe",
+    "next": "Chéad Eile",
+    "hot": "Te",
+    "new": "Nua",
+    "old": "Sean",
+    "top_day": "Lá barr",
+    "week": "Seachtain",
+    "month": "Mí",
+    "year": "Bliain",
+    "all": "Gach",
+    "top": "Barr",
+    "api": "API",
+    "docs": "Doic",
+    "inbox": "Bosca Isteach",
+    "inbox_for": "Bosca Isteach do <1>{{user}}</1>",
+    "mark_all_as_read": "marcáil gach rud mar atá léite",
+    "type": "Cineál",
+    "unread": "Gan léamh",
+    "replies": "Freagraí",
+    "mentions": "Luann",
+    "reply_sent": "Freagra seolta",
+    "message_sent": "Teachtaireacht seolta",
+    "search": "Cuardaigh",
+    "overview": "Forbhreathnú",
+    "view": "Amharc",
+    "logout": "Logáil Amach",
+    "login_sign_up": "Logáil Isteach / Cláraigh",
+    "login": "Logáil Isteach",
+    "unread_messages": "Teachtaireachtaí Neamhléite",
+    "messages": "Teachtaireachtaí",
+    "password": "Pasfhocal",
+    "verify_password": "Deimhnigh Pasfhocal",
+    "old_password": "Sean Pasfhocal",
+    "forgot_password": "Dearmad ar pasfhocal",
+    "reset_password_mail_sent": "Seol Ríomhphost chun do phasfhocal a athshocrú.",
+    "password_change": "Athrú Pasfhocal",
+    "new_password": "Focal Faire Nua",
+    "no_email_setup": "Níl ríomhphost curtha ar bun i gceart ag an bhfreastalaí seo.",
+    "email": "Ríomhphost",
+    "send_notifications_to_email": "Seol fógraí chuig Ríomhphost",
+    "expires": "In éag",
+    "joined": "Teanga",
+    "by": "le",
+    "to": "chun",
+    "from": "ó",
+    "transfer_community": "aistrithe pobal",
+    "transfer_site": "aistrithe suíomh",
+    "number_of_comments_0": "{{count}} Trácht",
+    "number_of_comments_1": "{{count}} Tráchtanna",
+    "number_of_comments_2": "{{count}} Tráchtanna",
+    "number_of_comments_3": "{{count}} Tráchtanna",
+    "number_of_comments_4": "{{count}} Tráchtanna",
+    "send_secure_message": "Seol Teachtaireacht Sábháilte",
+    "delete_account_confirm": "Rabhadh: scriosfaidh sé seo do chuid sonraí go buan. Iontráil do phasfhocal le deimhniú.",
+    "username": "Ainm Úsáideora",
+    "number_of_subscribers_0": "{{count}} Suibscríobhaí",
+    "number_of_subscribers_1": "{{count}} Síntiúsóirí",
+    "number_of_subscribers_2": "{{count}} Síntiúsóirí",
+    "number_of_subscribers_3": "{{count}} Síntiúsóirí",
+    "number_of_subscribers_4": "{{count}} Síntiúsóirí",
+    "sign_up": "Cláraigh",
+    "notifications_error": "Níl faisnéisí deisce ar fáil i do bhrabhsálaí. Bain triail as Firefox nó Chrome.",
+    "private_message_disclaimer": "Rabhadh: Níl teachtaireachtaí príobháideacha i Lemmy slán. Cruthaigh cuntas ar <1>Element.io</1> le haghaidh teachtaireachtaí slán.",
+    "landing": "Is <1>comhiomlánóir nasc</1> / reddit malartach é Lemmy atá beartaithe le bheith ag obair sa <2>fediverse </2>.<3> </3> Tá sé féin-hostable, tá snáitheanna tráchta ann a nuashonraíonn beo, agus beag bídeach (<4> ~ 80kB </4>). Cónaidhm isteach sa líonra ActivityPub tá sé ar an treochlár. <5> </5> Seo <6> leagan béite an-luath </6>, agus tá a lán gnéithe briste nó in easnamh faoi láthair. <7> </7> Mol gnéithe nua nó tuairiscigh fabhtanna <8> áit. </8> <9> </9> Déanta le <10> Meirge </10>, <11> Actix </11>, < 12> Inferno </12>, <13> Clóscríbhneoireacht </13>. <14> </14> <15> Go raibh maith agat dár rannpháirtithe: </15> dessalines, Nutomic, asonix, zacanger, agus iav.",
+    "post": "postáil",
+    "remove_post": "Postáil a Bhaint",
+    "no_posts": "Gan aon Postáil.",
+    "create_a_post": "Cruthaigh Postáil",
+    "create_post": "Cruthaigh Postáil",
+    "number_of_posts_0": "{{count}} Postáil",
+    "number_of_posts_1": "{{count}} Postálacha",
+    "number_of_posts_2": "{{count}} Postálacha",
+    "number_of_posts_3": "{{count}} Postálacha",
+    "number_of_posts_4": "{{count}} Postálacha",
+    "posts": "Postálacha",
+    "related_posts": "D’fhéadfadh baint a bheith ag na Poist seo",
+    "cross_posts": "Cuireadh an nasc seo sa phost freisin chuig:",
+    "cross_post": "tras-phost",
+    "cross_posted_to": "tras-phostáilte chuig: ",
+    "comments": "Tráchtanna",
+    "remove_comment": "Bain Trácht",
+    "communities": "Pobail",
+    "users": "Úsáideoirí",
+    "create_a_community": "Cruthaigh Pobal",
+    "select_a_community": "Roghnaigh Pobal",
+    "create_community": "Cruthaigh Pobal",
+    "remove_community": "Bain Pobal",
+    "subscribed_to_communities": "Suibscríofa le <1>pobail</1>",
+    "trending_communities": "Treocht <1>pobail</1>",
+    "list_of_communities": "Liosta na bPobal",
+    "community_reqs": "cás íochtair, fostríoc, agus gan aon spásanna.",
+    "number_of_communities_0": "{{count}} Pobal",
+    "number_of_communities_1": "{{count}} Pobail",
+    "number_of_communities_2": "{{count}} Pobail",
+    "number_of_communities_3": "{{count}} Pobail",
+    "number_of_communities_4": "{{count}} Pobail",
+    "invalid_community_name": "Ainm neamhbhailí.",
+    "create_private_message": "Cruthaigh Teachtaireacht Phríobháideach",
+    "send_message": "Seol Teachtaireacht",
+    "message": "Teachtaireacht",
+    "edit": "cuir in eagar",
+    "reply": "freagra",
+    "more": "tuilleadh",
+    "cancel": "Cealú",
+    "preview": "Réamhléiriú",
+    "upload_image": "íomhá a uaslódáil",
+    "avatar": "Abhatár",
+    "upload_avatar": "Uaslódáil Abhatár",
+    "show_avatars": "Taispeáin Abhatáranna",
+    "show_context": "Taispeáin comhthéacs",
+    "formatting_help": "formáidiú cabhrú",
+    "sorting_help": "cúnamh a shórtáil",
+    "view_source": "féachaint foinse",
+    "unlock": "dhíghlasáil",
+    "lock": "glas",
+    "sticky": "bioráin",
+    "unsticky": "cealaigh bioráin",
+    "link": "nasc",
+    "archive_link": "nasc cartlainne",
+    "mod": "modhnóir",
+    "mods": "modhnóirí",
+    "moderates": "Modhnóireacht",
+    "settings": "Socruithe",
+    "modlog": "Logamod",
+    "admin": "riarthóir",
+    "admins": "riarthóirí",
+    "remove_as_admin": "bhaint mar riarthóir",
+    "appoint_as_admin": "ceapachá mar riarthóir",
+    "remove": "bain",
+    "removed": "bainte ag an modhnóir",
+    "category": "Catagóir",
+    "sidebar": "Barrataobh",
+    "sort_type": "Cineál sórtála",
+    "matrix_user_id": "Úsáideoir Matrix",
+    "optional": "Roghnach",
+    "are_you_sure": "An bhfuil tú cinnte?",
+    "yes": "tá",
+    "no": "níl",
+    "powered_by": "Cumhachtaithe ag",
+    "not_logged_in": "Ní logáilte isteach.",
+    "logged_in": "Logáilte isteach.",
+    "number_of_users_0": "{{count}} Úsáideoir",
+    "number_of_users_1": "{{count}} Úsáideoirí",
+    "number_of_users_2": "{{count}} Úsáideoirí",
+    "number_of_users_3": "{{count}} Úsáideoirí",
+    "number_of_users_4": "{{count}} Úsáideoirí",
+    "number_of_points_0": "{{count}} Pointe",
+    "number_of_points_1": "{{count}} Pointí",
+    "number_of_points_2": "{{count}} Pointí",
+    "number_of_points_3": "{{count}} Pointí",
+    "number_of_points_4": "{{count}} Pointí",
+    "subscribe_to_communities": "Liostáil le roinnt <1>pobail</1>.",
+    "chat": "Comhrá",
+    "recent_comments": "Tráchtanna le Déanaí",
+    "no_results": "Gan torthaí.",
+    "setup": "Cumraigh",
+    "lemmy_instance_setup": "Lemmy Ásc Cumraigh",
+    "setup_admin": "Riarthóir Suímh a Bhunú",
+    "your_site": "do suíomh",
+    "modified": "modhnaithe",
+    "sponsors": "Urraitheoirí",
+    "sponsors_of_lemmy": "Urraitheoirí Lemmy",
+    "sponsor_message": "Tá Lemmy saor in aisce, <1>bogearraí foinse oscailte</1>, gan aon fhógraíocht, monetizing, ná caipiteal fiontair, riamh. Tacaíonn do shíntiúis go díreach le forbairt lánaimseartha an tionscadail. Go raibh maith agat do na daoine seo a leanas:",
+    "support_on_patreon": "Tacaíocht ar Patreon",
+    "support_on_liberapay": "Tacaíocht ar Liberapay",
+    "support_on_open_collective": "Tacaíocht ar OpenCollective",
+    "donate_to_lemmy": "Bronn do Lemmy",
+    "donate": "Bronn",
+    "general_sponsors": "Is iad Urraitheoirí Ginearálta iad siúd a gheall $10 go $39 chun Lemmy.",
+    "silver_sponsors": "Is iad Urraitheoirí Airgid iad siúd a gheall $ 40 chun Lemmy.",
+    "crypto": "Criptea",
+    "bitcoin": "Bonn Giotáin",
+    "ethereum": "Ethereum",
+    "monero": "Monero",
+    "code": "Cód",
+    "language": "Teanga",
+    "body": "Corp",
+    "copy_suggested_title": "cóip teideal molta: {{title}}",
+    "community": "Pobal",
+    "expand_here": "Leathnaigh anseo",
+    "browser_default": "Réamhshocrú Brabhsálaí",
+    "downvotes_disabled": "Síosvótaí faoi mhíchumas",
+    "enable_downvotes": "Cumasaigh Síosvótaí",
+    "open_registration": "Clárú Oscailte",
+    "registration_closed": "Clárú dúnta",
+    "enable_nsfw": "Cumasaigh NSFW",
+    "must_login": "Caithfidh tú <1>logáil isteach nó clárú</1> chun trácht a dhéanamh.",
+    "community_ban": "Cuireadh cosc ort ón bpobal seo.",
+    "site_ban": "Cuireadh cosc ort ón suíomh",
+    "couldnt_create_comment": "Níorbh fhéidir a chruthú trácht.",
+    "couldnt_like_comment": "Níorbh fhéidir a is maith trácht.",
+    "couldnt_update_comment": "Níorbh fhéidir trácht a nuashonrú.",
+    "couldnt_save_comment": "Níorbh fhéidir trácht a shábháil.",
+    "couldnt_get_comments": "Níorbh fhéidir tuairimí a fháil.",
+    "no_community_edit_allowed": "Ní cheadaítear an pobal a chur in eagar.",
+    "couldnt_find_community": "Níorbh fhéidir Pobal a aimsiú.",
+    "couldnt_update_community": "Níorbh fhéidir an Pobal a nuashonrú.",
+    "community_already_exists": "Pobal ann cheana féin.",
+    "community_moderator_already_exists": "Tá modhnóir pobail ann cheana féin.",
+    "community_follower_already_exists": "Tá leantóir pobail ann cheana féin.",
+    "community_user_already_banned": "Toirmisctear úsáideoir pobail cheana féin.",
+    "couldnt_create_post": "Níorbh fhéidir postáil a chruthú.",
+    "post_title_too_long": "Tá teideal an postáil ró-fhada.",
+    "couldnt_like_post": "Níorbh fhéidir a is maith post.",
+    "couldnt_find_post": "Níorbh fhéidir an post a aimsiú.",
+    "couldnt_get_posts": "Níorbh fhéidir an post a fháil",
+    "couldnt_update_post": "Níorbh fhéidir an post a nuashonrú",
+    "not_a_moderator": "Ní modhnóir.",
+    "system_err_login": "Earráid chórais. Bain triail as logáil amach agus ar ais isteach.",
+    "couldnt_create_private_message": "Níorbh fhéidir teachtaireacht phríobháideach a chruthú.",
+    "couldnt_update_private_message": "Níorbh fhéidir teachtaireacht phríobháideach a nuashonrú.",
+    "action": "Gníomh",
+    "emoji_picker": "Piocálaí Emoji",
+    "block_leaving": "An bhfuil tú cinnte gur mhaith leat imeacht?",
+    "what_is": "Cád é",
+    "cake_day_title": "Lá císte:",
+    "cake_day_info": "Lá císte {{ creator_name }} é atá ann inniu!",
+    "invalid_post_title": "Teideal poist neamhbhailí",
+    "invalid_url": "URL neamhbhailí.",
+    "couldnt_find_that_username_or_email": "Níorbh fhéidir an t-ainm úsáideora nó an ríomhphost sin a fháil.",
+    "admin_already_created": "Tá brón orm, tá riarthóir ann cheana féin.",
+    "number_of_downvotes_0": "{{count}} Síosvótáil",
+    "number_of_downvotes_1": "{{count}} Síosvótaí",
+    "number_of_downvotes_2": "{{count}} Síosvótaí",
+    "number_of_downvotes_3": "{{count}} Síosvótaí",
+    "number_of_downvotes_4": "{{count}} Síosvótaí",
+    "upvote": "Suasvótáil",
+    "downvote": "Síosvótáil",
+    "url": "URL",
+    "number_of_upvotes_0": "{{count}} Suasvótáil",
+    "number_of_upvotes_1": "{{count}} Suasvótaí",
+    "number_of_upvotes_2": "{{count}} Suasvótaí",
+    "number_of_upvotes_3": "{{count}} Suasvótaí",
+    "number_of_upvotes_4": "{{count}} Suasvótaí",
+    "nsfw": "NSFW",
+    "show_nsfw": "Taispeáin ábhar NSFW",
+    "theme": "Téama",
+    "site_saved": "Sábháil Suíomh.",
+    "no_private_message_edit_allowed": "Ní cheadaítear teachtaireacht phríobháideach a chur in eagar.",
+    "no_comment_edit_allowed": "Ní cheadaítear trácht a chur in eagar.",
+    "no_post_edit_allowed": "Ní cheadaítear an post a chur in eagar.",
+    "couldnt_save_post": "Níorbh fhéidir an post a shábháil.",
+    "no_slurs": "Uimh masla.",
+    "not_an_admin": "Ní riarthóir é.",
+    "site_already_exists": "Suíomh ann cheana.",
+    "couldnt_update_site": "Níorbh fhéidir an suíomh a nuashonrú.",
+    "password_incorrect": "Pasfhocal mícheart.",
+    "passwords_dont_match": "Ní hionann pasfhocail.",
+    "no_password_reset": "Ní bheidh tú in ann do phasfhocal a athshocrú gan ríomhphost.",
+    "invalid_username": "Ainm Úsáideora neamhbhailí.",
+    "user_already_exists": "Úsáideoir ann cheana.",
+    "email_already_exists": "Tá ríomhphost ann cheana féin.",
+    "couldnt_update_user": "Níorbh fhéidir an t-úsáideoir a nuashonrú.",
+    "time": "Am",
+    "subscript": "fo-script",
+    "superscript": "sár-script"
+}
index 0ff126dec24b3eebf6a3bad9a77f6d29a13eba16..b2c890c78ffb95fd66ed6bc907026d95c4f598ad 100644 (file)
@@ -84,7 +84,7 @@
     "category": "Categoria",
     "subscribers": "Iscritti",
     "both": "Entrambi",
-    "saved": "Salvato",
+    "saved": "Salvati",
     "unsubscribe": "Disiscriviti",
     "subscribe": "Iscriviti",
     "subscribed": "Iscritto",
     "yes": "sì",
     "no": "no",
     "powered_by": "Offerto da",
-    "landing": "Lemmy è un <1>aggregatore di link</1> / alternativa a reddit, creato per integrarsi con il <2>fediverso</2>. <3></3>È self-hosted, i commenti sono aggiornati in tempo reale ed è molto piccolo (<4>~80kB</4>). La federazione con la rete ActivityPub sarà implementata nel futuro. <5></5>Questa versione è una <6>beta molto giovane</6> e molte funzionalità sono incomplete o mancanti. <7></7>Suggerisci nuove funzionalità o segnala errori a <8>questa pagina.</8><9></9>Sviluppato con <10>Rust</10>, <11>Actix</11>, <12>Inferno</12>, <13>Typescript</13>.",
+    "landing": "Lemmy è un <1>aggregatore di link</1> / alternativa a reddit, creato per integrarsi con il <2>fediverso</2>. <3></3>È self-hosted, i commenti sono aggiornati in tempo reale ed è molto piccolo (<4>~80kB</4>). La federazione con la rete ActivityPub sarà implementata nel futuro. <5></5>Questa versione è una <6>beta molto giovane</6> e molte funzionalità sono incomplete o mancanti. <7></7>Suggerisci nuove funzionalità o segnala errori a <8>questa pagina.</8><9></9>Sviluppato con <10>Rust</10>, <11>Actix</11>, <12>Inferno</12>, <13>Typescript</13>.<14></14> <15>Un grazie ai nostri sostenitori: </15> dessalines, Nutomic, asonix, zacanger, and iav.",
     "not_logged_in": "Non hai effettuato l'accesso.",
     "community_ban": "Sei stato escluso da questa comunità.",
     "site_ban": "Sei stato escluso dal sito",
     "old_password": "Vecchia Password",
     "forgot_password": "password dimenticata",
     "new_password": "Nuova Password",
-    "private_message_disclaimer": "Attenzione: i messaggi privati su Lemmy non sono sicuri. Crea un account su <1>Riot.im</1> per una messaggistica sicura.",
+    "private_message_disclaimer": "Attenzione: i messaggi privati su Lemmy non sono sicuri. Crea un account su <1>Element.io</1> per una messaggistica sicura.",
     "language": "Lingua",
     "enable_downvotes": "Abilita voti negativi",
     "enable_nsfw": "Abilita NSFW",
     "downvotes_disabled": "Voti negativi disabilitati",
     "post_title_too_long": "Titolo della pubblicazione troppo lungo.",
     "email_already_exists": "Indirizzo email già presente.",
-    "cross_posted_to": "pubblicato pure su: ",
+    "cross_posted_to": "pubblicato anche su: ",
     "support_on_open_collective": "Sostieni su OpenCollective",
     "admin_settings": "Impostazioni per Admin",
     "site_config": "Configurazione del sito",
     "picture_deleted": "Foto eliminata.",
     "select_a_community": "Seleziona una comunità",
     "invalid_username": "Nome utente non valido.",
-    "what_is": "Cos'è"
+    "what_is": "Cos'è",
+    "must_login": "Devi <1>effettuare l'accesso o registrarti</1> per commentare.",
+    "no_password_reset": "Non sarai in grado di resettare la tua password senza una email.",
+    "cake_day_title": "Torta-giorno:",
+    "cake_day_info": "Oggi è il cake day di {{ creator_name }}!",
+    "invalid_post_title": "Titolo della pubblicazione non valido",
+    "bold": "grassetto",
+    "italic": "corsivo",
+    "subscript": "pedice",
+    "superscript": "apice",
+    "header": "intestazione",
+    "strikethrough": "barrato",
+    "quote": "citazione",
+    "spoiler": "spoiler",
+    "list": "lista",
+    "invalid_url": "URL non valido.",
+    "not_a_moderator": "Non moderatore."
 }
diff --git a/ui/translations/ko.json b/ui/translations/ko.json
new file mode 100644 (file)
index 0000000..0967ef4
--- /dev/null
@@ -0,0 +1 @@
+{}
index ad2e9c1c48a97beec330410482f71a66c2da7583..48c013be2bb47bf1fc7465c2d06f7b9e8bb5af35 100644 (file)
     "powered_by": "Powered by",
     "landing_0": "Lemmy jest <1>agregatorem linków</1> / alternatywą dla reddita. Jest przeznaczony do działania w ramach cyfrowej przestrzeni nazywanej <2>fediverse</2>. <3></3>Opiera się na samodzielnym hostingu, posiada aktualizowane na żywo wątki z komentarzami, i zajmuje bardzo mało miejsce (<4>~80kB</4>). Federacja w ramach sieci ActivityPub jest w planach. <5></5>Ta wersja jest <6>bardzo wczesną wersją beta</6>, co oznacza, że wiele funkcji nadal nie działa tak jak powinny. <7></7><8>Pod tym adresem</8> można sugerować nową funkcjonalność i zgłaszać błędy.<9></9>Stworzono z wykorzystaniem <10>Rust</10>, <11>Actix</11>, <12>Inferno</12>, <13>Typescript</13>.",
     "not_logged_in": "Nie jesteś zalogowana/y.",
+    "bio_length_overflow": "To pole nie może przekraczać 300 znaków!",
     "logged_in": "Zalogowano.",
     "community_ban": "Zostałaś/eś zbanowana/y z tej społeczności.",
     "site_ban": "Zostałaś/eś zbanowana/y z tej witryny",
     "emoji_picker": "Wybór Emoji",
     "silver_sponsors": "Srebrni Sponsorzy to ci, którzy wpłacili co najmniej $40 na Lemmiego.",
     "select_a_community": "Wybierz społeczność",
-    "invalid_username": "Nieprawidłowa nazwa użytkownika."
+    "invalid_username": "Nieprawidłowa nazwa użytkownika.",
+    "invalid_community_name": "Niepoprawna nazwa.",
+    "play_captcha_audio": "Odsłuchaj Captcha Audio",
+    "bio": "Bio"
 }
index 30f3b05ea3013136eebf70a749394fce05ba352b..64ce1ebbc628f4971f6221809ba0ce5de894baa7 100644 (file)
     "all": "Tudo",
     "top": "Top",
     "api": "API",
-    "docs": "Docs",
+    "docs": "Documentação",
     "inbox": "Caixa de entrada",
     "inbox_for": "Caixa de entrada de <1>{{user}}</1>",
     "mark_all_as_read": "marcar tudo como lido",
     "yes": "sim",
     "no": "não",
     "powered_by": "Fornecido por",
-    "landing_0": "Lemmy é um <1>agregador de links</1> / alternativa ao reddit, com a intenção de funcionar junto ao <2>fediverso</2>.<3></3>Pode ser hospedado em servidor próprio, tem atualização de comentários em tempo real e é minúsculo (<4>~80kB</4>). A federação com a rede ActivityPub está no roteiro do projeto. <5></5>Esta é uma <6>versão beta bastante antecipada</6>, e muitas funcionalidades ainda estão quebradas ou ausentes. <7></7>Sugira novas funcionalidades ou reporte erros <8>aqui.</8><9></9>Feito com <10>Rust</10>, <11>Actix</11>, <12>Inferno</12>, <13>Typescript</13>.",
+    "landing": "Lemmy é um <1>agregador de links</1> / alternativa ao reddit, com a intenção de funcionar junto ao <2>fediverso</2>.<3></3>Pode ser hospedado em servidor próprio, tem atualização de comentários em tempo real e é minúsculo (<4>~80kB</4>). A federação com a rede ActivityPub está no roteiro do projeto. <5></5>Esta é uma <6>versão beta bastante antecipada</6>, e muitas funcionalidades ainda estão quebradas ou ausentes. <7></7>Sugira novas funcionalidades ou reporte erros <8>aqui.</8><9></9>Feito com <10>Rust</10>, <11>Actix</11>, <12>Inferno</12>, <13>Typescript</13>. <14></14> <15>Agradecemos aos nossos contribuidores: </15> dessalines, Nutomic, asonix, zacanger, e iav.",
     "not_logged_in": "Não autenticado.",
     "logged_in": "Autenticado.",
     "community_ban": "Você foi banido desta comunidade.",
     "site_saved": "Site Salvo.",
     "emoji_picker": "Selecionador de Emoji",
     "select_a_community": "Selecione uma comunidade",
-    "invalid_username": "Nome de usuário inválido."
+    "invalid_username": "Nome de usuário inválido.",
+    "must_login": "Você precisa <1>entrar ou registrar-se</1> para comentar.",
+    "no_password_reset": "Você não conseguirá redefinir sua senha sem um e-mail.",
+    "invalid_post_title": "Título de publicação inválido",
+    "cake_day_info": "Hoje é o dia do bolo de {{ creator_name }}!",
+    "cake_day_title": "Dia do bolo:",
+    "what_is": "Quanto é"
 }
index d6a7fee28e23188ca078c9fc68e005b89fb05792..50abe05eb56e6a7341d71c4c3d08913385dc6d37 100644 (file)
     "sponsors_of_lemmy": "Спонсоры Lemmy",
     "sponsor_message": "Lemmy это бесплатное, <1>открытое</1> программное обеспечение, без рекламы, монетизации или венчурного капитала, никогда. Ваши пожертвования напрямую поддерживают развитие проекта. Спасибо нижеуказанным людям:",
     "support_on_patreon": "Поддержать на Patreon",
-    "general_sponsors": "Ð\93енеÑ\80алÑ\8cнÑ\8bе Ñ\81понÑ\81оÑ\80Ñ\8b - Ñ\8dÑ\82о Ñ\82е, ÐºÑ\82о Ð¿Ð¾Ð¾Ð±ÐµÑ\89ал Lemmy от $10 до $39.",
+    "general_sponsors": "Ð\93енеÑ\80алÑ\8cнÑ\8bе Ñ\81понÑ\81оÑ\80Ñ\8b - Ñ\8dÑ\82о Ñ\82е, ÐºÑ\82о Ð¿Ð¾Ð¶ÐµÑ\80Ñ\82вовал Lemmy от $10 до $39.",
     "crypto": "Крипто",
     "bitcoin": "Bitcoin",
     "ethereum": "Ethereum",
     "code": "Код",
     "joined": "Присоединился",
     "powered_by": "Работает на",
-    "landing_0": "Lemmy - это <1>агрегатор ссылок</1> / альтернатива reddit, предназначенный для работы в <2>федиверсе</2>.<3></3>Это самодостаточная система, с обновляемыми комментариями, и эта система крошечная (<4>~80 Кб</4>). Федерация в сети ActivityPub находится в разработке. <5></5>Это <6>очень ранняя бета-версия</6>, и многие функции в настоящее время сломаны или отсутствуют. <7></7>Предлагать новые функции или сообщать об ошибках можно <8>здесь.</8><9></9>Сделано на <10>Rust</10>, <11>Actix</11>, <12>Inferno</12>, <13>Typescript</13>.",
+    "landing": "Lemmy - это <1>агрегатор ссылок</1> / альтернатива reddit, предназначенный для работы в <2>федиверсе</2>.<3></3>Это самодостаточная система, с обновляемыми комментариями, и эта система крошечная (<4>~80 Кб</4>). Федерация в сети ActivityPub находится в разработке. <5></5>Это <6>очень ранняя бета-версия</6>, и многие функции в настоящее время сломаны или отсутствуют. <7></7>Предлагать новые функции или сообщать об ошибках можно <8>здесь.</8><9></9>Сделано на <10>Rust</10>, <11>Actix</11>, <12>Inferno</12>, <13>Typescript</13>.<14></14><15>Спасибо нашим помощникам:</15>dessalines, Nutomic, asonix, zacanger, и iav.",
     "not_logged_in": "Не авторизованы.",
     "community_ban": "Вы были заблокированы на данном сообществе.",
     "site_ban": "Вы были заблокированы на данном сайте",
     "send_message": "Послать сообщение",
     "message": "Сообщение",
     "avatar": "Аватар",
-    "show_avatars": "Показать Аватары",
+    "show_avatars": "Показывать аватары",
     "formatting_help": "Помощь в верстке текста",
     "sticky": "приклеить",
     "stickied": "закрепленный пост",
     "old_password": "Действующий пароль",
     "forgot_password": "я забыл(а) пароль",
     "reset_password_mail_sent": "Письмо для восстановления пароля было выслано.",
-    "private_message_disclaimer": "Предупреждение: Приватные сообщения Lemmy на данный момент не зашифрованы. Для безопасной коммуникации создайте аккаунт на <1>Riot.im</1>.",
+    "private_message_disclaimer": "Предупреждение: Приватные сообщения Lemmy на данный момент не зашифрованы. Для безопасной коммуникации создайте аккаунт на <1>Element.io</1>.",
     "send_notifications_to_email": "Посылать уведомления на e-mail адрес",
     "language": "Язык",
     "browser_default": "Браузер по умолчанию",
     "messages": "Сообщения",
     "new_password": "Новый пароль",
     "theme": "Визуальная тема",
-    "post_title_too_long": "Ð\94лина Ð½Ð°Ð·Ð²Ð°Ð½Ð¸Ñ\8f Ð¿Ð¾Ñ\81Ñ\82а превышает допустимый лимит.",
+    "post_title_too_long": "Ð\94лина Ð½Ð°Ð·Ð²Ð°Ð½Ð¸Ñ\8f Ð·Ð°Ð¿Ð¸Ñ\81и превышает допустимый лимит.",
     "time": "Время",
     "action": "Действие",
     "view_source": "исходный код сообщения",
     "to": "в",
     "admin_settings": "Настройки админа",
     "banned_users": "Забаненные Пользователи",
-    "support_on_open_collective": "Ð\9fоддеÑ\80жка на OpenCollective",
+    "support_on_open_collective": "Ð\9fоддеÑ\80жаÑ\82Ñ\8c на OpenCollective",
     "site_saved": "Сайт Сохранен.",
     "enable_nsfw": "Включить NSFW",
-    "donate": "Пожертвование",
+    "donate": "Пожертвования",
     "unsticky": "отклеить",
     "site_config": "Конфигурация сайта",
     "banned": "забаненный",
     "password_change": "Смена пароля",
     "no_email_setup": "Этот сервер неправильно настроил электронную почту.",
-    "matrix_user_id": "Ð\9cаÑ\82Ñ\80иÑ\86а Ð¿Ð¾Ð»Ñ\8cзоваÑ\82елÑ\8f",
+    "matrix_user_id": "Ð\90дÑ\80еÑ\81 Ð² Matrix",
     "are_you_sure": "вы уверены?",
     "archive_link": "архивировать ссылку",
-    "logged_in": "Войти в систему.",
+    "logged_in": "Вошли в систему.",
     "couldnt_get_comments": "Не удалось получить комментарии.",
     "from": "от",
     "transfer_site": "трансфер сайт",
     "monero": "Monero",
     "emoji_picker": "Сборщик эмодзи",
     "select_a_community": "Выбрать сообщество",
-    "invalid_username": "Неверное имя пользователя."
+    "invalid_username": "Неверное имя пользователя.",
+    "must_login": "Вы должны <1>авторизироваться или зарегестрироваться</1> что бы комментировать.",
+    "no_password_reset": "Вы не сможете сбросить ваш пароль без адреса электронной почты.",
+    "cake_day_title": "День торта:",
+    "what_is": "Что такое",
+    "superscript": "верхний индекс",
+    "cake_day_info": "Сегодня день торта у {{ creator_name }}!",
+    "invalid_post_title": "Недопустимый заголовок записи",
+    "bold": "жирный",
+    "italic": "курсив",
+    "subscript": "нижний индекс",
+    "header": "заголовок",
+    "strikethrough": "зачёркивание",
+    "quote": "цитата",
+    "spoiler": "спойлер",
+    "list": "список",
+    "not_a_moderator": "Не модератор.",
+    "invalid_url": "Недопустимый URL."
 }
index 1feb07f41a3848a35a54ee22bd6eb0937b230bb6..b7f88631ff1749906b1bf994fc0ade16681c1efb 100644 (file)
@@ -1,29 +1,29 @@
 {
-    "post": "inlägg",
-    "remove_post": "Radera inlägg",
+    "post": "publicera",
+    "remove_post": "Ta bort inlägg",
     "no_posts": "Inga inlägg.",
     "create_a_post": "Skriv ett inlägg",
     "create_post": "Skapa inlägg",
-    "number_of_posts": "{{count}} Inlägg",
-    "number_of_posts_plural": "{{count}} Inlägg",
+    "number_of_posts": "{{count}} inlägg",
+    "number_of_posts_plural": "{{count}} inlägg",
     "posts": "Inlägg",
-    "related_posts": "Dessa inlägg kan vara relaterade",
+    "related_posts": "Dessa inlägg kan höra samman",
     "cross_posts": "Den här länken har även publicerats i:",
     "cross_post": "tvärposta",
     "comments": "Kommentarer",
-    "number_of_comments": "{{count}} Kommentar",
-    "number_of_comments_plural": "{{count}} Kommentarer",
-    "remove_comment": "Radera kommentar",
+    "number_of_comments": "{{count}} kommentar",
+    "number_of_comments_plural": "{{count}} kommentarer",
+    "remove_comment": "Ta bort kommentar",
     "communities": "Gemenskaper",
     "users": "Användare",
     "create_a_community": "Skapa en gemenskap",
     "create_community": "Skapa gemenskap",
-    "remove_community": "Radera gemenskap",
+    "remove_community": "Ta bort gemenskap",
     "subscribed_to_communities": "Prenumererar på <1>gemenskaper</1>",
     "trending_communities": "Populära <1>gemenskaper</1>",
     "list_of_communities": "Lista över gemenskaper",
-    "number_of_communities": "{{count}} Gemenskap",
-    "number_of_communities_plural": "{{count}} Gemenskaper",
+    "number_of_communities": "{{count}} gemenskap",
+    "number_of_communities_plural": "{{count}} gemenskaper",
     "community_reqs": "gemener, understreck och inga blanksteg.",
     "edit": "redigera",
     "reply": "svara",
     "creator": "skapare",
     "username": "Användarnamn",
     "email_or_username": "E-postadress eller användarnamn",
-    "number_of_users": "{{count}} Användare",
-    "number_of_users_plural": "{{count}} Användare",
-    "number_of_subscribers": "{{count}} Prenumerant",
-    "number_of_subscribers_plural": "{{count}} Prenumeranter",
-    "number_of_points": "{{count}} Poäng",
-    "number_of_points_plural": "{{count}} Poäng",
-    "number_online": "{{count}} Användare inloggad",
-    "number_online_plural": "{{count}} Användare inloggade",
+    "number_of_users": "{{count}} användare",
+    "number_of_users_plural": "{{count}} användare",
+    "number_of_subscribers": "{{count}} prenumerant",
+    "number_of_subscribers_plural": "{{count}} prenumeranter",
+    "number_of_points": "{{count}} poäng",
+    "number_of_points_plural": "{{count}} poäng",
+    "number_online": "{{count}} användare inloggad",
+    "number_online_plural": "{{count}} användare inloggade",
     "name": "Namn",
     "title": "Titel",
     "category": "Kategori",
     "subscribers": "Prenumeranter",
     "both": "Båda",
     "saved": "Sparade",
-    "unsubscribe": "Avbryt prenumeration",
+    "unsubscribe": "Avsluta prenumeration",
     "subscribe": "Prenumerera",
     "subscribed": "Prenumererar",
     "prev": "Föregående",
     "next": "Nästa",
-    "sidebar": "Sidlist",
-    "sort_type": "Sorteringstyp",
+    "sidebar": "Sidolist",
+    "sort_type": "Sortering",
     "hot": "Hett",
     "new": "Nytt",
     "top_day": "Dagstoppen",
-    "week": "Vecka",
-    "month": "Månad",
-    "year": "År",
-    "all": "Samtliga",
+    "week": "Veckotoppen",
+    "month": "Månadstoppen",
+    "year": "Årstoppen",
+    "all": "Totaltoppen",
     "top": "Topp",
     "api": "API",
     "inbox": "Inkorg",
     "theme": "Utseende",
     "sponsors": "Sponsorer",
     "sponsors_of_lemmy": "Lemmys sponsorer",
-    "sponsor_message": "Lemmy är fri mjukvara med <1>öppen källkod</1>, vilket innebär att ingen reklam, vinstindrivning eller venturekapital förekommer, någonsin. Dina donationer går direkt till att stöda utvecklingen av projektet. Stort tack till följande personer:",
+    "sponsor_message": "Lemmy är en fri mjukvara med <1>öppen källkod</1>, vilket innebär att ingen reklam, vinstindrivning eller venture-kapital förekommer, någonsin. Dina donationer går direkt till att stöda utvecklingen av projektet. Stort tack till följande personer:",
     "support_on_patreon": "Stöd på Patreon",
     "general_sponsors": "Allmänna sponsorer är de som donerat mellan 10 och 39 dollar till Lemmy.",
     "crypto": "Kryptovaluta",
     "yes": "ja",
     "no": "nej",
     "powered_by": "Drivs av",
-    "landing_0": "Lemmy är en <1>länksamlare</1> och alternativ till reddit, ämnad att fungera i <2>Fediversumet</2>.<3></3>Lemmy kan drivas av vem som helst, har kommentarstrådar som updateras i realid och är mycket liten (<4>ca 80 kB</4>). Federering med ActivityPub-nätverket är planerat. <5></5>Detta är en <6>väldigt tidig betaversion</6> och många funktioner saknas därför eller är trasiga.<7></7>Föreslå nya funktioner eller anmäl buggar <8>här</8>.<9></9>Skapad i <10>Rust</10>, <11>Actix</11>, <12>Inferno</12> och <13>Typescript</13>.",
+    "landing": "Lemmy är en <1>länksamlare</1> och alternativ till reddit, ämnad att fungera i <2>Fediversumet</2>.<3></3>Lemmy kan drivas av vem som helst, har kommentarstrådar som uppdateras i realtid och är mycket liten (<4>ca 80 kB</4>). Federering med ActivityPub-nätverket är planerat. <5></5>Detta är en <6>väldigt tidig betaversion</6> och många funktioner saknas därför eller är trasiga.<7></7>Föreslå nya funktioner eller anmäl buggar <8>här</8>.<9></9>Skapad i <10>Rust</10>, <11>Actix</11>, <12>Inferno</12> och <13>Typescript</13>. <14></14> <15>Ett stort tack till våra bidragsgivare: </15> dessalines, Nutomic, asonix, zacanger, och iav.",
     "not_logged_in": "Inte inloggad.",
     "community_ban": "Du har blockerats från den här gemenskapen.",
     "site_ban": "Du har blockerats från webbplatsen",
     "system_err_login": "Systemfel. Försök att logga ut och sedan in igen.",
     "invalid_community_name": "Ogiltigt namn.",
     "click_to_delete_picture": "Klicka för att ta bort bild.",
-    "picture_deleted": "Bild borttagen.",
-    "upload_avatar": "Ladda upp avatar",
+    "picture_deleted": "Bilden har raderats.",
+    "upload_avatar": "Ladda upp profilbild",
     "enable_nsfw": "Aktivera NSFW",
     "sorting_help": "sorteringshjälp",
     "more": "mer",
-    "avatar": "Avatar",
+    "avatar": "Profilbild",
     "cross_posted_to": "Tvärpostat till: ",
     "send_secure_message": "Skicka säkert meddelande",
     "send_message": "Skicka meddelande",
     "message": "Meddelande",
-    "create_private_message": "Skapa Privatmeddelande",
-    "show_avatars": "Visa avatarer",
-    "archive_link": "Arkivera länk",
+    "create_private_message": "Skriv privat meddelande",
+    "show_avatars": "Visa profilbilder",
+    "archive_link": "Arkivlänk",
     "admin_settings": "Administratörsinställningar",
-    "site_config": "Webbplats konfiguration",
-    "old": "Gammal",
+    "site_config": "Webbplatsinställningar",
+    "old": "Gammalt",
     "banned_users": "Blockerade användare",
     "docs": "Dokumentation",
-    "post_title_too_long": "Inläggstitel är för lång.",
+    "post_title_too_long": "Inläggstiteln är för lång.",
     "replies": "Svar",
     "mentions": "Nämner",
     "message_sent": "Meddelande skickat",
     "new_password": "Nytt lösenord",
     "no_email_setup": "Denna server har inte satt upp e-post korrekt.",
     "matrix_user_id": "Matrix-användare",
-    "show_context": "Visa innehåll",
-    "private_message_disclaimer": "Varning: Privata meddelanden på Lemmy är inte säkra. Vänligen skapa ett konto på <1>Riot.im</1> för att skicka säkra meddelanden.",
-    "send_notifications_to_email": "Skicka aviseringar till E-post",
+    "show_context": "Visa sammanhang",
+    "private_message_disclaimer": "Varning: Privata meddelanden på Lemmy är inte säkra. Vänligen skapa ett konto på <1>Element.io</1> för att skicka säkra meddelanden.",
+    "send_notifications_to_email": "Skicka aviseringar till e-postadress",
     "language": "Språk",
-    "browser_default": "Webbläsarestandard",
-    "downvotes_disabled": "Nedröstningar inaktiverat",
+    "browser_default": "Webbläsarens språk",
+    "downvotes_disabled": "Nedröstningar inaktiverade",
     "enable_downvotes": "Aktivera nedröstningar",
-    "upvote": "Upprösta",
-    "number_of_upvotes": "{{count}} Uppröst",
-    "number_of_upvotes_plural": "{{count}} Uppröstningar",
-    "downvote": "Nedrösta",
-    "number_of_downvotes": "{{count}} Nedröst",
-    "number_of_downvotes_plural": "{{count}} Nedröstningar",
+    "upvote": "Rösta upp",
+    "number_of_upvotes": "{{count}} uppröst",
+    "number_of_upvotes_plural": "{{count}} uppröster",
+    "downvote": "Rösta ned",
+    "number_of_downvotes": "{{count}} nedröst",
+    "number_of_downvotes_plural": "{{count}} nedröster",
     "open_registration": "Öppen registrering",
     "registration_closed": "Registrering stängd",
     "support_on_liberapay": "Stöd på Liberapay",
     "couldnt_create_private_message": "Kunde inte skapa privat meddelande.",
     "no_private_message_edit_allowed": "Inte tillåtet att redigera privata meddelanden.",
     "couldnt_update_private_message": "Kunde inte uppdatera privat meddelande.",
-    "time": "Tid",
+    "time": "Tidpunkt",
     "emoji_picker": "Emoji-väljare",
     "block_leaving": "Är du säker på att du vill lämna?",
     "select_a_community": "Välj en gemenskap",
     "from": "från",
-    "invalid_username": "Ogiltigt användarnamn."
+    "invalid_username": "Ogiltigt användarnamn.",
+    "cake_day_info": "Idag firar vi {{ creator_name }} med tårta!",
+    "must_login": "Du måste <1>logga in eller registrera dig</1> för att kommentera.",
+    "no_password_reset": "Du kommer inte kunna återställa ditt lösenord utan en e-postadress.",
+    "what_is": "Vad är",
+    "cake_day_title": "Tårtdag:",
+    "invalid_post_title": "Ogiltig inläggstitel",
+    "bold": "fetstil",
+    "italic": "kursiv stil",
+    "header": "rubrik",
+    "quote": "citat",
+    "subscript": "nedsänkt (indexläge)",
+    "superscript": "upphöjt (exponentläge)",
+    "strikethrough": "genomstruket",
+    "spoiler": "innehållsvarning",
+    "list": "lista",
+    "not_a_moderator": "Inte en moderator.",
+    "invalid_url": "Ogiltig URL."
 }
index c39f1dc4b058e3c83f1b87b19f2bd2eea4b5d840..a474636ed71510010d8c383dbec6f77355e151fe 100644 (file)
 # yarn lockfile v1
 
 
-"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.8.3":
-  version "7.8.3"
-  resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.8.3.tgz#33e25903d7481181534e12ec0a25f16b6fcf419e"
-  integrity sha512-a9gxpmdXtZEInkCSHUJDLHZVBgb1QS0jhss4cPP93EW7s+uC5bikET2twEF3KV+7rDblJcmNvTR7VJejqd2C2g==
+"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.10.4":
+  version "7.10.4"
+  resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.10.4.tgz#168da1a36e90da68ae8d49c0f1b48c7c6249213a"
+  integrity sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==
   dependencies:
-    "@babel/highlight" "^7.8.3"
+    "@babel/highlight" "^7.10.4"
 
 "@babel/core@^7.1.0", "@babel/core@^7.7.5":
-  version "7.9.0"
-  resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.9.0.tgz#ac977b538b77e132ff706f3b8a4dbad09c03c56e"
-  integrity sha512-kWc7L0fw1xwvI0zi8OKVBuxRVefwGOrKSQMvrQ3dW+bIIavBY3/NpXmpjMy7bQnLgwgzWQZ8TlM57YHpHNHz4w==
-  dependencies:
-    "@babel/code-frame" "^7.8.3"
-    "@babel/generator" "^7.9.0"
-    "@babel/helper-module-transforms" "^7.9.0"
-    "@babel/helpers" "^7.9.0"
-    "@babel/parser" "^7.9.0"
-    "@babel/template" "^7.8.6"
-    "@babel/traverse" "^7.9.0"
-    "@babel/types" "^7.9.0"
+  version "7.10.5"
+  resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.10.5.tgz#1f15e2cca8ad9a1d78a38ddba612f5e7cdbbd330"
+  integrity sha512-O34LQooYVDXPl7QWCdW9p4NR+QlzOr7xShPPJz8GsuCU3/8ua/wqTr7gmnxXv+WBESiGU/G5s16i6tUvHkNb+w==
+  dependencies:
+    "@babel/code-frame" "^7.10.4"
+    "@babel/generator" "^7.10.5"
+    "@babel/helper-module-transforms" "^7.10.5"
+    "@babel/helpers" "^7.10.4"
+    "@babel/parser" "^7.10.5"
+    "@babel/template" "^7.10.4"
+    "@babel/traverse" "^7.10.5"
+    "@babel/types" "^7.10.5"
     convert-source-map "^1.7.0"
     debug "^4.1.0"
     gensync "^1.0.0-beta.1"
     json5 "^2.1.2"
-    lodash "^4.17.13"
+    lodash "^4.17.19"
     resolve "^1.3.2"
     semver "^5.4.1"
     source-map "^0.5.0"
 
-"@babel/generator@^7.8.6":
-  version "7.8.8"
-  resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.8.8.tgz#cdcd58caab730834cee9eeadb729e833b625da3e"
-  integrity sha512-HKyUVu69cZoclptr8t8U5b6sx6zoWjh8jiUhnuj3MpZuKT2dJ8zPTuiy31luq32swhI0SpwItCIlU8XW7BZeJg==
-  dependencies:
-    "@babel/types" "^7.8.7"
-    jsesc "^2.5.1"
-    lodash "^4.17.13"
-    source-map "^0.5.0"
-
-"@babel/generator@^7.9.0", "@babel/generator@^7.9.5":
-  version "7.9.5"
-  resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.9.5.tgz#27f0917741acc41e6eaaced6d68f96c3fa9afaf9"
-  integrity sha512-GbNIxVB3ZJe3tLeDm1HSn2AhuD/mVcyLDpgtLXa5tplmWrJdF/elxB56XNqCuD6szyNkDi6wuoKXln3QeBmCHQ==
+"@babel/generator@^7.10.5":
+  version "7.10.5"
+  resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.10.5.tgz#1b903554bc8c583ee8d25f1e8969732e6b829a69"
+  integrity sha512-3vXxr3FEW7E7lJZiWQ3bM4+v/Vyr9C+hpolQ8BGFr9Y8Ri2tFLWTixmwKBafDujO1WVah4fhZBeU1bieKdghig==
   dependencies:
-    "@babel/types" "^7.9.5"
+    "@babel/types" "^7.10.5"
     jsesc "^2.5.1"
-    lodash "^4.17.13"
     source-map "^0.5.0"
 
-"@babel/helper-function-name@^7.8.3":
-  version "7.8.3"
-  resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.8.3.tgz#eeeb665a01b1f11068e9fb86ad56a1cb1a824cca"
-  integrity sha512-BCxgX1BC2hD/oBlIFUgOCQDOPV8nSINxCwM3o93xP4P9Fq6aV5sgv2cOOITDMtCfQ+3PvHp3l689XZvAM9QyOA==
-  dependencies:
-    "@babel/helper-get-function-arity" "^7.8.3"
-    "@babel/template" "^7.8.3"
-    "@babel/types" "^7.8.3"
-
-"@babel/helper-function-name@^7.9.5":
-  version "7.9.5"
-  resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.9.5.tgz#2b53820d35275120e1874a82e5aabe1376920a5c"
-  integrity sha512-JVcQZeXM59Cd1qanDUxv9fgJpt3NeKUaqBqUEvfmQ+BCOKq2xUgaWZW2hr0dkbyJgezYuplEoh5knmrnS68efw==
-  dependencies:
-    "@babel/helper-get-function-arity" "^7.8.3"
-    "@babel/template" "^7.8.3"
-    "@babel/types" "^7.9.5"
-
-"@babel/helper-get-function-arity@^7.8.3":
-  version "7.8.3"
-  resolved "https://registry.yarnpkg.com/@babel/helper-get-function-arity/-/helper-get-function-arity-7.8.3.tgz#b894b947bd004381ce63ea1db9f08547e920abd5"
-  integrity sha512-FVDR+Gd9iLjUMY1fzE2SR0IuaJToR4RkCDARVfsBBPSP53GEqSFjD8gNyxg246VUyc/ALRxFaAK8rVG7UT7xRA==
-  dependencies:
-    "@babel/types" "^7.8.3"
-
-"@babel/helper-member-expression-to-functions@^7.8.3":
-  version "7.8.3"
-  resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.8.3.tgz#659b710498ea6c1d9907e0c73f206eee7dadc24c"
-  integrity sha512-fO4Egq88utkQFjbPrSHGmGLFqmrshs11d46WI+WZDESt7Wu7wN2G2Iu+NMMZJFDOVRHAMIkB5SNh30NtwCA7RA==
-  dependencies:
-    "@babel/types" "^7.8.3"
-
-"@babel/helper-module-imports@^7.8.3":
-  version "7.8.3"
-  resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.8.3.tgz#7fe39589b39c016331b6b8c3f441e8f0b1419498"
-  integrity sha512-R0Bx3jippsbAEtzkpZ/6FIiuzOURPcMjHp+Z6xPe6DtApDJx+w7UYyOLanZqO8+wKR9G10s/FmHXvxaMd9s6Kg==
-  dependencies:
-    "@babel/types" "^7.8.3"
-
-"@babel/helper-module-transforms@^7.9.0":
-  version "7.9.0"
-  resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.9.0.tgz#43b34dfe15961918707d247327431388e9fe96e5"
-  integrity sha512-0FvKyu0gpPfIQ8EkxlrAydOWROdHpBmiCiRwLkUiBGhCUPRRbVD2/tm3sFr/c/GWFrQ/ffutGUAnx7V0FzT2wA==
-  dependencies:
-    "@babel/helper-module-imports" "^7.8.3"
-    "@babel/helper-replace-supers" "^7.8.6"
-    "@babel/helper-simple-access" "^7.8.3"
-    "@babel/helper-split-export-declaration" "^7.8.3"
-    "@babel/template" "^7.8.6"
-    "@babel/types" "^7.9.0"
-    lodash "^4.17.13"
-
-"@babel/helper-optimise-call-expression@^7.8.3":
-  version "7.8.3"
-  resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.8.3.tgz#7ed071813d09c75298ef4f208956006b6111ecb9"
-  integrity sha512-Kag20n86cbO2AvHca6EJsvqAd82gc6VMGule4HwebwMlwkpXuVqrNRj6CkCV2sKxgi9MyAUnZVnZ6lJ1/vKhHQ==
-  dependencies:
-    "@babel/types" "^7.8.3"
-
-"@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.8.0", "@babel/helper-plugin-utils@^7.8.3":
-  version "7.8.3"
-  resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.8.3.tgz#9ea293be19babc0f52ff8ca88b34c3611b208670"
-  integrity sha512-j+fq49Xds2smCUNYmEHF9kGNkhbet6yVIBp4e6oeQpH1RUs/Ir06xUKzDjDkGcaaokPiTNs2JBWHjaE4csUkZQ==
-
-"@babel/helper-replace-supers@^7.8.6":
-  version "7.8.6"
-  resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.8.6.tgz#5ada744fd5ad73203bf1d67459a27dcba67effc8"
-  integrity sha512-PeMArdA4Sv/Wf4zXwBKPqVj7n9UF/xg6slNRtZW84FM7JpE1CbG8B612FyM4cxrf4fMAMGO0kR7voy1ForHHFA==
-  dependencies:
-    "@babel/helper-member-expression-to-functions" "^7.8.3"
-    "@babel/helper-optimise-call-expression" "^7.8.3"
-    "@babel/traverse" "^7.8.6"
-    "@babel/types" "^7.8.6"
-
-"@babel/helper-simple-access@^7.8.3":
-  version "7.8.3"
-  resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.8.3.tgz#7f8109928b4dab4654076986af575231deb639ae"
-  integrity sha512-VNGUDjx5cCWg4vvCTR8qQ7YJYZ+HBjxOgXEl7ounz+4Sn7+LMD3CFrCTEU6/qXKbA2nKg21CwhhBzO0RpRbdCw==
-  dependencies:
-    "@babel/template" "^7.8.3"
-    "@babel/types" "^7.8.3"
-
-"@babel/helper-split-export-declaration@^7.8.3":
-  version "7.8.3"
-  resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.8.3.tgz#31a9f30070f91368a7182cf05f831781065fc7a9"
-  integrity sha512-3x3yOeyBhW851hroze7ElzdkeRXQYQbFIb7gLK1WQYsw2GWDay5gAJNw1sWJ0VFP6z5J1whqeXH/WCdCjZv6dA==
-  dependencies:
-    "@babel/types" "^7.8.3"
-
-"@babel/helper-validator-identifier@^7.9.5":
-  version "7.9.5"
-  resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.9.5.tgz#90977a8e6fbf6b431a7dc31752eee233bf052d80"
-  integrity sha512-/8arLKUFq882w4tWGj9JYzRpAlZgiWUJ+dtteNTDqrRBz9Iguck9Rn3ykuBDoUwh2TO4tSAJlrxDUOXWklJe4g==
-
-"@babel/helpers@^7.9.0":
-  version "7.9.2"
-  resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.9.2.tgz#b42a81a811f1e7313b88cba8adc66b3d9ae6c09f"
-  integrity sha512-JwLvzlXVPjO8eU9c/wF9/zOIN7X6h8DYf7mG4CiFRZRvZNKEF5dQ3H3V+ASkHoIB3mWhatgl5ONhyqHRI6MppA==
-  dependencies:
-    "@babel/template" "^7.8.3"
-    "@babel/traverse" "^7.9.0"
-    "@babel/types" "^7.9.0"
-
-"@babel/highlight@^7.8.3":
-  version "7.8.3"
-  resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.8.3.tgz#28f173d04223eaaa59bc1d439a3836e6d1265797"
-  integrity sha512-PX4y5xQUvy0fnEVHrYOarRPXVWafSjTW9T0Hab8gVIawpl2Sj0ORyrygANq+KjcNlSSTw0YCLSNA8OyZ1I4yEg==
-  dependencies:
+"@babel/helper-function-name@^7.10.4":
+  version "7.10.4"
+  resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.10.4.tgz#d2d3b20c59ad8c47112fa7d2a94bc09d5ef82f1a"
+  integrity sha512-YdaSyz1n8gY44EmN7x44zBn9zQ1Ry2Y+3GTA+3vH6Mizke1Vw0aWDM66FOYEPw8//qKkmqOckrGgTYa+6sceqQ==
+  dependencies:
+    "@babel/helper-get-function-arity" "^7.10.4"
+    "@babel/template" "^7.10.4"
+    "@babel/types" "^7.10.4"
+
+"@babel/helper-get-function-arity@^7.10.4":
+  version "7.10.4"
+  resolved "https://registry.yarnpkg.com/@babel/helper-get-function-arity/-/helper-get-function-arity-7.10.4.tgz#98c1cbea0e2332f33f9a4661b8ce1505b2c19ba2"
+  integrity sha512-EkN3YDB+SRDgiIUnNgcmiD361ti+AVbL3f3Henf6dqqUyr5dMsorno0lJWJuLhDhkI5sYEpgj6y9kB8AOU1I2A==
+  dependencies:
+    "@babel/types" "^7.10.4"
+
+"@babel/helper-member-expression-to-functions@^7.10.4":
+  version "7.10.5"
+  resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.10.5.tgz#172f56e7a63e78112f3a04055f24365af702e7ee"
+  integrity sha512-HiqJpYD5+WopCXIAbQDG0zye5XYVvcO9w/DHp5GsaGkRUaamLj2bEtu6i8rnGGprAhHM3qidCMgp71HF4endhA==
+  dependencies:
+    "@babel/types" "^7.10.5"
+
+"@babel/helper-module-imports@^7.10.4":
+  version "7.10.4"
+  resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.10.4.tgz#4c5c54be04bd31670a7382797d75b9fa2e5b5620"
+  integrity sha512-nEQJHqYavI217oD9+s5MUBzk6x1IlvoS9WTPfgG43CbMEeStE0v+r+TucWdx8KFGowPGvyOkDT9+7DHedIDnVw==
+  dependencies:
+    "@babel/types" "^7.10.4"
+
+"@babel/helper-module-transforms@^7.10.5":
+  version "7.10.5"
+  resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.10.5.tgz#120c271c0b3353673fcdfd8c053db3c544a260d6"
+  integrity sha512-4P+CWMJ6/j1W915ITJaUkadLObmCRRSC234uctJfn/vHrsLNxsR8dwlcXv9ZhJWzl77awf+mWXSZEKt5t0OnlA==
+  dependencies:
+    "@babel/helper-module-imports" "^7.10.4"
+    "@babel/helper-replace-supers" "^7.10.4"
+    "@babel/helper-simple-access" "^7.10.4"
+    "@babel/helper-split-export-declaration" "^7.10.4"
+    "@babel/template" "^7.10.4"
+    "@babel/types" "^7.10.5"
+    lodash "^4.17.19"
+
+"@babel/helper-optimise-call-expression@^7.10.4":
+  version "7.10.4"
+  resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.10.4.tgz#50dc96413d594f995a77905905b05893cd779673"
+  integrity sha512-n3UGKY4VXwXThEiKrgRAoVPBMqeoPgHVqiHZOanAJCG9nQUL2pLRQirUzl0ioKclHGpGqRgIOkgcIJaIWLpygg==
+  dependencies:
+    "@babel/types" "^7.10.4"
+
+"@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.10.4", "@babel/helper-plugin-utils@^7.8.0":
+  version "7.10.4"
+  resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.4.tgz#2f75a831269d4f677de49986dff59927533cf375"
+  integrity sha512-O4KCvQA6lLiMU9l2eawBPMf1xPP8xPfB3iEQw150hOVTqj/rfXz0ThTb4HEzqQfs2Bmo5Ay8BzxfzVtBrr9dVg==
+
+"@babel/helper-replace-supers@^7.10.4":
+  version "7.10.4"
+  resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.10.4.tgz#d585cd9388ea06e6031e4cd44b6713cbead9e6cf"
+  integrity sha512-sPxZfFXocEymYTdVK1UNmFPBN+Hv5mJkLPsYWwGBxZAxaWfFu+xqp7b6qWD0yjNuNL2VKc6L5M18tOXUP7NU0A==
+  dependencies:
+    "@babel/helper-member-expression-to-functions" "^7.10.4"
+    "@babel/helper-optimise-call-expression" "^7.10.4"
+    "@babel/traverse" "^7.10.4"
+    "@babel/types" "^7.10.4"
+
+"@babel/helper-simple-access@^7.10.4":
+  version "7.10.4"
+  resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.10.4.tgz#0f5ccda2945277a2a7a2d3a821e15395edcf3461"
+  integrity sha512-0fMy72ej/VEvF8ULmX6yb5MtHG4uH4Dbd6I/aHDb/JVg0bbivwt9Wg+h3uMvX+QSFtwr5MeItvazbrc4jtRAXw==
+  dependencies:
+    "@babel/template" "^7.10.4"
+    "@babel/types" "^7.10.4"
+
+"@babel/helper-split-export-declaration@^7.10.4":
+  version "7.10.4"
+  resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.10.4.tgz#2c70576eaa3b5609b24cb99db2888cc3fc4251d1"
+  integrity sha512-pySBTeoUff56fL5CBU2hWm9TesA4r/rOkI9DyJLvvgz09MB9YtfIYe3iBriVaYNaPe+Alua0vBIOVOLs2buWhg==
+  dependencies:
+    "@babel/types" "^7.10.4"
+
+"@babel/helper-validator-identifier@^7.10.4":
+  version "7.10.4"
+  resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz#a78c7a7251e01f616512d31b10adcf52ada5e0d2"
+  integrity sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw==
+
+"@babel/helpers@^7.10.4":
+  version "7.10.4"
+  resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.10.4.tgz#2abeb0d721aff7c0a97376b9e1f6f65d7a475044"
+  integrity sha512-L2gX/XeUONeEbI78dXSrJzGdz4GQ+ZTA/aazfUsFaWjSe95kiCuOZ5HsXvkiw3iwF+mFHSRUfJU8t6YavocdXA==
+  dependencies:
+    "@babel/template" "^7.10.4"
+    "@babel/traverse" "^7.10.4"
+    "@babel/types" "^7.10.4"
+
+"@babel/highlight@^7.10.4":
+  version "7.10.4"
+  resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.10.4.tgz#7d1bdfd65753538fabe6c38596cdb76d9ac60143"
+  integrity sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA==
+  dependencies:
+    "@babel/helper-validator-identifier" "^7.10.4"
     chalk "^2.0.0"
-    esutils "^2.0.2"
     js-tokens "^4.0.0"
 
-"@babel/parser@^7.1.0", "@babel/parser@^7.7.5", "@babel/parser@^7.9.0":
-  version "7.9.4"
-  resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.9.4.tgz#68a35e6b0319bbc014465be43828300113f2f2e8"
-  integrity sha512-bC49otXX6N0/VYhgOMh4gnP26E9xnDZK3TmbNpxYzzz9BQLBosQwfyOe9/cXUU3txYhTzLCbcqd5c8y/OmCjHA==
-
-"@babel/parser@^7.7.0", "@babel/parser@^7.8.6":
-  version "7.8.8"
-  resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.8.8.tgz#4c3b7ce36db37e0629be1f0d50a571d2f86f6cd4"
-  integrity sha512-mO5GWzBPsPf6865iIbzNE0AvkKF3NE+2S3eRUpE+FE07BOAkXh6G+GW/Pj01hhXjve1WScbaIO4UlY1JKeqCcA==
-
-"@babel/parser@^7.8.3":
-  version "7.8.4"
-  resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.8.4.tgz#d1dbe64691d60358a974295fa53da074dd2ce8e8"
-  integrity sha512-0fKu/QqildpXmPVaRBoXOlyBb3MC+J0A66x97qEfLOMkn3u6nfY5esWogQwi/K0BjASYy4DbnsEWnpNL6qT5Mw==
+"@babel/parser@^7.1.0", "@babel/parser@^7.10.4", "@babel/parser@^7.10.5", "@babel/parser@^7.7.0":
+  version "7.10.5"
+  resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.10.5.tgz#e7c6bf5a7deff957cec9f04b551e2762909d826b"
+  integrity sha512-wfryxy4bE1UivvQKSQDU4/X6dr+i8bctjUjj8Zyt3DQy7NtPizJXT8M52nqpNKL+nq2PW8lxk4ZqLj0fD4B4hQ==
 
 "@babel/plugin-syntax-async-generators@^7.8.4":
   version "7.8.4"
     "@babel/helper-plugin-utils" "^7.8.0"
 
 "@babel/plugin-syntax-class-properties@^7.8.3":
-  version "7.8.3"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.8.3.tgz#6cb933a8872c8d359bfde69bbeaae5162fd1e8f7"
-  integrity sha512-UcAyQWg2bAN647Q+O811tG9MrJ38Z10jjhQdKNAL8fsyPzE3cCN/uT+f55cFVY4aGO4jqJAvmqsuY3GQDwAoXg==
+  version "7.10.4"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.10.4.tgz#6644e6a0baa55a61f9e3231f6c9eeb6ee46c124c"
+  integrity sha512-GCSBF7iUle6rNugfURwNmCGG3Z/2+opxAMLs1nND4bhEG5PuxTIggDBoeYYSujAlLtsupzOHYJQgPS3pivwXIA==
   dependencies:
-    "@babel/helper-plugin-utils" "^7.8.3"
+    "@babel/helper-plugin-utils" "^7.10.4"
+
+"@babel/plugin-syntax-import-meta@^7.8.3":
+  version "7.10.4"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz#ee601348c370fa334d2207be158777496521fd51"
+  integrity sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.10.4"
 
 "@babel/plugin-syntax-json-strings@^7.8.3":
   version "7.8.3"
     "@babel/helper-plugin-utils" "^7.8.0"
 
 "@babel/plugin-syntax-logical-assignment-operators@^7.8.3":
-  version "7.8.3"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.8.3.tgz#3995d7d7ffff432f6ddc742b47e730c054599897"
-  integrity sha512-Zpg2Sgc++37kuFl6ppq2Q7Awc6E6AIW671x5PY8E/f7MCIyPPGK/EoeZXvvY3P42exZ3Q4/t3YOzP/HiN79jDg==
+  version "7.10.4"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz#ca91ef46303530448b906652bac2e9fe9941f699"
+  integrity sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==
   dependencies:
-    "@babel/helper-plugin-utils" "^7.8.3"
+    "@babel/helper-plugin-utils" "^7.10.4"
 
 "@babel/plugin-syntax-nullish-coalescing-operator@^7.8.3":
   version "7.8.3"
     "@babel/helper-plugin-utils" "^7.8.0"
 
 "@babel/plugin-syntax-numeric-separator@^7.8.3":
-  version "7.8.3"
-  resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.8.3.tgz#0e3fb63e09bea1b11e96467271c8308007e7c41f"
-  integrity sha512-H7dCMAdN83PcCmqmkHB5dtp+Xa9a6LKSvA2hiFBC/5alSHxM5VgWZXFqDi0YFe8XNGT6iCa+z4V4zSt/PdZ7Dw==
+  version "7.10.4"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz#b9b070b3e33570cd9fd07ba7fa91c0dd37b9af97"
+  integrity sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==
   dependencies:
-    "@babel/helper-plugin-utils" "^7.8.3"
+    "@babel/helper-plugin-utils" "^7.10.4"
 
 "@babel/plugin-syntax-object-rest-spread@^7.8.3":
   version "7.8.3"
   dependencies:
     "@babel/helper-plugin-utils" "^7.8.0"
 
-"@babel/runtime-corejs3@^7.7.4":
-  version "7.8.4"
-  resolved "https://registry.yarnpkg.com/@babel/runtime-corejs3/-/runtime-corejs3-7.8.4.tgz#ccc4e042e2fae419c67fa709567e5d2179ed3940"
-  integrity sha512-+wpLqy5+fbQhvbllvlJEVRIpYj+COUWnnsm+I4jZlA8Lo7/MJmBhGTCHyk1/RWfOqBRJ2MbadddG6QltTKTlrg==
-  dependencies:
-    core-js-pure "^3.0.0"
-    regenerator-runtime "^0.13.2"
-
-"@babel/runtime-corejs3@^7.8.3":
-  version "7.8.7"
-  resolved "https://registry.yarnpkg.com/@babel/runtime-corejs3/-/runtime-corejs3-7.8.7.tgz#8209d9dff2f33aa2616cb319c83fe159ffb07b8c"
-  integrity sha512-sc7A+H4I8kTd7S61dgB9RomXu/C+F4IrRr4Ytze4dnfx7AXEpCrejSNpjx7vq6y/Bak9S6Kbk65a/WgMLtg43Q==
+"@babel/runtime-corejs3@^7.10.2", "@babel/runtime-corejs3@^7.8.3":
+  version "7.10.5"
+  resolved "https://registry.yarnpkg.com/@babel/runtime-corejs3/-/runtime-corejs3-7.10.5.tgz#a57fe6c13045ca33768a2aa527ead795146febe1"
+  integrity sha512-RMafpmrNB5E/bwdSphLr8a8++9TosnyJp98RZzI6VOx2R2CCMpsXXXRvmI700O9oEKpXdZat6oEK68/F0zjd4A==
   dependencies:
     core-js-pure "^3.0.0"
     regenerator-runtime "^0.13.4"
 
-"@babel/runtime@^7.1.2", "@babel/runtime@^7.3.1", "@babel/runtime@^7.4.5", "@babel/runtime@^7.6.3", "@babel/runtime@^7.7.4":
-  version "7.8.4"
-  resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.8.4.tgz#d79f5a2040f7caa24d53e563aad49cbc05581308"
-  integrity sha512-neAp3zt80trRVBI1x0azq6c57aNBqYZH8KhMm3TaB7wEI5Q4A2SHfBHE8w9gOhI/lrqxtEbXZgQIrHP+wvSGwQ==
-  dependencies:
-    regenerator-runtime "^0.13.2"
-
-"@babel/template@^7.7.4", "@babel/template@^7.8.6":
-  version "7.8.6"
-  resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.8.6.tgz#86b22af15f828dfb086474f964dcc3e39c43ce2b"
-  integrity sha512-zbMsPMy/v0PWFZEhQJ66bqjhH+z0JgMoBWuikXybgG3Gkd/3t5oQ1Rw2WQhnSrsOmsKXnZOx15tkC4qON/+JPg==
+"@babel/runtime@^7.1.2", "@babel/runtime@^7.10.1", "@babel/runtime@^7.10.2":
+  version "7.10.5"
+  resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.10.5.tgz#303d8bd440ecd5a491eae6117fd3367698674c5c"
+  integrity sha512-otddXKhdNn7d0ptoFRHtMLa8LqDxLYwTjB4nYgM1yy5N6gU/MUf8zqyyLltCH3yAVitBzmwK4us+DD0l/MauAg==
   dependencies:
-    "@babel/code-frame" "^7.8.3"
-    "@babel/parser" "^7.8.6"
-    "@babel/types" "^7.8.6"
+    regenerator-runtime "^0.13.4"
 
-"@babel/template@^7.8.3":
-  version "7.8.3"
-  resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.8.3.tgz#e02ad04fe262a657809327f578056ca15fd4d1b8"
-  integrity sha512-04m87AcQgAFdvuoyiQ2kgELr2tV8B4fP/xJAVUL3Yb3bkNdMedD3d0rlSQr3PegP0cms3eHjl1F7PWlvWbU8FQ==
-  dependencies:
-    "@babel/code-frame" "^7.8.3"
-    "@babel/parser" "^7.8.3"
-    "@babel/types" "^7.8.3"
-
-"@babel/traverse@^7.1.0", "@babel/traverse@^7.7.4", "@babel/traverse@^7.8.6", "@babel/traverse@^7.9.0":
-  version "7.9.5"
-  resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.9.5.tgz#6e7c56b44e2ac7011a948c21e283ddd9d9db97a2"
-  integrity sha512-c4gH3jsvSuGUezlP6rzSJ6jf8fYjLj3hsMZRx/nX0h+fmHN0w+ekubRrHPqnMec0meycA2nwCsJ7dC8IPem2FQ==
-  dependencies:
-    "@babel/code-frame" "^7.8.3"
-    "@babel/generator" "^7.9.5"
-    "@babel/helper-function-name" "^7.9.5"
-    "@babel/helper-split-export-declaration" "^7.8.3"
-    "@babel/parser" "^7.9.0"
-    "@babel/types" "^7.9.5"
-    debug "^4.1.0"
-    globals "^11.1.0"
-    lodash "^4.17.13"
-
-"@babel/traverse@^7.7.0":
-  version "7.8.6"
-  resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.8.6.tgz#acfe0c64e1cd991b3e32eae813a6eb564954b5ff"
-  integrity sha512-2B8l0db/DPi8iinITKuo7cbPznLCEk0kCxDoB9/N6gGNg/gxOXiR/IcymAFPiBwk5w6TtQ27w4wpElgp9btR9A==
-  dependencies:
-    "@babel/code-frame" "^7.8.3"
-    "@babel/generator" "^7.8.6"
-    "@babel/helper-function-name" "^7.8.3"
-    "@babel/helper-split-export-declaration" "^7.8.3"
-    "@babel/parser" "^7.8.6"
-    "@babel/types" "^7.8.6"
+"@babel/template@^7.10.4", "@babel/template@^7.3.3":
+  version "7.10.4"
+  resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.10.4.tgz#3251996c4200ebc71d1a8fc405fba940f36ba278"
+  integrity sha512-ZCjD27cGJFUB6nmCB1Enki3r+L5kJveX9pq1SvAUKoICy6CZ9yD8xO086YXdYhvNjBdnekm4ZnaP5yC8Cs/1tA==
+  dependencies:
+    "@babel/code-frame" "^7.10.4"
+    "@babel/parser" "^7.10.4"
+    "@babel/types" "^7.10.4"
+
+"@babel/traverse@^7.1.0", "@babel/traverse@^7.10.4", "@babel/traverse@^7.10.5", "@babel/traverse@^7.7.0":
+  version "7.10.5"
+  resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.10.5.tgz#77ce464f5b258be265af618d8fddf0536f20b564"
+  integrity sha512-yc/fyv2gUjPqzTz0WHeRJH2pv7jA9kA7mBX2tXl/x5iOE81uaVPuGPtaYk7wmkx4b67mQ7NqI8rmT2pF47KYKQ==
+  dependencies:
+    "@babel/code-frame" "^7.10.4"
+    "@babel/generator" "^7.10.5"
+    "@babel/helper-function-name" "^7.10.4"
+    "@babel/helper-split-export-declaration" "^7.10.4"
+    "@babel/parser" "^7.10.5"
+    "@babel/types" "^7.10.5"
     debug "^4.1.0"
     globals "^11.1.0"
-    lodash "^4.17.13"
-
-"@babel/types@^7.0.0", "@babel/types@^7.3.0", "@babel/types@^7.9.0", "@babel/types@^7.9.5":
-  version "7.9.5"
-  resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.9.5.tgz#89231f82915a8a566a703b3b20133f73da6b9444"
-  integrity sha512-XjnvNqenk818r5zMaba+sLQjnbda31UfUURv3ei0qPQw4u+j2jMyJ5b11y8ZHYTRSI3NnInQkkkRT4fLqqPdHg==
-  dependencies:
-    "@babel/helper-validator-identifier" "^7.9.5"
-    lodash "^4.17.13"
-    to-fast-properties "^2.0.0"
-
-"@babel/types@^7.7.0", "@babel/types@^7.8.6", "@babel/types@^7.8.7":
-  version "7.8.7"
-  resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.8.7.tgz#1fc9729e1acbb2337d5b6977a63979b4819f5d1d"
-  integrity sha512-k2TreEHxFA4CjGkL+GYjRyx35W0Mr7DP5+9q6WMkyKXB+904bYmG40syjMFV0oLlhhFCwWl0vA0DyzTDkwAiJw==
-  dependencies:
-    esutils "^2.0.2"
-    lodash "^4.17.13"
-    to-fast-properties "^2.0.0"
+    lodash "^4.17.19"
 
-"@babel/types@^7.8.3":
-  version "7.8.3"
-  resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.8.3.tgz#5a383dffa5416db1b73dedffd311ffd0788fb31c"
-  integrity sha512-jBD+G8+LWpMBBWvVcdr4QysjUE4mU/syrhN17o1u3gx0/WzJB1kwiVZAXRtWbsIPOwW8pF/YJV5+nmetPzepXg==
+"@babel/types@^7.0.0", "@babel/types@^7.10.4", "@babel/types@^7.10.5", "@babel/types@^7.3.0", "@babel/types@^7.3.3", "@babel/types@^7.7.0":
+  version "7.10.5"
+  resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.10.5.tgz#d88ae7e2fde86bfbfe851d4d81afa70a997b5d15"
+  integrity sha512-ixV66KWfCI6GKoA/2H9v6bQdbfXEwwpOdQ8cRvb4F+eyvhlaHxWFMQB4+3d9QFJXZsiiiqVrewNV0DFEQpyT4Q==
   dependencies:
-    esutils "^2.0.2"
-    lodash "^4.17.13"
+    "@babel/helper-validator-identifier" "^7.10.4"
+    lodash "^4.17.19"
     to-fast-properties "^2.0.0"
 
 "@bcoe/v8-coverage@^0.2.3":
     exec-sh "^0.3.2"
     minimist "^1.2.0"
 
+"@iarna/cli@^1.2.0":
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/@iarna/cli/-/cli-1.2.0.tgz#0f7af5e851afe895104583c4ca07377a8094d641"
+  integrity sha512-ukITQAqVs2n9HGmn3car/Ir7d3ta650iXhrG7pjr3EWdFmJuuOVWgYsu7ftsSe5VifEFFhjxVuX9+8F7L8hwcA==
+  dependencies:
+    signal-exit "^3.0.2"
+    update-notifier "^2.2.0"
+    yargs "^8.0.2"
+
 "@istanbuljs/load-nyc-config@^1.0.0":
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/@istanbuljs/load-nyc-config/-/load-nyc-config-1.0.0.tgz#10602de5570baea82f8afbfa2630b24e7a8cfe5b"
-  integrity sha512-ZR0rq/f/E4f4XcgnDvtMWXCUJpi8eO0rssVhmztsZqLIEFA9UUP9zmpE0VxlM+kv/E1ul2I876Fwil2ayptDVg==
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz#fd3db1d59ecf7cf121e80650bb86712f9b55eced"
+  integrity sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==
   dependencies:
     camelcase "^5.3.1"
     find-up "^4.1.0"
+    get-package-type "^0.1.0"
     js-yaml "^3.13.1"
     resolve-from "^5.0.0"
 
   resolved "https://registry.yarnpkg.com/@istanbuljs/schema/-/schema-0.1.2.tgz#26520bf09abe4a5644cd5414e37125a8954241dd"
   integrity sha512-tsAQNx32a8CoFhjhijUIhI4kccIAgmGhy8LZMZgGfmXcpMbPRUqn5LWmgRttILi6yeGmBJd2xsPkFMs0PzgPCw==
 
-"@jest/console@^25.4.0":
-  version "25.4.0"
-  resolved "https://registry.yarnpkg.com/@jest/console/-/console-25.4.0.tgz#e2760b532701137801ba824dcff6bc822c961bac"
-  integrity sha512-CfE0erx4hdJ6t7RzAcE1wLG6ZzsHSmybvIBQDoCkDM1QaSeWL9wJMzID/2BbHHa7ll9SsbbK43HjbERbBaFX2A==
+"@jest/console@^26.1.0":
+  version "26.1.0"
+  resolved "https://registry.yarnpkg.com/@jest/console/-/console-26.1.0.tgz#f67c89e4f4d04dbcf7b052aed5ab9c74f915b954"
+  integrity sha512-+0lpTHMd/8pJp+Nd4lyip+/Iyf2dZJvcCqrlkeZQoQid+JlThA4M9vxHtheyrQ99jJTMQam+es4BcvZ5W5cC3A==
   dependencies:
-    "@jest/types" "^25.4.0"
-    chalk "^3.0.0"
-    jest-message-util "^25.4.0"
-    jest-util "^25.4.0"
+    "@jest/types" "^26.1.0"
+    chalk "^4.0.0"
+    jest-message-util "^26.1.0"
+    jest-util "^26.1.0"
     slash "^3.0.0"
 
-"@jest/core@^25.4.0":
-  version "25.4.0"
-  resolved "https://registry.yarnpkg.com/@jest/core/-/core-25.4.0.tgz#cc1fe078df69b8f0fbb023bb0bcee23ef3b89411"
-  integrity sha512-h1x9WSVV0+TKVtATGjyQIMJENs8aF6eUjnCoi4jyRemYZmekLr8EJOGQqTWEX8W6SbZ6Skesy9pGXrKeAolUJw==
+"@jest/core@^26.1.0":
+  version "26.1.0"
+  resolved "https://registry.yarnpkg.com/@jest/core/-/core-26.1.0.tgz#4580555b522de412a7998b3938c851e4f9da1c18"
+  integrity sha512-zyizYmDJOOVke4OO/De//aiv8b07OwZzL2cfsvWF3q9YssfpcKfcnZAwDY8f+A76xXSMMYe8i/f/LPocLlByfw==
   dependencies:
-    "@jest/console" "^25.4.0"
-    "@jest/reporters" "^25.4.0"
-    "@jest/test-result" "^25.4.0"
-    "@jest/transform" "^25.4.0"
-    "@jest/types" "^25.4.0"
+    "@jest/console" "^26.1.0"
+    "@jest/reporters" "^26.1.0"
+    "@jest/test-result" "^26.1.0"
+    "@jest/transform" "^26.1.0"
+    "@jest/types" "^26.1.0"
     ansi-escapes "^4.2.1"
-    chalk "^3.0.0"
+    chalk "^4.0.0"
     exit "^0.1.2"
-    graceful-fs "^4.2.3"
-    jest-changed-files "^25.4.0"
-    jest-config "^25.4.0"
-    jest-haste-map "^25.4.0"
-    jest-message-util "^25.4.0"
-    jest-regex-util "^25.2.6"
-    jest-resolve "^25.4.0"
-    jest-resolve-dependencies "^25.4.0"
-    jest-runner "^25.4.0"
-    jest-runtime "^25.4.0"
-    jest-snapshot "^25.4.0"
-    jest-util "^25.4.0"
-    jest-validate "^25.4.0"
-    jest-watcher "^25.4.0"
+    graceful-fs "^4.2.4"
+    jest-changed-files "^26.1.0"
+    jest-config "^26.1.0"
+    jest-haste-map "^26.1.0"
+    jest-message-util "^26.1.0"
+    jest-regex-util "^26.0.0"
+    jest-resolve "^26.1.0"
+    jest-resolve-dependencies "^26.1.0"
+    jest-runner "^26.1.0"
+    jest-runtime "^26.1.0"
+    jest-snapshot "^26.1.0"
+    jest-util "^26.1.0"
+    jest-validate "^26.1.0"
+    jest-watcher "^26.1.0"
     micromatch "^4.0.2"
     p-each-series "^2.1.0"
-    realpath-native "^2.0.0"
     rimraf "^3.0.0"
     slash "^3.0.0"
     strip-ansi "^6.0.0"
 
-"@jest/environment@^25.4.0":
-  version "25.4.0"
-  resolved "https://registry.yarnpkg.com/@jest/environment/-/environment-25.4.0.tgz#45071f525f0d8c5a51ed2b04fd42b55a8f0c7cb3"
-  integrity sha512-KDctiak4mu7b4J6BIoN/+LUL3pscBzoUCP+EtSPd2tK9fqyDY5OF+CmkBywkFWezS9tyH5ACOQNtpjtueEDH6Q==
+"@jest/environment@^26.1.0":
+  version "26.1.0"
+  resolved "https://registry.yarnpkg.com/@jest/environment/-/environment-26.1.0.tgz#378853bcdd1c2443b4555ab908cfbabb851e96da"
+  integrity sha512-86+DNcGongbX7ai/KE/S3/NcUVZfrwvFzOOWX/W+OOTvTds7j07LtC+MgGydH5c8Ri3uIrvdmVgd1xFD5zt/xA==
+  dependencies:
+    "@jest/fake-timers" "^26.1.0"
+    "@jest/types" "^26.1.0"
+    jest-mock "^26.1.0"
+
+"@jest/fake-timers@^26.1.0":
+  version "26.1.0"
+  resolved "https://registry.yarnpkg.com/@jest/fake-timers/-/fake-timers-26.1.0.tgz#9a76b7a94c351cdbc0ad53e5a748789f819a65fe"
+  integrity sha512-Y5F3kBVWxhau3TJ825iuWy++BAuQzK/xEa+wD9vDH3RytW9f2DbMVodfUQC54rZDX3POqdxCgcKdgcOL0rYUpA==
   dependencies:
-    "@jest/fake-timers" "^25.4.0"
-    "@jest/types" "^25.4.0"
-    jest-mock "^25.4.0"
+    "@jest/types" "^26.1.0"
+    "@sinonjs/fake-timers" "^6.0.1"
+    jest-message-util "^26.1.0"
+    jest-mock "^26.1.0"
+    jest-util "^26.1.0"
 
-"@jest/fake-timers@^25.4.0":
-  version "25.4.0"
-  resolved "https://registry.yarnpkg.com/@jest/fake-timers/-/fake-timers-25.4.0.tgz#3a9a4289ba836abd084953dca406389a57e00fbd"
-  integrity sha512-lI9z+VOmVX4dPPFzyj0vm+UtaB8dCJJ852lcDnY0uCPRvZAaVGnMwBBc1wxtf+h7Vz6KszoOvKAt4QijDnHDkg==
+"@jest/globals@^26.1.0":
+  version "26.1.0"
+  resolved "https://registry.yarnpkg.com/@jest/globals/-/globals-26.1.0.tgz#6cc5d7cbb79b76b120f2403d7d755693cf063ab1"
+  integrity sha512-MKiHPNaT+ZoG85oMaYUmGHEqu98y3WO2yeIDJrs2sJqHhYOy3Z6F7F/luzFomRQ8SQ1wEkmahFAz2291Iv8EAw==
   dependencies:
-    "@jest/types" "^25.4.0"
-    jest-message-util "^25.4.0"
-    jest-mock "^25.4.0"
-    jest-util "^25.4.0"
-    lolex "^5.0.0"
+    "@jest/environment" "^26.1.0"
+    "@jest/types" "^26.1.0"
+    expect "^26.1.0"
 
-"@jest/reporters@^25.4.0":
-  version "25.4.0"
-  resolved "https://registry.yarnpkg.com/@jest/reporters/-/reporters-25.4.0.tgz#836093433b32ce4e866298af2d6fcf6ed351b0b0"
-  integrity sha512-bhx/buYbZgLZm4JWLcRJ/q9Gvmd3oUh7k2V7gA4ZYBx6J28pIuykIouclRdiAC6eGVX1uRZT+GK4CQJLd/PwPg==
+"@jest/reporters@^26.1.0":
+  version "26.1.0"
+  resolved "https://registry.yarnpkg.com/@jest/reporters/-/reporters-26.1.0.tgz#08952e90c90282e14ff49e927bdf1873617dae78"
+  integrity sha512-SVAysur9FOIojJbF4wLP0TybmqwDkdnFxHSPzHMMIYyBtldCW9gG+Q5xWjpMFyErDiwlRuPyMSJSU64A67Pazg==
   dependencies:
     "@bcoe/v8-coverage" "^0.2.3"
-    "@jest/console" "^25.4.0"
-    "@jest/test-result" "^25.4.0"
-    "@jest/transform" "^25.4.0"
-    "@jest/types" "^25.4.0"
-    chalk "^3.0.0"
+    "@jest/console" "^26.1.0"
+    "@jest/test-result" "^26.1.0"
+    "@jest/transform" "^26.1.0"
+    "@jest/types" "^26.1.0"
+    chalk "^4.0.0"
     collect-v8-coverage "^1.0.0"
     exit "^0.1.2"
     glob "^7.1.2"
+    graceful-fs "^4.2.4"
     istanbul-lib-coverage "^3.0.0"
-    istanbul-lib-instrument "^4.0.0"
+    istanbul-lib-instrument "^4.0.3"
     istanbul-lib-report "^3.0.0"
     istanbul-lib-source-maps "^4.0.0"
     istanbul-reports "^3.0.2"
-    jest-haste-map "^25.4.0"
-    jest-resolve "^25.4.0"
-    jest-util "^25.4.0"
-    jest-worker "^25.4.0"
+    jest-haste-map "^26.1.0"
+    jest-resolve "^26.1.0"
+    jest-util "^26.1.0"
+    jest-worker "^26.1.0"
     slash "^3.0.0"
     source-map "^0.6.0"
-    string-length "^3.1.0"
+    string-length "^4.0.1"
     terminal-link "^2.0.0"
     v8-to-istanbul "^4.1.3"
   optionalDependencies:
-    node-notifier "^6.0.0"
+    node-notifier "^7.0.0"
 
-"@jest/source-map@^25.2.6":
-  version "25.2.6"
-  resolved "https://registry.yarnpkg.com/@jest/source-map/-/source-map-25.2.6.tgz#0ef2209514c6d445ebccea1438c55647f22abb4c"
-  integrity sha512-VuIRZF8M2zxYFGTEhkNSvQkUKafQro4y+mwUxy5ewRqs5N/ynSFUODYp3fy1zCnbCMy1pz3k+u57uCqx8QRSQQ==
+"@jest/source-map@^26.1.0":
+  version "26.1.0"
+  resolved "https://registry.yarnpkg.com/@jest/source-map/-/source-map-26.1.0.tgz#a6a020d00e7d9478f4b690167c5e8b77e63adb26"
+  integrity sha512-XYRPYx4eEVX15cMT9mstnO7hkHP3krNtKfxUYd8L7gbtia8JvZZ6bMzSwa6IQJENbudTwKMw5R1BePRD+bkEmA==
   dependencies:
     callsites "^3.0.0"
-    graceful-fs "^4.2.3"
+    graceful-fs "^4.2.4"
     source-map "^0.6.0"
 
-"@jest/test-result@^25.4.0":
-  version "25.4.0"
-  resolved "https://registry.yarnpkg.com/@jest/test-result/-/test-result-25.4.0.tgz#6f2ec2c8da9981ef013ad8651c1c6f0cb20c6324"
-  integrity sha512-8BAKPaMCHlL941eyfqhWbmp3MebtzywlxzV+qtngQ3FH+RBqnoSAhNEPj4MG7d2NVUrMOVfrwuzGpVIK+QnMAA==
+"@jest/test-result@^26.1.0":
+  version "26.1.0"
+  resolved "https://registry.yarnpkg.com/@jest/test-result/-/test-result-26.1.0.tgz#a93fa15b21ad3c7ceb21c2b4c35be2e407d8e971"
+  integrity sha512-Xz44mhXph93EYMA8aYDz+75mFbarTV/d/x0yMdI3tfSRs/vh4CqSxgzVmCps1fPkHDCtn0tU8IH9iCKgGeGpfw==
   dependencies:
-    "@jest/console" "^25.4.0"
-    "@jest/types" "^25.4.0"
+    "@jest/console" "^26.1.0"
+    "@jest/types" "^26.1.0"
     "@types/istanbul-lib-coverage" "^2.0.0"
     collect-v8-coverage "^1.0.0"
 
-"@jest/test-sequencer@^25.4.0":
-  version "25.4.0"
-  resolved "https://registry.yarnpkg.com/@jest/test-sequencer/-/test-sequencer-25.4.0.tgz#2b96f9d37f18dc3336b28e3c8070f97f9f55f43b"
-  integrity sha512-240cI+nsM3attx2bMp9uGjjHrwrpvxxrZi8Tyqp/cfOzl98oZXVakXBgxODGyBYAy/UGXPKXLvNc2GaqItrsJg==
+"@jest/test-sequencer@^26.1.0":
+  version "26.1.0"
+  resolved "https://registry.yarnpkg.com/@jest/test-sequencer/-/test-sequencer-26.1.0.tgz#41a6fc8b850c3f33f48288ea9ea517c047e7f14e"
+  integrity sha512-Z/hcK+rTq56E6sBwMoQhSRDVjqrGtj1y14e2bIgcowARaIE1SgOanwx6gvY4Q9gTKMoZQXbXvptji+q5GYxa6Q==
   dependencies:
-    "@jest/test-result" "^25.4.0"
-    jest-haste-map "^25.4.0"
-    jest-runner "^25.4.0"
-    jest-runtime "^25.4.0"
+    "@jest/test-result" "^26.1.0"
+    graceful-fs "^4.2.4"
+    jest-haste-map "^26.1.0"
+    jest-runner "^26.1.0"
+    jest-runtime "^26.1.0"
 
-"@jest/transform@^25.4.0":
-  version "25.4.0"
-  resolved "https://registry.yarnpkg.com/@jest/transform/-/transform-25.4.0.tgz#eef36f0367d639e2fd93dccd758550377fbb9962"
-  integrity sha512-t1w2S6V1sk++1HHsxboWxPEuSpN8pxEvNrZN+Ud/knkROWtf8LeUmz73A4ezE8476a5AM00IZr9a8FO9x1+j3g==
+"@jest/transform@^26.1.0":
+  version "26.1.0"
+  resolved "https://registry.yarnpkg.com/@jest/transform/-/transform-26.1.0.tgz#697f48898c2a2787c9b4cb71d09d7e617464e509"
+  integrity sha512-ICPm6sUXmZJieq45ix28k0s+d/z2E8CHDsq+WwtWI6kW8m7I8kPqarSEcUN86entHQ570ZBRci5OWaKL0wlAWw==
   dependencies:
     "@babel/core" "^7.1.0"
-    "@jest/types" "^25.4.0"
+    "@jest/types" "^26.1.0"
     babel-plugin-istanbul "^6.0.0"
-    chalk "^3.0.0"
+    chalk "^4.0.0"
     convert-source-map "^1.4.0"
     fast-json-stable-stringify "^2.0.0"
-    graceful-fs "^4.2.3"
-    jest-haste-map "^25.4.0"
-    jest-regex-util "^25.2.6"
-    jest-util "^25.4.0"
+    graceful-fs "^4.2.4"
+    jest-haste-map "^26.1.0"
+    jest-regex-util "^26.0.0"
+    jest-util "^26.1.0"
     micromatch "^4.0.2"
     pirates "^4.0.1"
-    realpath-native "^2.0.0"
     slash "^3.0.0"
     source-map "^0.6.1"
     write-file-atomic "^3.0.0"
 
-"@jest/types@^25.4.0":
-  version "25.4.0"
-  resolved "https://registry.yarnpkg.com/@jest/types/-/types-25.4.0.tgz#5afeb8f7e1cba153a28e5ac3c9fe3eede7206d59"
-  integrity sha512-XBeaWNzw2PPnGW5aXvZt3+VO60M+34RY3XDsCK5tW7kyj3RK0XClRutCfjqcBuaR2aBQTbluEDME9b5MB9UAPw==
+"@jest/types@^25.5.0":
+  version "25.5.0"
+  resolved "https://registry.yarnpkg.com/@jest/types/-/types-25.5.0.tgz#4d6a4793f7b9599fc3680877b856a97dbccf2a9d"
+  integrity sha512-OXD0RgQ86Tu3MazKo8bnrkDRaDXXMGUqd+kTtLtK1Zb7CRzQcaSRPPPV37SvYTdevXEBVxe0HXylEjs8ibkmCw==
   dependencies:
     "@types/istanbul-lib-coverage" "^2.0.0"
     "@types/istanbul-reports" "^1.1.1"
     "@types/yargs" "^15.0.0"
     chalk "^3.0.0"
 
-"@popperjs/core@^2.2.0":
-  version "2.3.2"
-  resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.3.2.tgz#1e56eb99bccddbda6a3e29aa4f3660f5b23edc43"
-  integrity sha512-18Tz3QghwsuHUC4gTNoxcEw1ClsrJ+lRypYpm+aucQonYNnmskQYvDZZKLHMPvQ7OwthWJl715UEX+Tg2fJkJw==
-
-"@samverschueren/stream-to-observable@^0.3.0":
-  version "0.3.0"
-  resolved "https://registry.yarnpkg.com/@samverschueren/stream-to-observable/-/stream-to-observable-0.3.0.tgz#ecdf48d532c58ea477acfcab80348424f8d0662f"
-  integrity sha512-MI4Xx6LHs4Webyvi6EbspgyAb4D2Q2VtnCQ1blOJcoLS6mVa8lNN2rkIy1CVxfTUpoyIbCTkXES1rLXztFD1lg==
+"@jest/types@^26.1.0":
+  version "26.1.0"
+  resolved "https://registry.yarnpkg.com/@jest/types/-/types-26.1.0.tgz#f8afaaaeeb23b5cad49dd1f7779689941dcb6057"
+  integrity sha512-GXigDDsp6ZlNMhXQDeuy/iYCDsRIHJabWtDzvnn36+aqFfG14JmFV0e/iXxY4SP9vbXSiPNOWdehU5MeqrYHBQ==
   dependencies:
-    any-observable "^0.3.0"
+    "@types/istanbul-lib-coverage" "^2.0.0"
+    "@types/istanbul-reports" "^1.1.1"
+    "@types/yargs" "^15.0.0"
+    chalk "^4.0.0"
+
+"@popperjs/core@^2.4.4":
+  version "2.4.4"
+  resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.4.4.tgz#11d5db19bd178936ec89cd84519c4de439574398"
+  integrity sha512-1oO6+dN5kdIA3sKPZhRGJTfGVP4SWV6KqlMOwry4J3HfyD68sl/3KmG7DeYUzvN+RbhXDnv/D8vNNB8168tAMg==
 
 "@sinonjs/commons@^1.7.0":
-  version "1.7.2"
-  resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-1.7.2.tgz#505f55c74e0272b43f6c52d81946bed7058fc0e2"
-  integrity sha512-+DUO6pnp3udV/v2VfUWgaY5BIE1IfT7lLfeDzPVeMT1XKkaAp9LgSI9x5RtrFQoZ9Oi0PgXQQHPaoKu7dCjVxw==
+  version "1.8.1"
+  resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-1.8.1.tgz#e7df00f98a203324f6dc7cc606cad9d4a8ab2217"
+  integrity sha512-892K+kWUUi3cl+LlqEWIDrhvLgdL79tECi8JZUyq6IviKy/DNhuzCRlbHUjxK89f4ypPMMaFnFuR9Ie6DoIMsw==
   dependencies:
     type-detect "4.0.8"
 
+"@sinonjs/fake-timers@^6.0.1":
+  version "6.0.1"
+  resolved "https://registry.yarnpkg.com/@sinonjs/fake-timers/-/fake-timers-6.0.1.tgz#293674fccb3262ac782c7aadfdeca86b10c75c40"
+  integrity sha512-MZPUxrmFubI36XS1DI3qmI0YdN1gks62JtFZvxR67ljjSNCeK6U08Zx4msEWOXuofgqUt6zPHSi1H9fbjR/NRA==
+  dependencies:
+    "@sinonjs/commons" "^1.7.0"
+
 "@types/autosize@^3.0.6":
   version "3.0.7"
   resolved "https://registry.yarnpkg.com/@types/autosize/-/autosize-3.0.7.tgz#f5da28d7ea4532c8b60573d67ec04fc866fa13db"
   dependencies:
     "@types/jquery" "*"
 
-"@types/babel__core@^7.1.7":
-  version "7.1.7"
-  resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.1.7.tgz#1dacad8840364a57c98d0dd4855c6dd3752c6b89"
-  integrity sha512-RL62NqSFPCDK2FM1pSDH0scHpJvsXtZNiYlMB73DgPBaG1E38ZYVL+ei5EkWRbr+KC4YNiAUNBnRj+bgwpgjMw==
+"@types/babel__core@^7.0.0", "@types/babel__core@^7.1.7":
+  version "7.1.9"
+  resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.1.9.tgz#77e59d438522a6fb898fa43dc3455c6e72f3963d"
+  integrity sha512-sY2RsIJ5rpER1u3/aQ8OFSI7qGIy8o1NEEbgb2UaJcvOtXOMpd39ko723NBpjQFg9SIX7TXtjejZVGeIMLhoOw==
   dependencies:
     "@babel/parser" "^7.1.0"
     "@babel/types" "^7.0.0"
     "@babel/types" "^7.0.0"
 
 "@types/babel__traverse@*", "@types/babel__traverse@^7.0.6":
-  version "7.0.10"
-  resolved "https://registry.yarnpkg.com/@types/babel__traverse/-/babel__traverse-7.0.10.tgz#d9a99f017317d9b3d1abc2ced45d3bca68df0daf"
-  integrity sha512-74fNdUGrWsgIB/V9kTO5FGHPWYY6Eqn+3Z7L6Hc4e/BxjYV7puvBqp5HwsVYYfLm6iURYBNCx4Ut37OF9yitCw==
+  version "7.0.13"
+  resolved "https://registry.yarnpkg.com/@types/babel__traverse/-/babel__traverse-7.0.13.tgz#1874914be974a492e1b4cb00585cabb274e8ba18"
+  integrity sha512-i+zS7t6/s9cdQvbqKDARrcbrPvtJGlbYsMkazo03nTAK3RX9FNrLllXys22uiTGJapPOTZTQ35nHh4ISph4SLQ==
   dependencies:
     "@babel/types" "^7.3.0"
 
   resolved "https://registry.yarnpkg.com/@types/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz#1ee30d79544ca84d68d4b3cdb0af4f205663dd2d"
   integrity sha512-OCutwjDZ4aFS6PB1UZ988C4YgwlBHJd6wCeQqaLdmadZ/7e+w79+hbMUFC1QXDNCmdyoRfAFdm0RypzwR+Qpag==
 
+"@types/graceful-fs@^4.1.2":
+  version "4.1.3"
+  resolved "https://registry.yarnpkg.com/@types/graceful-fs/-/graceful-fs-4.1.3.tgz#039af35fe26bec35003e8d86d2ee9c586354348f"
+  integrity sha512-AiHRaEB50LQg0pZmm659vNBb9f4SJ0qrAnteuzhSeAUcJKxoYgEnprg/83kppCnc2zvtCKbdZry1a5pVY3lOTQ==
+  dependencies:
+    "@types/node" "*"
+
 "@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0", "@types/istanbul-lib-coverage@^2.0.1":
-  version "2.0.1"
-  resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.1.tgz#42995b446db9a48a11a07ec083499a860e9138ff"
-  integrity sha512-hRJD2ahnnpLgsj6KWMYSrmXkM3rm2Dl1qkx6IOFD5FnuNPXJIG5L0dhgKXCYTRMGzU4n0wImQ/xfmRc4POUFlg==
+  version "2.0.3"
+  resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.3.tgz#4ba8ddb720221f432e443bd5f9117fd22cfd4762"
+  integrity sha512-sz7iLqvVUg1gIedBOvlkxPlc8/uVzyS5OwGz1cKjXzkl3FpL3al0crU8YGU1WoHkxn0Wxbw5tyi6hvzJKNzFsw==
 
 "@types/istanbul-lib-report@*":
   version "3.0.0"
     "@types/istanbul-lib-coverage" "*"
 
 "@types/istanbul-reports@^1.1.1":
-  version "1.1.1"
-  resolved "https://registry.yarnpkg.com/@types/istanbul-reports/-/istanbul-reports-1.1.1.tgz#7a8cbf6a406f36c8add871625b278eaf0b0d255a"
-  integrity sha512-UpYjBi8xefVChsCoBpKShdxTllC9pwISirfoZsUa2AAdQg/Jd2KQGtSbw+ya7GPo7x/wAPlH6JBhKhAsXUEZNA==
+  version "1.1.2"
+  resolved "https://registry.yarnpkg.com/@types/istanbul-reports/-/istanbul-reports-1.1.2.tgz#e875cc689e47bce549ec81f3df5e6f6f11cfaeb2"
+  integrity sha512-P/W9yOX/3oPZSpaYOCQzGqgCQRXn0FFO/V8bWrCQs+wLmvVVxk6CRBXALEvNs9OHIatlnlFokfhuDo2ug01ciw==
   dependencies:
     "@types/istanbul-lib-coverage" "*"
     "@types/istanbul-lib-report" "*"
 
-"@types/jest@^25.2.1":
-  version "25.2.1"
-  resolved "https://registry.yarnpkg.com/@types/jest/-/jest-25.2.1.tgz#9544cd438607955381c1bdbdb97767a249297db5"
-  integrity sha512-msra1bCaAeEdkSyA0CZ6gW1ukMIvZ5YoJkdXw/qhQdsuuDlFTcEUrUw8CLCPt2rVRUfXlClVvK2gvPs9IokZaA==
+"@types/jest@^26.0.7":
+  version "26.0.7"
+  resolved "https://registry.yarnpkg.com/@types/jest/-/jest-26.0.7.tgz#495cb1d1818c1699dbc3b8b046baf1c86ef5e324"
+  integrity sha512-+x0077/LoN6MjqBcVOe1y9dpryWnfDZ+Xfo3EqGeBcfPRJlQp3Lw62RvNlWxuGv7kOEwlHriAa54updi3Jvvwg==
   dependencies:
     jest-diff "^25.2.1"
     pretty-format "^25.2.1"
 
 "@types/jquery@*":
-  version "3.3.31"
-  resolved "https://registry.yarnpkg.com/@types/jquery/-/jquery-3.3.31.tgz#27c706e4bf488474e1cb54a71d8303f37c93451b"
-  integrity sha512-Lz4BAJihoFw5nRzKvg4nawXPzutkv7wmfQ5121avptaSIXlDNJCUuxZxX/G+9EVidZGuO0UBlk+YjKbwRKJigg==
+  version "3.5.0"
+  resolved "https://registry.yarnpkg.com/@types/jquery/-/jquery-3.5.0.tgz#ccb7dfd317d02d4227dd3803c75297d0c10dad68"
+  integrity sha512-C7qQUjpMWDUNYQRTXsP5nbYYwCwwgy84yPgoTT7fPN69NH92wLeCtFaMsWeolJD1AF/6uQw3pYt62rzv83sMmw==
   dependencies:
     "@types/sizzle" "*"
 
   integrity sha512-+oY0FDTO2GYKEV0YPvSshGq9t7YozVkgvXLty7zogQNuCxBhT9/3INX9Q7H1aRZ4SUDRXAKlJuA4EA5nTt7SNw==
 
 "@types/json-schema@^7.0.3":
-  version "7.0.4"
-  resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.4.tgz#38fd73ddfd9b55abb1e1b2ed578cb55bd7b7d339"
-  integrity sha512-8+KAKzEvSUdeo+kmqnKrqgeE+LcA0tjYWFY7RPProVYwnqDjukzO+3b6dLD56rYX5TdWejnEOLJYOIeh4CXKuA==
+  version "7.0.5"
+  resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.5.tgz#dcce4430e64b443ba8945f0290fb564ad5bac6dd"
+  integrity sha512-7+2BITlgjgDhH0vvwZU/HZJVyk+2XUlvxXe8dFMedNX/aMkaOq++rMAFXc0tM7ij15QaWlbdQASBR9dihi+bDQ==
+
+"@types/json5@^0.0.29":
+  version "0.0.29"
+  resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee"
+  integrity sha1-7ihweulOEdK4J7y+UnC86n8+ce4=
 
 "@types/jwt-decode@^2.2.1":
   version "2.2.1"
   integrity sha512-Q7DYAOi9O/+cLLhdaSvKdaumWyHbm7HAk/bFwwyTuU0arR5yyCeW5GOoqt4tJTpDRxhpx9Q8kQL6vMpuw9hDSw==
 
 "@types/markdown-it-container@^2.0.2":
-  version "2.0.2"
-  resolved "https://registry.yarnpkg.com/@types/markdown-it-container/-/markdown-it-container-2.0.2.tgz#0e624653415a1c2f088a5ae51f7bfff480c03f49"
-  integrity sha512-T770GL+zJz8Ssh1NpLiOruYhrU96yb8ovPSegLrWY5XIkJc6PVVC7kH/oQaVD0rkePpWMFJK018OgS/pwviOMw==
+  version "2.0.3"
+  resolved "https://registry.yarnpkg.com/@types/markdown-it-container/-/markdown-it-container-2.0.3.tgz#436de4c019d7d71b60f759037fd4d03611569eb8"
+  integrity sha512-ouJluaEGWV7clX7NVMRjkQfS/a11hFXDG1U04l8vrS1P2UgAFPlgMpk1rAPgK0MWU1NhcBYWVW7w/SpgePLs0A==
   dependencies:
     "@types/markdown-it" "*"
 
-"@types/markdown-it@*", "@types/markdown-it@^0.0.9":
-  version "0.0.9"
-  resolved "https://registry.yarnpkg.com/@types/markdown-it/-/markdown-it-0.0.9.tgz#a5d552f95216c478e0a27a5acc1b28dcffd989ce"
-  integrity sha512-IFSepyZXbF4dgSvsk8EsgaQ/8Msv1I5eTL0BZ0X3iGO9jw6tCVtPG8HchIPm3wrkmGdqZOD42kE0zplVi1gYDA==
+"@types/markdown-it@*", "@types/markdown-it@^10.0.1":
+  version "10.0.1"
+  resolved "https://registry.yarnpkg.com/@types/markdown-it/-/markdown-it-10.0.1.tgz#94e252ab689c8e9ceb9aff2946e0a458390105eb"
+  integrity sha512-L1ibTdA5IUe/cRBlf3N3syAOBQSN1WCMGtAWir6mKxibiRl4LmpZM4jLz+7zAqiMnhQuAP1sqZOF9wXgn2kpEg==
   dependencies:
     "@types/linkify-it" "*"
+    "@types/mdurl" "*"
+
+"@types/mdurl@*":
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/@types/mdurl/-/mdurl-1.0.2.tgz#e2ce9d83a613bacf284c7be7d491945e39e1f8e9"
+  integrity sha512-eC4U9MlIcu2q0KQmXszyn5Akca/0jrQmwDRgpAMJai7qBWq4amIQhZyNau4VYGtCeALvW1/NtjzJJ567aZxfKA==
 
 "@types/node-fetch@^2.5.6":
-  version "2.5.6"
-  resolved "https://registry.yarnpkg.com/@types/node-fetch/-/node-fetch-2.5.6.tgz#df8377a66e64ddf75b65b072e37b3c5c5425a96f"
-  integrity sha512-2w0NTwMWF1d3NJMK0Uiq2UNN8htVCyOWOD0jIPjPgC5Ph/YP4dVhs9YxxcMcuLuwAslz0dVEcZQUaqkLs3IzOQ==
+  version "2.5.7"
+  resolved "https://registry.yarnpkg.com/@types/node-fetch/-/node-fetch-2.5.7.tgz#20a2afffa882ab04d44ca786449a276f9f6bbf3c"
+  integrity sha512-o2WVNf5UhWRkxlf6eq+jMZDu7kjgpgJfl4xVNlvryc95O/6F2ld8ztKX+qu+Rjyet93WAWm5LjeX9H5FGkODvw==
   dependencies:
     "@types/node" "*"
     form-data "^3.0.0"
 
-"@types/node@*":
-  version "13.13.2"
-  resolved "https://registry.yarnpkg.com/@types/node/-/node-13.13.2.tgz#160d82623610db590a64e8ca81784e11117e5a54"
-  integrity sha512-LB2R1Oyhpg8gu4SON/mfforE525+Hi/M1ineICEDftqNVTyFg1aRIeGuTvXAoWHc4nbrFncWtJgMmoyRvuGh7A==
-
-"@types/node@^13.11.1":
-  version "13.11.1"
-  resolved "https://registry.yarnpkg.com/@types/node/-/node-13.11.1.tgz#49a2a83df9d26daacead30d0ccc8762b128d53c7"
-  integrity sha512-eWQGP3qtxwL8FGneRrC5DwrJLGN4/dH1clNTuLfN81HCrxVtxRjygDTUoZJ5ASlDEeo0ppYFQjQIlXhtXpOn6g==
+"@types/node@*", "@types/node@^14.0.26":
+  version "14.0.26"
+  resolved "https://registry.yarnpkg.com/@types/node/-/node-14.0.26.tgz#22a3b8a46510da8944b67bfc27df02c34a35331c"
+  integrity sha512-W+fpe5s91FBGE0pEa0lnqGLL4USgpLgs4nokw16SrBBco/gQxuua7KnArSEOd5iaMqbbSHV10vUDkJYJJqpXKA==
 
 "@types/normalize-package-data@^2.4.0":
   version "2.4.0"
   resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0"
   integrity sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==
 
-"@types/prettier@^1.19.0":
-  version "1.19.1"
-  resolved "https://registry.yarnpkg.com/@types/prettier/-/prettier-1.19.1.tgz#33509849f8e679e4add158959fdb086440e9553f"
-  integrity sha512-5qOlnZscTn4xxM5MeGXAMOsIOIKIbh9e85zJWfBRVPlRMEVawzoPhINYbRGkBZCI8LxvBe7tJCdWiarA99OZfQ==
+"@types/prettier@^2.0.0":
+  version "2.0.2"
+  resolved "https://registry.yarnpkg.com/@types/prettier/-/prettier-2.0.2.tgz#5bb52ee68d0f8efa9cc0099920e56be6cc4e37f3"
+  integrity sha512-IkVfat549ggtkZUthUzEX49562eGikhSYeVGX97SkMFn+sTZrgRewXjQ4tPKFPCykZHkX1Zfd9OoELGqKU2jJA==
 
 "@types/sizzle@*":
   version "2.3.2"
   integrity sha512-FA/BWv8t8ZWJ+gEOnLLd8ygxH/2UFbAvgEonyfN6yWGLKc7zVjbpl2Y4CTjid9h2RfgPP6SEt6uHwEOply00yw==
 
 "@types/yargs@^15.0.0":
-  version "15.0.4"
-  resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-15.0.4.tgz#7e5d0f8ca25e9d5849f2ea443cf7c402decd8299"
-  integrity sha512-9T1auFmbPZoxHz0enUFlUuKRy3it01R+hlggyVUMtnCTQRunsQYifnSGb8hET4Xo8yiC0o0r1paW3ud5+rbURg==
+  version "15.0.5"
+  resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-15.0.5.tgz#947e9a6561483bdee9adffc983e91a6902af8b79"
+  integrity sha512-Dk/IDOPtOgubt/IaevIUbTgV7doaKkoorvOyYM2CMwuDyP89bekI7H4xLIwunNYiK9jhCkmc6pUrJk3cj2AB9w==
   dependencies:
     "@types/yargs-parser" "*"
 
-"@typescript-eslint/eslint-plugin@2.24.0":
-  version "2.24.0"
-  resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-2.24.0.tgz#a86cf618c965a462cddf3601f594544b134d6d68"
-  integrity sha512-wJRBeaMeT7RLQ27UQkDFOu25MqFOBus8PtOa9KaT5ZuxC1kAsd7JEHqWt4YXuY9eancX0GK9C68i5OROnlIzBA==
+"@typescript-eslint/eslint-plugin@3.6.1":
+  version "3.6.1"
+  resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-3.6.1.tgz#5ced8fd2087fbb83a76973dea4a0d39d9cb4a642"
+  integrity sha512-06lfjo76naNeOMDl+mWG9Fh/a0UHKLGhin+mGaIw72FUMbMGBkdi/FEJmgEDzh4eE73KIYzHWvOCYJ0ak7nrJQ==
   dependencies:
-    "@typescript-eslint/experimental-utils" "2.24.0"
-    eslint-utils "^1.4.3"
+    "@typescript-eslint/experimental-utils" "3.6.1"
+    debug "^4.1.1"
     functional-red-black-tree "^1.0.1"
     regexpp "^3.0.0"
+    semver "^7.3.2"
     tsutils "^3.17.1"
 
-"@typescript-eslint/experimental-utils@2.24.0":
-  version "2.24.0"
-  resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-2.24.0.tgz#a5cb2ed89fedf8b59638dc83484eb0c8c35e1143"
-  integrity sha512-DXrwuXTdVh3ycNCMYmWhUzn/gfqu9N0VzNnahjiDJvcyhfBy4gb59ncVZVxdp5XzBC77dCncu0daQgOkbvPwBw==
+"@typescript-eslint/experimental-utils@3.6.1":
+  version "3.6.1"
+  resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-3.6.1.tgz#b5a2738ebbceb3fa90c5b07d50bb1225403c4a54"
+  integrity sha512-oS+hihzQE5M84ewXrTlVx7eTgc52eu+sVmG7ayLfOhyZmJ8Unvf3osyFQNADHP26yoThFfbxcibbO0d2FjnYhg==
   dependencies:
     "@types/json-schema" "^7.0.3"
-    "@typescript-eslint/typescript-estree" "2.24.0"
+    "@typescript-eslint/types" "3.6.1"
+    "@typescript-eslint/typescript-estree" "3.6.1"
     eslint-scope "^5.0.0"
+    eslint-utils "^2.0.0"
 
 "@typescript-eslint/experimental-utils@^2.5.0":
-  version "2.18.0"
-  resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-2.18.0.tgz#e4eab839082030282496c1439bbf9fdf2a4f3da8"
-  integrity sha512-J6MopKPHuJYmQUkANLip7g9I82ZLe1naCbxZZW3O2sIxTiq/9YYoOELEKY7oPg0hJ0V/AQ225h2z0Yp+RRMXhw==
+  version "2.34.0"
+  resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-2.34.0.tgz#d3524b644cdb40eebceca67f8cf3e4cc9c8f980f"
+  integrity sha512-eS6FTkq+wuMJ+sgtuNTtcqavWXqsflWcfBnlYhg/nS4aZ1leewkXGbvBhaapn1q6qf4M71bsR1tez5JTRMuqwA==
   dependencies:
     "@types/json-schema" "^7.0.3"
-    "@typescript-eslint/typescript-estree" "2.18.0"
+    "@typescript-eslint/typescript-estree" "2.34.0"
     eslint-scope "^5.0.0"
+    eslint-utils "^2.0.0"
 
-"@typescript-eslint/parser@2.24.0":
-  version "2.24.0"
-  resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-2.24.0.tgz#2cf0eae6e6dd44d162486ad949c126b887f11eb8"
-  integrity sha512-H2Y7uacwSSg8IbVxdYExSI3T7uM1DzmOn2COGtCahCC3g8YtM1xYAPi2MAHyfPs61VKxP/J/UiSctcRgw4G8aw==
+"@typescript-eslint/parser@3.6.1":
+  version "3.6.1"
+  resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-3.6.1.tgz#216e8adf4ee9c629f77c985476a2ea07fb80e1dc"
+  integrity sha512-SLihQU8RMe77YJ/jGTqOt0lMq7k3hlPVfp7v/cxMnXA9T0bQYoMDfTsNgHXpwSJM1Iq2aAJ8WqekxUwGv5F67Q==
   dependencies:
     "@types/eslint-visitor-keys" "^1.0.0"
-    "@typescript-eslint/experimental-utils" "2.24.0"
-    "@typescript-eslint/typescript-estree" "2.24.0"
+    "@typescript-eslint/experimental-utils" "3.6.1"
+    "@typescript-eslint/types" "3.6.1"
+    "@typescript-eslint/typescript-estree" "3.6.1"
     eslint-visitor-keys "^1.1.0"
 
-"@typescript-eslint/typescript-estree@2.18.0":
-  version "2.18.0"
-  resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-2.18.0.tgz#cfbd16ed1b111166617d718619c19b62764c8460"
-  integrity sha512-gVHylf7FDb8VSi2ypFuEL3hOtoC4HkZZ5dOjXvVjoyKdRrvXAOPSzpNRnKMfaUUEiSLP8UF9j9X9EDLxC0lfZg==
+"@typescript-eslint/types@3.6.1":
+  version "3.6.1"
+  resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-3.6.1.tgz#87600fe79a1874235d3cc1cf5c7e1a12eea69eee"
+  integrity sha512-NPxd5yXG63gx57WDTW1rp0cF3XlNuuFFB5G+Kc48zZ+51ZnQn9yjDEsjTPQ+aWM+V+Z0I4kuTFKjKvgcT1F7xQ==
+
+"@typescript-eslint/typescript-estree@2.34.0":
+  version "2.34.0"
+  resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-2.34.0.tgz#14aeb6353b39ef0732cc7f1b8285294937cf37d5"
+  integrity sha512-OMAr+nJWKdlVM9LOqCqh3pQQPwxHAN7Du8DR6dmwCrAmxtiXQnhHJ6tBNtf+cggqfo51SG/FCwnKhXCIM7hnVg==
   dependencies:
     debug "^4.1.1"
     eslint-visitor-keys "^1.1.0"
     glob "^7.1.6"
     is-glob "^4.0.1"
     lodash "^4.17.15"
-    semver "^6.3.0"
+    semver "^7.3.2"
     tsutils "^3.17.1"
 
-"@typescript-eslint/typescript-estree@2.24.0":
-  version "2.24.0"
-  resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-2.24.0.tgz#38bbc8bb479790d2f324797ffbcdb346d897c62a"
-  integrity sha512-RJ0yMe5owMSix55qX7Mi9V6z2FDuuDpN6eR5fzRJrp+8in9UF41IGNQHbg5aMK4/PjVaEQksLvz0IA8n+Mr/FA==
+"@typescript-eslint/typescript-estree@3.6.1":
+  version "3.6.1"
+  resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-3.6.1.tgz#a5c91fcc5497cce7922ff86bc37d5e5891dcdefa"
+  integrity sha512-G4XRe/ZbCZkL1fy09DPN3U0mR6SayIv1zSeBNquRFRk7CnVLgkC2ZPj8llEMJg5Y8dJ3T76SvTGtceytniaztQ==
   dependencies:
+    "@typescript-eslint/types" "3.6.1"
+    "@typescript-eslint/visitor-keys" "3.6.1"
     debug "^4.1.1"
-    eslint-visitor-keys "^1.1.0"
     glob "^7.1.6"
     is-glob "^4.0.1"
     lodash "^4.17.15"
-    semver "^6.3.0"
+    semver "^7.3.2"
     tsutils "^3.17.1"
 
-abab@^2.0.0:
+"@typescript-eslint/visitor-keys@3.6.1":
+  version "3.6.1"
+  resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-3.6.1.tgz#5c57a7772f4dd623cfeacc219303e7d46f963b37"
+  integrity sha512-qC8Olwz5ZyMTZrh4Wl3K4U6tfms0R/mzU4/5W3XeUZptVraGVmbptJbn6h2Ey6Rb3hOs3zWoAUebZk8t47KGiQ==
+  dependencies:
+    eslint-visitor-keys "^1.1.0"
+
+JSONStream@^1.3.2:
+  version "1.3.5"
+  resolved "https://registry.yarnpkg.com/JSONStream/-/JSONStream-1.3.5.tgz#3208c1f08d3a4d99261ab64f92302bc15e111ca0"
+  integrity sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==
+  dependencies:
+    jsonparse "^1.2.0"
+    through ">=2.2.7 <3"
+
+abab@^2.0.3:
   version "2.0.3"
   resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.3.tgz#623e2075e02eb2d3f2475e49f99c91846467907a"
   integrity sha512-tsFzPpcttalNjFBCFMqsKYQcWxxen1pgJR56by//QwvJc4/OUS3kPOOttx2tSIfjsylB0pYu7f5D3K1RCxUnUg==
 
+abbrev@1, abbrev@~1.1.1:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8"
+  integrity sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==
+
 accepts@~1.3.7:
   version "1.3.7"
   resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.7.tgz#531bc726517a3b2b41f850021c6cc15eaab507cd"
@@ -802,13 +802,13 @@ accepts@~1.3.7:
     mime-types "~2.1.24"
     negotiator "0.6.2"
 
-acorn-globals@^4.3.2:
-  version "4.3.4"
-  resolved "https://registry.yarnpkg.com/acorn-globals/-/acorn-globals-4.3.4.tgz#9fa1926addc11c97308c4e66d7add0d40c3272e7"
-  integrity sha512-clfQEh21R+D0leSbUdWf3OcfqyaCSAQ8Ryq00bofSekfr9W8u1jyYZo6ir0xu9Gtcf7BjcHJpnbZH7JOCpP60A==
+acorn-globals@^6.0.0:
+  version "6.0.0"
+  resolved "https://registry.yarnpkg.com/acorn-globals/-/acorn-globals-6.0.0.tgz#46cdd39f0f8ff08a876619b55f5ac8a6dc770b45"
+  integrity sha512-ZQl7LOWaF5ePqqcX4hLuv/bLXYQNfNWw2c0/yX/TsPRKamzHcTGQnlCjHT3TsmkOUVEPS3crCxiPfdzE/Trlhg==
   dependencies:
-    acorn "^6.0.1"
-    acorn-walk "^6.0.1"
+    acorn "^7.1.1"
+    acorn-walk "^7.1.1"
 
 acorn-jsx@^4.0.1:
   version "4.1.1"
@@ -817,30 +817,54 @@ acorn-jsx@^4.0.1:
   dependencies:
     acorn "^5.0.3"
 
-acorn-jsx@^5.1.0:
-  version "5.1.0"
-  resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.1.0.tgz#294adb71b57398b0680015f0a38c563ee1db5384"
-  integrity sha512-tMUqwBWfLFbJbizRmEcWSLw6HnFzfdJs2sOJEOwwtVPMoH/0Ay+E703oZz78VSXZiiDcZrQ5XKjPIUQixhmgVw==
+acorn-jsx@^5.2.0:
+  version "5.2.0"
+  resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.2.0.tgz#4c66069173d6fdd68ed85239fc256226182b2ebe"
+  integrity sha512-HiUX/+K2YpkpJ+SzBffkM/AQ2YE03S0U1kjTLVpoJdhZMOWy8qvXVN9JdLqv2QsaQ6MPYQIuNmwD8zOiYUofLQ==
 
-acorn-walk@^6.0.1:
-  version "6.2.0"
-  resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-6.2.0.tgz#123cb8f3b84c2171f1f7fb252615b1c78a6b1a8c"
-  integrity sha512-7evsyfH1cLOCdAzZAd43Cic04yKydNx0cF+7tiA19p1XnLLPU4dpCQOqpjqwokFe//vS0QqfqqjCS2JkiIs0cA==
+acorn-walk@^7.1.1:
+  version "7.2.0"
+  resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-7.2.0.tgz#0de889a601203909b0fbe07b8938dc21d2e967bc"
+  integrity sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==
 
 acorn@^5.0.3, acorn@^5.7.3:
   version "5.7.4"
   resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.7.4.tgz#3e8d8a9947d0599a1796d10225d7432f4a4acf5e"
   integrity sha512-1D++VG7BhrtvQpNbBzovKNc1FLGGEE/oGe7b9xJm/RFHMBeUaUGpluV9RLjZa47YFdPcDAenEYuq9pQPcMdLJg==
 
-acorn@^6.0.1:
-  version "6.4.1"
-  resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.4.1.tgz#531e58ba3f51b9dacb9a6646ca4debf5b14ca474"
-  integrity sha512-ZVA9k326Nwrj3Cj9jlh3wGFutC2ZornPNARZwsNYqQYgN0EsV2d53w5RN/co65Ohn4sUAUtb1rSUAOD6XN9idA==
+acorn@^7.1.1, acorn@^7.3.1:
+  version "7.3.1"
+  resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.3.1.tgz#85010754db53c3fbaf3b9ea3e083aa5c5d147ffd"
+  integrity sha512-tLc0wSnatxAQHVHUapaHdz72pi9KUyHjq5KyHjGg9Y8Ifdc79pTh2XvI6I1/chZbnM7QtNKzh66ooDogPZSleA==
 
-acorn@^7.1.0:
-  version "7.1.0"
-  resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.1.0.tgz#949d36f2c292535da602283586c2477c57eb2d6c"
-  integrity sha512-kL5CuoXA/dgxlBbVrflsflzQ3PAas7RYZB52NOm/6839iVYJgKMJ3cQJD+t2i5+qFa8h3MDpEOJiS64E8JLnSQ==
+agent-base@4, agent-base@^4.1.0, agent-base@^4.3.0:
+  version "4.3.0"
+  resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-4.3.0.tgz#8165f01c436009bccad0b1d122f05ed770efc6ee"
+  integrity sha512-salcGninV0nPrwpGNn4VTXBb1SOuXQBiqbrNXoeizJsHrsL6ERFM2Ne3JUSBWRE6aeNJI2ROP/WEEIDUiDe3cg==
+  dependencies:
+    es6-promisify "^5.0.0"
+
+agent-base@~4.2.1:
+  version "4.2.1"
+  resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-4.2.1.tgz#d89e5999f797875674c07d87f260fc41e83e8ca9"
+  integrity sha512-JVwXMr9nHYTUXsBFKUqhJwvlcYU/blreOEUkhNR2eXZIvwd+c+o5V4MgDPKWnMS/56awN3TRzIP+KoPn+roQtg==
+  dependencies:
+    es6-promisify "^5.0.0"
+
+agentkeepalive@^3.3.0, agentkeepalive@^3.4.1:
+  version "3.5.2"
+  resolved "https://registry.yarnpkg.com/agentkeepalive/-/agentkeepalive-3.5.2.tgz#a113924dd3fa24a0bc3b78108c450c2abee00f67"
+  integrity sha512-e0L/HNe6qkQ7H19kTlRRqUibEAwDK5AFk6y3PtMsuut2VAH6+Q4xZml1tNDJD7kSAyqmbG/K08K5WEJYtUrSlQ==
+  dependencies:
+    humanize-ms "^1.2.1"
+
+aggregate-error@^3.0.0:
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/aggregate-error/-/aggregate-error-3.0.1.tgz#db2fe7246e536f40d9b5442a39e117d7dd6a24e0"
+  integrity sha512-quoaXsZ9/BLNae5yiNoUz+Nhkwz83GhWwtYFglcjEQB2NDHCIpApbqXxIFnm4Pq/Nvhrsq5sYJFyohrrxnTGAA==
+  dependencies:
+    clean-stack "^2.0.0"
+    indent-string "^4.0.0"
 
 ajax-request@^1.2.0:
   version "1.2.3"
@@ -851,33 +875,45 @@ ajax-request@^1.2.0:
     utils-extend "^1.0.7"
 
 ajv@^6.10.0, ajv@^6.10.2, ajv@^6.5.5:
-  version "6.11.0"
-  resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.11.0.tgz#c3607cbc8ae392d8a5a536f25b21f8e5f3f87fe9"
-  integrity sha512-nCprB/0syFYy9fVYU1ox1l2KN8S9I+tziH8D4zdZuLT3N6RMlGSGt5FSTpAiHB/Whv8Qs1cWHma1aMKZyaHRKA==
+  version "6.12.3"
+  resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.3.tgz#18c5af38a111ddeb4f2697bd78d68abc1cabd706"
+  integrity sha512-4K0cK3L1hsqk9xIb2z9vs/XU+PGJZ9PNpJRDS9YLzmNdX6jmVPfamLvTJr0aDAusnHyCHO6MjzlkAsgtqp9teA==
   dependencies:
     fast-deep-equal "^3.1.1"
     fast-json-stable-stringify "^2.0.0"
     json-schema-traverse "^0.4.1"
     uri-js "^4.2.2"
 
+ansi-align@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/ansi-align/-/ansi-align-2.0.0.tgz#c36aeccba563b89ceb556f3690f0b1d9e3547f7f"
+  integrity sha1-w2rsy6VjuJzrVW82kPCx2eNUf38=
+  dependencies:
+    string-width "^2.0.0"
+
+ansi-colors@^4.1.1:
+  version "4.1.1"
+  resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.1.tgz#cbb9ae256bf750af1eab344f229aa27fe94ba348"
+  integrity sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==
+
 ansi-escapes@^3.0.0:
   version "3.2.0"
   resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-3.2.0.tgz#8780b98ff9dbf5638152d1f1fe5c1d7b4442976b"
   integrity sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ==
 
-ansi-escapes@^4.2.1:
-  version "4.3.0"
-  resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.3.0.tgz#a4ce2b33d6b214b7950d8595c212f12ac9cc569d"
-  integrity sha512-EiYhwo0v255HUL6eDyuLrXEkTi7WwVCLAw+SeOQ7M7qdun1z1pum4DEm/nuqIVbPvi9RPPc9k9LbyBv6H0DwVg==
+ansi-escapes@^4.2.1, ansi-escapes@^4.3.0:
+  version "4.3.1"
+  resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.3.1.tgz#a5c47cc43181f1f38ffd7076837700d395522a61"
+  integrity sha512-JWF7ocqNrp8u9oqpgV+wH5ftbt+cfvv+PTjOvKLT3AdYly/LmORARfEVT1iyjwN+4MqE5UmVKoAdIBqeoCHgLA==
   dependencies:
-    type-fest "^0.8.1"
+    type-fest "^0.11.0"
 
 ansi-regex@^2.0.0:
   version "2.1.1"
   resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df"
   integrity sha1-w7M6te42DYbg5ijwRorn7yfWVN8=
 
-ansi-regex@^3.0.0:
+ansi-regex@^3.0.0, ansi-regex@~3.0.0:
   version "3.0.0"
   resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.0.tgz#ed0317c322064f79466c02966bddb605ab37d998"
   integrity sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=
@@ -892,11 +928,6 @@ ansi-regex@^5.0.0:
   resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.0.tgz#388539f55179bf39339c81af30a654d69f87cb75"
   integrity sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==
 
-ansi-styles@^2.2.1:
-  version "2.2.1"
-  resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe"
-  integrity sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=
-
 ansi-styles@^3.2.0, ansi-styles@^3.2.1:
   version "3.2.1"
   resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d"
@@ -917,10 +948,15 @@ ansi@^0.3.1:
   resolved "https://registry.yarnpkg.com/ansi/-/ansi-0.3.1.tgz#0c42d4fb17160d5a9af1e484bace1c66922c1b21"
   integrity sha1-DELU+xcWDVqa8eSEus4cZpIsGyE=
 
-any-observable@^0.3.0:
-  version "0.3.0"
-  resolved "https://registry.yarnpkg.com/any-observable/-/any-observable-0.3.0.tgz#af933475e5806a67d0d7df090dd5e8bef65d119b"
-  integrity sha512-/FQM1EDkTsf63Ub2C6O7GuYFDsSXUwsaZDurV0np41ocwq0jthUAYCmhBX9f+KwlaCgIuWyr/4WlUQUBfKfZog==
+ansicolors@~0.3.2:
+  version "0.3.2"
+  resolved "https://registry.yarnpkg.com/ansicolors/-/ansicolors-0.3.2.tgz#665597de86a9ffe3aa9bfbe6cae5c6ea426b4979"
+  integrity sha1-ZlWX3oap/+Oqm/vmyuXG6kJrSXk=
+
+ansistyles@~0.1.3:
+  version "0.1.3"
+  resolved "https://registry.yarnpkg.com/ansistyles/-/ansistyles-0.1.3.tgz#5de60415bda071bb37127854c864f41b23254539"
+  integrity sha1-XeYEFb2gcbs3EnhUyGT0GyMlRTk=
 
 anymatch@^1.3.0:
   version "1.3.2"
@@ -956,10 +992,33 @@ app-root-path@^2.0.1:
   resolved "https://registry.yarnpkg.com/app-root-path/-/app-root-path-2.2.1.tgz#d0df4a682ee408273583d43f6f79e9892624bc9a"
   integrity sha512-91IFKeKk7FjfmezPKkwtaRvSpnUc4gDwPAjA1YZ9Gn0q0PPeW+vbeUsZuyDwjI7+QTHhcLen2v25fi/AmhvbJA==
 
+aproba@^1.0.3, aproba@^1.1.1, aproba@^1.1.2, aproba@~1.2.0:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.2.0.tgz#6802e6264efd18c790a1b0d517f0f2627bf2c94a"
+  integrity sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==
+
+"aproba@^1.1.2 || 2":
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/aproba/-/aproba-2.0.0.tgz#52520b8ae5b569215b354efc0caa3fe1e45a8adc"
+  integrity sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==
+
+archy@~1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/archy/-/archy-1.0.0.tgz#f9c8c13757cc1dd7bc379ac77b2c62a5c2868c40"
+  integrity sha1-+cjBN1fMHde8N5rHeyxipcKGjEA=
+
+are-we-there-yet@~1.1.2:
+  version "1.1.5"
+  resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz#4b35c2944f062a8bfcda66410760350fe9ddfc21"
+  integrity sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==
+  dependencies:
+    delegates "^1.0.0"
+    readable-stream "^2.0.6"
+
 arg@^4.1.0:
-  version "4.1.2"
-  resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.2.tgz#e70c90579e02c63d80e3ad4e31d8bfdb8bd50064"
-  integrity sha512-+ytCkGcBtHZ3V2r2Z06AncYO8jz46UEamcspGoU8lHcEbpn6J77QK0vdWvChsclg/tM5XIJC5tnjmPp7Eq6Obg==
+  version "4.1.3"
+  resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.3.tgz#269fc7ad5b8e42cb63c896d5666017261c144089"
+  integrity sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==
 
 argparse@^1.0.7:
   version "1.0.10"
@@ -968,13 +1027,13 @@ argparse@^1.0.7:
   dependencies:
     sprintf-js "~1.0.2"
 
-aria-query@^3.0.0:
-  version "3.0.0"
-  resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-3.0.0.tgz#65b3fcc1ca1155a8c9ae64d6eee297f15d5133cc"
-  integrity sha1-ZbP8wcoRVajJrmTW7uKX8V1RM8w=
+aria-query@^4.2.2:
+  version "4.2.2"
+  resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-4.2.2.tgz#0d2ca6c9aceb56b8977e9fed6aed7e15bbd2f83b"
+  integrity sha512-o/HelwhuKpTj/frsOsbNLNgnNGVIFsVP/SW2BSF14gVl7kAfMOJ6/8wUAUvG1R1NHKrfG+2sHZTu0yauT1qBrA==
   dependencies:
-    ast-types-flow "0.0.7"
-    commander "^2.11.0"
+    "@babel/runtime" "^7.10.2"
+    "@babel/runtime-corejs3" "^7.10.2"
 
 arr-diff@^2.0.0:
   version "2.0.0"
@@ -998,17 +1057,12 @@ arr-union@^3.1.0:
   resolved "https://registry.yarnpkg.com/arr-union/-/arr-union-3.1.0.tgz#e39b09aea9def866a8f206e288af63919bae39c4"
   integrity sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=
 
-array-equal@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/array-equal/-/array-equal-1.0.0.tgz#8c2a5ef2472fd9ea742b04c77a75093ba2757c93"
-  integrity sha1-jCpe8kcv2ep0KwTHenUJO6J1fJM=
-
 array-flatten@1.1.1:
   version "1.1.1"
   resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2"
   integrity sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=
 
-array-includes@^3.0.3, array-includes@^3.1.1:
+array-includes@^3.1.1:
   version "3.1.1"
   resolved "https://registry.yarnpkg.com/array-includes/-/array-includes-3.1.1.tgz#cdd67e6852bdf9c1215460786732255ed2459348"
   integrity sha512-c2VXaCHl7zPsvpkFsw4nxvFie4fh1ur9bpcgsVkIjqn0H/Xwdg+7fv3n2r/isyS8EBj5b06M9kHyZuIr4El6WQ==
@@ -1027,7 +1081,7 @@ array-unique@^0.3.2:
   resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.3.2.tgz#a894b75d4bc4f6cd679ef3244a9fd8f46ae2d428"
   integrity sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=
 
-array.prototype.flat@^1.2.1:
+array.prototype.flat@^1.2.3:
   version "1.2.3"
   resolved "https://registry.yarnpkg.com/array.prototype.flat/-/array.prototype.flat-1.2.3.tgz#0de82b426b0318dbfdb940089e38b043d37f6c7b"
   integrity sha512-gBlRZV0VSmfPIeWfuuy56XZMvbVfbEUnOXUvt3F/eUUUSyzlgLxhEX4YAEpxNAogRGehPSnfXyPtYyKAhkzQhQ==
@@ -1035,6 +1089,20 @@ array.prototype.flat@^1.2.1:
     define-properties "^1.1.3"
     es-abstract "^1.17.0-next.1"
 
+array.prototype.flatmap@^1.2.3:
+  version "1.2.3"
+  resolved "https://registry.yarnpkg.com/array.prototype.flatmap/-/array.prototype.flatmap-1.2.3.tgz#1c13f84a178566042dd63de4414440db9222e443"
+  integrity sha512-OOEk+lkePcg+ODXIpvuU9PAryCikCJyo7GlDG1upleEpQRx6mzL9puEBkozQ5iAx20KV0l3DbyQwqciJtqe5Pg==
+  dependencies:
+    define-properties "^1.1.3"
+    es-abstract "^1.17.0-next.1"
+    function-bind "^1.1.1"
+
+asap@^2.0.0:
+  version "2.0.6"
+  resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46"
+  integrity sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=
+
 asn1@~0.2.3:
   version "0.2.4"
   resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.4.tgz#8d2475dfab553bb33e77b54e59e880bb8ce23136"
@@ -1052,7 +1120,7 @@ assign-symbols@^1.0.0:
   resolved "https://registry.yarnpkg.com/assign-symbols/-/assign-symbols-1.0.0.tgz#59667f41fadd4f20ccbc2bb96b8d4f7f78ec0367"
   integrity sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=
 
-ast-types-flow@0.0.7, ast-types-flow@^0.0.7:
+ast-types-flow@^0.0.7:
   version "0.0.7"
   resolved "https://registry.yarnpkg.com/ast-types-flow/-/ast-types-flow-0.0.7.tgz#f70b735c6bca1a5c9c22d982c3e39e7feba3bdad"
   integrity sha1-9wtzXGvKGlycItmCw+Oef+ujva0=
@@ -1062,6 +1130,11 @@ astral-regex@^1.0.0:
   resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-1.0.0.tgz#6c8c3fb827dd43ee3918f27b82782ab7658a6fd9"
   integrity sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg==
 
+astral-regex@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-2.0.0.tgz#483143c567aeed4785759c0865786dc77d7d2e31"
+  integrity sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==
+
 async-each@^1.0.0:
   version "1.0.3"
   resolved "https://registry.yarnpkg.com/async-each/-/async-each-1.0.3.tgz#b727dbf87d7651602f06f4d4ac387f47d91b0cbf"
@@ -1088,19 +1161,21 @@ aws-sign2@~0.7.0:
   integrity sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=
 
 aws4@^1.8.0:
-  version "1.9.1"
-  resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.9.1.tgz#7e33d8f7d449b3f673cd72deb9abdc552dbe528e"
-  integrity sha512-wMHVg2EOHaMRxbzgFJ9gtjOOCrI80OHLG14rxi28XwOW8ux6IiEbRCGGGqCtdAIg4FQCbW20k9RsT4y3gJlFug==
+  version "1.10.0"
+  resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.10.0.tgz#a17b3a8ea811060e74d47d306122400ad4497ae2"
+  integrity sha512-3YDiu347mtVtjpyV3u5kVqQLP242c06zwDOgpeRnybmXlYYsLbtTrUBUm8i8srONt+FWobl5aibnU1030PeeuA==
 
-axobject-query@^2.0.2:
-  version "2.1.1"
-  resolved "https://registry.yarnpkg.com/axobject-query/-/axobject-query-2.1.1.tgz#2a3b1271ec722d48a4cd4b3fcc20c853326a49a7"
-  integrity sha512-lF98xa/yvy6j3fBHAgQXIYl+J4eZadOSqsPojemUqClzNbBV38wWGpUbQbVEyf4eUF5yF7eHmGgGA2JiHyjeqw==
-  dependencies:
-    "@babel/runtime" "^7.7.4"
-    "@babel/runtime-corejs3" "^7.7.4"
+axe-core@^3.5.4:
+  version "3.5.5"
+  resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-3.5.5.tgz#84315073b53fa3c0c51676c588d59da09a192227"
+  integrity sha512-5P0QZ6J5xGikH780pghEdbEKijCTrruK9KxtPZCFWUpef0f6GipO+xEZ5GKCb020mmqgbiNO6TcA55CriL784Q==
+
+axobject-query@^2.1.2:
+  version "2.2.0"
+  resolved "https://registry.yarnpkg.com/axobject-query/-/axobject-query-2.2.0.tgz#943d47e10c0b704aa42275e20edf3722648989be"
+  integrity sha512-Td525n+iPOOyUQIeBfcASuG6uJsDOITl7Mds5gFyerkWiX7qhUTdYUBlSgNMyVqtSJqwpt1kXGLdUt6SykLMRA==
 
-babel-eslint@10.1.0:
+babel-eslint@10.1.0, babel-eslint@^10.1.0:
   version "10.1.0"
   resolved "https://registry.yarnpkg.com/babel-eslint/-/babel-eslint-10.1.0.tgz#6968e568a910b78fb3779cdd8b6ac2f479943232"
   integrity sha512-ifWaTHQ0ce+448CYop8AdrQiBsGrnC+bMgfyKFdi6EsPLTAWG+QfyDeM6OH+FmWnKvEq5NnBMLvlBUPKQZoDSg==
@@ -1112,17 +1187,18 @@ babel-eslint@10.1.0:
     eslint-visitor-keys "^1.0.0"
     resolve "^1.12.0"
 
-babel-jest@^25.4.0:
-  version "25.4.0"
-  resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-25.4.0.tgz#409eb3e2ddc2ad9a92afdbb00991f1633f8018d0"
-  integrity sha512-p+epx4K0ypmHuCnd8BapfyOwWwosNCYhedetQey1awddtfmEX0MmdxctGl956uwUmjwXR5VSS5xJcGX9DvdIog==
+babel-jest@^26.1.0:
+  version "26.1.0"
+  resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-26.1.0.tgz#b20751185fc7569a0f135730584044d1cb934328"
+  integrity sha512-Nkqgtfe7j6PxLO6TnCQQlkMm8wdTdnIF8xrdpooHCuD5hXRzVEPbPneTJKknH5Dsv3L8ip9unHDAp48YQ54Dkg==
   dependencies:
-    "@jest/transform" "^25.4.0"
-    "@jest/types" "^25.4.0"
+    "@jest/transform" "^26.1.0"
+    "@jest/types" "^26.1.0"
     "@types/babel__core" "^7.1.7"
     babel-plugin-istanbul "^6.0.0"
-    babel-preset-jest "^25.4.0"
-    chalk "^3.0.0"
+    babel-preset-jest "^26.1.0"
+    chalk "^4.0.0"
+    graceful-fs "^4.2.4"
     slash "^3.0.0"
 
 babel-plugin-istanbul@^6.0.0:
@@ -1136,21 +1212,25 @@ babel-plugin-istanbul@^6.0.0:
     istanbul-lib-instrument "^4.0.0"
     test-exclude "^6.0.0"
 
-babel-plugin-jest-hoist@^25.4.0:
-  version "25.4.0"
-  resolved "https://registry.yarnpkg.com/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-25.4.0.tgz#0c122c1b93fb76f52d2465be2e8069e798e9d442"
-  integrity sha512-M3a10JCtTyKevb0MjuH6tU+cP/NVQZ82QPADqI1RQYY1OphztsCeIeQmTsHmF/NS6m0E51Zl4QNsI3odXSQF5w==
+babel-plugin-jest-hoist@^26.1.0:
+  version "26.1.0"
+  resolved "https://registry.yarnpkg.com/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-26.1.0.tgz#c6a774da08247a28285620a64dfadbd05dd5233a"
+  integrity sha512-qhqLVkkSlqmC83bdMhM8WW4Z9tB+JkjqAqlbbohS9sJLT5Ha2vfzuKqg5yenXrAjOPG2YC0WiXdH3a9PvB+YYw==
   dependencies:
+    "@babel/template" "^7.3.3"
+    "@babel/types" "^7.3.3"
+    "@types/babel__core" "^7.0.0"
     "@types/babel__traverse" "^7.0.6"
 
 babel-preset-current-node-syntax@^0.1.2:
-  version "0.1.2"
-  resolved "https://registry.yarnpkg.com/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-0.1.2.tgz#fb4a4c51fe38ca60fede1dc74ab35eb843cb41d6"
-  integrity sha512-u/8cS+dEiK1SFILbOC8/rUI3ml9lboKuuMvZ/4aQnQmhecQAgPw5ew066C1ObnEAUmlx7dv/s2z52psWEtLNiw==
+  version "0.1.3"
+  resolved "https://registry.yarnpkg.com/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-0.1.3.tgz#b4b547acddbf963cba555ba9f9cbbb70bfd044da"
+  integrity sha512-uyexu1sVwcdFnyq9o8UQYsXwXflIh8LvrF5+cKrYam93ned1CStffB3+BEcsxGSgagoA3GEyjDqO4a/58hyPYQ==
   dependencies:
     "@babel/plugin-syntax-async-generators" "^7.8.4"
     "@babel/plugin-syntax-bigint" "^7.8.3"
     "@babel/plugin-syntax-class-properties" "^7.8.3"
+    "@babel/plugin-syntax-import-meta" "^7.8.3"
     "@babel/plugin-syntax-json-strings" "^7.8.3"
     "@babel/plugin-syntax-logical-assignment-operators" "^7.8.3"
     "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.3"
@@ -1159,12 +1239,12 @@ babel-preset-current-node-syntax@^0.1.2:
     "@babel/plugin-syntax-optional-catch-binding" "^7.8.3"
     "@babel/plugin-syntax-optional-chaining" "^7.8.3"
 
-babel-preset-jest@^25.4.0:
-  version "25.4.0"
-  resolved "https://registry.yarnpkg.com/babel-preset-jest/-/babel-preset-jest-25.4.0.tgz#10037cc32b751b994b260964629e49dc479abf4c"
-  integrity sha512-PwFiEWflHdu3JCeTr0Pb9NcHHE34qWFnPQRVPvqQITx4CsDCzs6o05923I10XvLvn9nNsRHuiVgB72wG/90ZHQ==
+babel-preset-jest@^26.1.0:
+  version "26.1.0"
+  resolved "https://registry.yarnpkg.com/babel-preset-jest/-/babel-preset-jest-26.1.0.tgz#612f714e5b457394acfd863793c564cbcdb7d1c1"
+  integrity sha512-na9qCqFksknlEj5iSdw1ehMVR06LCCTkZLGKeEtxDDdhg8xpUF09m29Kvh1pRbZ07h7AQ5ttLYUwpXL4tO6w7w==
   dependencies:
-    babel-plugin-jest-hoist "^25.4.0"
+    babel-plugin-jest-hoist "^26.1.0"
     babel-preset-current-node-syntax "^0.1.2"
 
 balanced-match@^1.0.0:
@@ -1205,6 +1285,18 @@ bcrypt-pbkdf@^1.0.0:
   dependencies:
     tweetnacl "^0.14.3"
 
+bin-links@^1.1.0, bin-links@^1.1.2:
+  version "1.1.8"
+  resolved "https://registry.yarnpkg.com/bin-links/-/bin-links-1.1.8.tgz#bd39aadab5dc4bdac222a07df5baf1af745b2228"
+  integrity sha512-KgmVfx+QqggqP9dA3iIc5pA4T1qEEEL+hOhOhNPaUm77OTrJoOXE/C05SJLNJe6m/2wUK7F1tDSou7n5TfCDzQ==
+  dependencies:
+    bluebird "^3.5.3"
+    cmd-shim "^3.0.0"
+    gentle-fs "^2.3.0"
+    graceful-fs "^4.1.15"
+    npm-normalize-package-bin "^1.0.0"
+    write-file-atomic "^2.3.0"
+
 binary-extensions@^1.0.0:
   version "1.13.1"
   resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-1.13.1.tgz#598afe54755b2868a5330d2aff9d4ebb53209b65"
@@ -1217,6 +1309,23 @@ bindings@^1.5.0:
   dependencies:
     file-uri-to-path "1.0.0"
 
+block-stream@*:
+  version "0.0.9"
+  resolved "https://registry.yarnpkg.com/block-stream/-/block-stream-0.0.9.tgz#13ebfe778a03205cfe03751481ebb4b3300c126a"
+  integrity sha1-E+v+d4oDIFz+A3UUgeu0szAMEmo=
+  dependencies:
+    inherits "~2.0.0"
+
+bluebird@^3.5.1, bluebird@^3.5.3, bluebird@^3.5.5:
+  version "3.7.2"
+  resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f"
+  integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==
+
+bluebird@~3.5.1:
+  version "3.5.5"
+  resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.5.tgz#a8d0afd73251effbbd5fe384a77d73003c17a71f"
+  integrity sha512-5am6HnnfN+urzt4yfg7IgTbotDjIT/u8AJpEt0sIU9FtXfVeezXAPKswrG+xKUCOYAINpSdgZVDU6QFh+cuH3w==
+
 body-parser@1.19.0:
   version "1.19.0"
   resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.19.0.tgz#96b2709e57c9c4e09a6fd66a8fd979844f69f08a"
@@ -1234,14 +1343,27 @@ body-parser@1.19.0:
     type-is "~1.6.17"
 
 bootswatch@^4.3.1:
-  version "4.4.1"
-  resolved "https://registry.yarnpkg.com/bootswatch/-/bootswatch-4.4.1.tgz#f270618b4ebe07de8e1748acd88f104cc31bfec3"
-  integrity sha512-Kx3z6+3Jpg9g6l/xZBCnc8d6KeJK0QawxCZWOomdcI5AuSZLZb+DoH5X9RJH+cOcSeMAxyzdIjkVUR01+Db5bQ==
+  version "4.5.0"
+  resolved "https://registry.yarnpkg.com/bootswatch/-/bootswatch-4.5.0.tgz#1f4ea460118d0da8113418f1627ca755697d7e0f"
+  integrity sha512-kAfYTTWIsgUOA5nybJNM4yvOpwxm37eIb1DFZlD5jLgPttK+bLJTt9UpKWAoMQZRBQbWfC1O1w1P5PGU7lz83Q==
 
 bowser@^2.0.0-beta.3:
-  version "2.9.0"
-  resolved "https://registry.yarnpkg.com/bowser/-/bowser-2.9.0.tgz#3bed854233b419b9a7422d9ee3e85504373821c9"
-  integrity sha512-2ld76tuLBNFekRgmJfT2+3j5MIrP6bFict8WAIT3beq+srz1gcKNAdNKMqHqauQt63NmAa88HfP1/Ypa9Er3HA==
+  version "2.10.0"
+  resolved "https://registry.yarnpkg.com/bowser/-/bowser-2.10.0.tgz#be3736f161c4bb8b10958027ab99465d2a811198"
+  integrity sha512-OCsqTQboTEWWsUjcp5jLSw2ZHsBiv2C105iFs61bOT0Hnwi9p7/uuXdd7mu8RYcarREfdjNN+8LitmEHATsLYg==
+
+boxen@^1.2.1:
+  version "1.3.0"
+  resolved "https://registry.yarnpkg.com/boxen/-/boxen-1.3.0.tgz#55c6c39a8ba58d9c61ad22cd877532deb665a20b"
+  integrity sha512-TNPjfTr432qx7yOjQyaXm3dSR0MH9vXp7eT1BFSl/C51g+EFnOR9hTg1IreahGBmDNCehscshe45f+C1TBZbLw==
+  dependencies:
+    ansi-align "^2.0.0"
+    camelcase "^4.0.0"
+    chalk "^2.0.1"
+    cli-boxes "^1.0.0"
+    string-width "^2.0.0"
+    term-size "^1.2.0"
+    widest-line "^2.0.0"
 
 brace-expansion@^1.1.7:
   version "1.1.11"
@@ -1288,13 +1410,6 @@ browser-process-hrtime@^1.0.0:
   resolved "https://registry.yarnpkg.com/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz#3c9b4b7d782c8121e56f10106d84c0d0ffc94626"
   integrity sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow==
 
-browser-resolve@^1.11.3:
-  version "1.11.3"
-  resolved "https://registry.yarnpkg.com/browser-resolve/-/browser-resolve-1.11.3.tgz#9b7cbb3d0f510e4cb86bdbd796124d28b5890af6"
-  integrity sha512-exDi1BYWB/6raKHmDTCicQfTkqwN5fioMFV4j8BsfMU4R2DK/QfZfK7kOVkmWCNANf0snkBzqGqAJBao9gZMdQ==
-  dependencies:
-    resolve "1.1.7"
-
 bs-logger@0.x:
   version "0.2.6"
   resolved "https://registry.yarnpkg.com/bs-logger/-/bs-logger-0.2.6.tgz#eb7d365307a72cf974cc6cda76b68354ad336bd8"
@@ -1314,11 +1429,70 @@ buffer-from@1.x, buffer-from@^1.0.0:
   resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef"
   integrity sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==
 
+builtin-modules@^1.0.0:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-1.1.1.tgz#270f076c5a72c02f5b65a47df94c5fe3a278892f"
+  integrity sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8=
+
+builtins@^1.0.3:
+  version "1.0.3"
+  resolved "https://registry.yarnpkg.com/builtins/-/builtins-1.0.3.tgz#cb94faeb61c8696451db36534e1422f94f0aee88"
+  integrity sha1-y5T662HIaWRR2zZTThQi+U8K7og=
+
+byline@^5.0.0:
+  version "5.0.0"
+  resolved "https://registry.yarnpkg.com/byline/-/byline-5.0.0.tgz#741c5216468eadc457b03410118ad77de8c1ddb1"
+  integrity sha1-dBxSFkaOrcRXsDQQEYrXfejB3bE=
+
+byte-size@^4.0.2:
+  version "4.0.4"
+  resolved "https://registry.yarnpkg.com/byte-size/-/byte-size-4.0.4.tgz#29d381709f41aae0d89c631f1c81aec88cd40b23"
+  integrity sha512-82RPeneC6nqCdSwCX2hZUz3JPOvN5at/nTEw/CMf05Smu3Hrpo9Psb7LjN+k+XndNArG1EY8L4+BM3aTM4BCvw==
+
 bytes@3.1.0:
   version "3.1.0"
   resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.0.tgz#f6cf7933a360e0588fa9fde85651cdc7f805d1f6"
   integrity sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==
 
+cacache@^10.0.0, cacache@^10.0.4:
+  version "10.0.4"
+  resolved "https://registry.yarnpkg.com/cacache/-/cacache-10.0.4.tgz#6452367999eff9d4188aefd9a14e9d7c6a263460"
+  integrity sha512-Dph0MzuH+rTQzGPNT9fAnrPmMmjKfST6trxJeK7NQuHRaVw24VzPRWTmg9MpcwOVQZO0E1FBICUlFeNaKPIfHA==
+  dependencies:
+    bluebird "^3.5.1"
+    chownr "^1.0.1"
+    glob "^7.1.2"
+    graceful-fs "^4.1.11"
+    lru-cache "^4.1.1"
+    mississippi "^2.0.0"
+    mkdirp "^0.5.1"
+    move-concurrently "^1.0.1"
+    promise-inflight "^1.0.1"
+    rimraf "^2.6.2"
+    ssri "^5.2.4"
+    unique-filename "^1.1.0"
+    y18n "^4.0.0"
+
+cacache@^11.0.2, cacache@^11.3.3:
+  version "11.3.3"
+  resolved "https://registry.yarnpkg.com/cacache/-/cacache-11.3.3.tgz#8bd29df8c6a718a6ebd2d010da4d7972ae3bbadc"
+  integrity sha512-p8WcneCytvzPxhDvYp31PD039vi77I12W+/KfR9S8AZbaiARFBCpsPJS+9uhWfeBfeAtW7o/4vt3MUqLkbY6nA==
+  dependencies:
+    bluebird "^3.5.5"
+    chownr "^1.1.1"
+    figgy-pudding "^3.5.1"
+    glob "^7.1.4"
+    graceful-fs "^4.1.15"
+    lru-cache "^5.1.1"
+    mississippi "^3.0.0"
+    mkdirp "^0.5.1"
+    move-concurrently "^1.0.1"
+    promise-inflight "^1.0.1"
+    rimraf "^2.6.3"
+    ssri "^6.0.1"
+    unique-filename "^1.1.1"
+    y18n "^4.0.0"
+
 cache-base@^1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/cache-base/-/cache-base-1.0.1.tgz#0a7f46416831c8b662ee36fe4e7c59d76f666ab2"
@@ -1334,16 +1508,31 @@ cache-base@^1.0.1:
     union-value "^1.0.0"
     unset-value "^1.0.0"
 
+call-limit@~1.1.0:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/call-limit/-/call-limit-1.1.1.tgz#ef15f2670db3f1992557e2d965abc459e6e358d4"
+  integrity sha512-5twvci5b9eRBw2wCfPtN0GmlR2/gadZqyFpPhOK6CvMFoFgA+USnZ6Jpu1lhG9h85pQ3Ouil3PfXWRD4EUaRiQ==
+
 callsites@^3.0.0:
   version "3.1.0"
   resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73"
   integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==
 
+camelcase@^4.0.0, camelcase@^4.1.0:
+  version "4.1.0"
+  resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-4.1.0.tgz#d545635be1e33c542649c69173e5de6acfae34dd"
+  integrity sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=
+
 camelcase@^5.0.0, camelcase@^5.3.1:
   version "5.3.1"
   resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320"
   integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==
 
+camelcase@^6.0.0:
+  version "6.0.0"
+  resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.0.0.tgz#5259f7c30e35e278f1bdc2a4d91230b37cad981e"
+  integrity sha512-8KMDF1Vz2gzOq54ONPJS65IvTUaB1cHJ2DMM7MbPmLZljDH1qpzzLsWdiN9pHh6qvkRVDTi/07+eNGch/oLU4w==
+
 capture-exit@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/capture-exit/-/capture-exit-2.0.0.tgz#fb953bfaebeb781f62898239dabb426d08a509a4"
@@ -1351,6 +1540,11 @@ capture-exit@^2.0.0:
   dependencies:
     rsvp "^4.8.4"
 
+capture-stack-trace@^1.0.0:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/capture-stack-trace/-/capture-stack-trace-1.0.1.tgz#a6c0bbe1f38f3aa0b92238ecb6ff42c344d4135d"
+  integrity sha512-mYQLZnx5Qt1JgB1WEiMCf2647plpGeQ2NMR/5L0HNZzGQo4fuSPnK+wjfPnKZV0aiJDgzmWqqkV/g7JD+DW0qw==
+
 caseless@~0.12.0:
   version "0.12.0"
   resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc"
@@ -1366,18 +1560,7 @@ chain-able@^3.0.0:
   resolved "https://registry.yarnpkg.com/chain-able/-/chain-able-3.0.0.tgz#dcffe8b04f3da210941a23843bc1332bb288ca9f"
   integrity sha512-26MoELhta86n7gCsE2T1hGRyncZvPjFXTkB/DEp4+i/EJVSxXQNwXMDZZb2+SWcbPuow18wQtztaW7GXOel9DA==
 
-chalk@^1.0.0, chalk@^1.1.3:
-  version "1.1.3"
-  resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98"
-  integrity sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=
-  dependencies:
-    ansi-styles "^2.2.1"
-    escape-string-regexp "^1.0.2"
-    has-ansi "^2.0.0"
-    strip-ansi "^3.0.0"
-    supports-color "^2.0.0"
-
-chalk@^2.0.0, chalk@^2.1.0, chalk@^2.4.1, chalk@^2.4.2:
+chalk@^2.0.0, chalk@^2.0.1, chalk@^2.4.1:
   version "2.4.2"
   resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424"
   integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==
@@ -1394,24 +1577,24 @@ chalk@^3.0.0:
     ansi-styles "^4.1.0"
     supports-color "^7.1.0"
 
-chalk@^4.0.0:
-  version "4.0.0"
-  resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.0.0.tgz#6e98081ed2d17faab615eb52ac66ec1fe6209e72"
-  integrity sha512-N9oWFcegS0sFr9oh1oz2d7Npos6vNoWW9HvtCg5N1KRFpUhaAhvTv5Y58g880fZaEYSNm3qDz8SU1UrGvp+n7A==
+chalk@^4.0.0, chalk@^4.1.0:
+  version "4.1.0"
+  resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.0.tgz#4e14870a618d9e2edd97dd8345fd9d9dc315646a"
+  integrity sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==
   dependencies:
     ansi-styles "^4.1.0"
     supports-color "^7.1.0"
 
+char-regex@^1.0.2:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/char-regex/-/char-regex-1.0.2.tgz#d744358226217f981ed58f479b1d6bcc29545dcf"
+  integrity sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==
+
 chardet@^0.4.0:
   version "0.4.2"
   resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.4.2.tgz#b5473b33dc97c424e5d98dc87d55d4d8a29c8bf2"
   integrity sha1-tUc7M9yXxCTl2Y3IfVXU2KKci/I=
 
-chardet@^0.7.0:
-  version "0.7.0"
-  resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e"
-  integrity sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==
-
 choices.js@^9.0.1:
   version "9.0.1"
   resolved "https://registry.yarnpkg.com/choices.js/-/choices.js-9.0.1.tgz#745fb29af8670428fdc0bf1cc9dfaa404e9d0510"
@@ -1437,12 +1620,32 @@ chokidar@^1.6.1:
   optionalDependencies:
     fsevents "^1.0.0"
 
+chownr@^1.0.1, chownr@^1.1.1, chownr@^1.1.2:
+  version "1.1.4"
+  resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.4.tgz#6fc9d7b42d32a583596337666e7d08084da2cc6b"
+  integrity sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==
+
+chownr@~1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.0.1.tgz#e2a75042a9551908bebd25b8523d5f9769d79181"
+  integrity sha1-4qdQQqlVGQi+vSW4Uj1fl2nXkYE=
+
+ci-info@^1.5.0:
+  version "1.6.0"
+  resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-1.6.0.tgz#2ca20dbb9ceb32d4524a683303313f0304b1e497"
+  integrity sha512-vsGdkwSCDpWmP80ncATX7iea5DWQemg1UgCW5J8tqjU3lYw4FBYuj89J0CTVomA7BEfvSZd84GmHko+MxFQU2A==
+
 ci-info@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-2.0.0.tgz#67a9e964be31a51e15e5010d58e6f12834002f46"
   integrity sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==
 
-class-utils@^0.3.5:
+cidr-regex@1.0.6:
+  version "1.0.6"
+  resolved "https://registry.yarnpkg.com/cidr-regex/-/cidr-regex-1.0.6.tgz#74abfd619df370b9d54ab14475568e97dd64c0c1"
+  integrity sha1-dKv9YZ3zcLnVSrFEdVaOl91kwME=
+
+class-utils@^0.3.5:
   version "0.3.6"
   resolved "https://registry.yarnpkg.com/class-utils/-/class-utils-0.3.6.tgz#f93369ae8b9a7ce02fd41faad0ca83033190c463"
   integrity sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==
@@ -1471,7 +1674,25 @@ clean-regexp@^1.0.0:
   dependencies:
     escape-string-regexp "^1.0.5"
 
-cli-cursor@^2.0.0, cli-cursor@^2.1.0:
+clean-stack@^2.0.0:
+  version "2.2.0"
+  resolved "https://registry.yarnpkg.com/clean-stack/-/clean-stack-2.2.0.tgz#ee8472dbb129e727b31e8a10a427dee9dfe4008b"
+  integrity sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==
+
+cli-boxes@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/cli-boxes/-/cli-boxes-1.0.0.tgz#4fa917c3e59c94a004cd61f8ee509da651687143"
+  integrity sha1-T6kXw+WclKAEzWH47lCdplFocUM=
+
+cli-columns@^3.1.2:
+  version "3.1.2"
+  resolved "https://registry.yarnpkg.com/cli-columns/-/cli-columns-3.1.2.tgz#6732d972979efc2ae444a1f08e08fa139c96a18e"
+  integrity sha1-ZzLZcpee/CrkRKHwjgj6E5yWoY4=
+  dependencies:
+    string-width "^2.0.0"
+    strip-ansi "^3.0.1"
+
+cli-cursor@^2.1.0:
   version "2.1.0"
   resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-2.1.0.tgz#b35dac376479facc3e94747d41d0d0f5238ffcb5"
   integrity sha1-s12sN2R5+sw+lHR9QdDQ9SOP/LU=
@@ -1485,18 +1706,56 @@ cli-cursor@^3.1.0:
   dependencies:
     restore-cursor "^3.1.0"
 
-cli-truncate@^0.2.1:
-  version "0.2.1"
-  resolved "https://registry.yarnpkg.com/cli-truncate/-/cli-truncate-0.2.1.tgz#9f15cfbb0705005369216c626ac7d05ab90dd574"
-  integrity sha1-nxXPuwcFAFNpIWxiasfQWrkN1XQ=
+cli-table2@~0.2.0:
+  version "0.2.0"
+  resolved "https://registry.yarnpkg.com/cli-table2/-/cli-table2-0.2.0.tgz#2d1ef7f218a0e786e214540562d4bd177fe32d97"
+  integrity sha1-LR738hig54biFFQFYtS9F3/jLZc=
   dependencies:
-    slice-ansi "0.0.4"
+    lodash "^3.10.1"
     string-width "^1.0.1"
+  optionalDependencies:
+    colors "^1.1.2"
+
+cli-table3@^0.5.0:
+  version "0.5.1"
+  resolved "https://registry.yarnpkg.com/cli-table3/-/cli-table3-0.5.1.tgz#0252372d94dfc40dbd8df06005f48f31f656f202"
+  integrity sha512-7Qg2Jrep1S/+Q3EceiZtQcDPWxhAvBw+ERf1162v4sikJrvojMHFqXt8QIVha8UlH9rgU0BeWPytZ9/TzYqlUw==
+  dependencies:
+    object-assign "^4.1.0"
+    string-width "^2.1.1"
+  optionalDependencies:
+    colors "^1.1.2"
+
+cli-truncate@2.1.0, cli-truncate@^2.1.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/cli-truncate/-/cli-truncate-2.1.0.tgz#c39e28bf05edcde5be3b98992a22deed5a2b93c7"
+  integrity sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==
+  dependencies:
+    slice-ansi "^3.0.0"
+    string-width "^4.2.0"
 
 cli-width@^2.0.0:
-  version "2.2.0"
-  resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-2.2.0.tgz#ff19ede8a9a5e579324147b0c11f0fbcbabed639"
-  integrity sha1-/xnt6Kml5XkyQUewwR8PvLq+1jk=
+  version "2.2.1"
+  resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-2.2.1.tgz#b0433d0b4e9c847ef18868a4ef16fd5fc8271c48"
+  integrity sha512-GRMWDxpOB6Dgk2E5Uo+3eEBvtOOlimMmpbFiKuLFnQzYDavtLFY3K5ona41jgN/WdRZtG7utuVSVTL4HbZHGkw==
+
+cliui@^3.2.0:
+  version "3.2.0"
+  resolved "https://registry.yarnpkg.com/cliui/-/cliui-3.2.0.tgz#120601537a916d29940f934da3b48d585a39213d"
+  integrity sha1-EgYBU3qRbSmUD5NNo7SNWFo5IT0=
+  dependencies:
+    string-width "^1.0.1"
+    strip-ansi "^3.0.1"
+    wrap-ansi "^2.0.0"
+
+cliui@^5.0.0:
+  version "5.0.0"
+  resolved "https://registry.yarnpkg.com/cliui/-/cliui-5.0.0.tgz#deefcfdb2e800784aa34f46fa08e06851c7bbbc5"
+  integrity sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==
+  dependencies:
+    string-width "^3.1.0"
+    strip-ansi "^5.2.0"
+    wrap-ansi "^5.1.0"
 
 cliui@^6.0.0:
   version "6.0.0"
@@ -1507,6 +1766,27 @@ cliui@^6.0.0:
     strip-ansi "^6.0.0"
     wrap-ansi "^6.2.0"
 
+clone@^1.0.2:
+  version "1.0.4"
+  resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.4.tgz#da309cc263df15994c688ca902179ca3c7cd7c7e"
+  integrity sha1-2jCcwmPfFZlMaIypAheco8fNfH4=
+
+cmd-shim@^3.0.0, cmd-shim@^3.0.3:
+  version "3.0.3"
+  resolved "https://registry.yarnpkg.com/cmd-shim/-/cmd-shim-3.0.3.tgz#2c35238d3df37d98ecdd7d5f6b8dc6b21cadc7cb"
+  integrity sha512-DtGg+0xiFhQIntSBRzL2fRQBnmtAVwXIDo4Qq46HPpObYquxMaZS4sb82U9nH91qJrlosC1wa9gwr0QyL/HypA==
+  dependencies:
+    graceful-fs "^4.1.2"
+    mkdirp "~0.5.0"
+
+cmd-shim@~2.0.2:
+  version "2.0.2"
+  resolved "https://registry.yarnpkg.com/cmd-shim/-/cmd-shim-2.0.2.tgz#6fcbda99483a8fd15d7d30a196ca69d688a2efdb"
+  integrity sha1-b8vamUg6j9FdfTChlspp1oii79s=
+  dependencies:
+    graceful-fs "^4.1.2"
+    mkdirp "~0.5.0"
+
 co@^4.6.0:
   version "4.6.0"
   resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184"
@@ -1554,6 +1834,19 @@ color-name@~1.1.4:
   resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2"
   integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
 
+colors@^1.1.2:
+  version "1.4.0"
+  resolved "https://registry.yarnpkg.com/colors/-/colors-1.4.0.tgz#c50491479d4c1bdaed2c9ced32cf7c7dc2360f78"
+  integrity sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==
+
+columnify@~1.5.4:
+  version "1.5.4"
+  resolved "https://registry.yarnpkg.com/columnify/-/columnify-1.5.4.tgz#4737ddf1c7b69a8a7c340570782e947eec8e78bb"
+  integrity sha1-Rzfd8ce2mop8NAVweC6UfuyOeLs=
+  dependencies:
+    strip-ansi "^3.0.0"
+    wcwidth "^1.0.0"
+
 combined-stream@^1.0.6, combined-stream@^1.0.8, combined-stream@~1.0.6:
   version "1.0.8"
   resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f"
@@ -1561,15 +1854,15 @@ combined-stream@^1.0.6, combined-stream@^1.0.8, combined-stream@~1.0.6:
   dependencies:
     delayed-stream "~1.0.0"
 
-commander@^2.11.0, commander@^2.20.0:
+commander@^2.20.0:
   version "2.20.3"
   resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33"
   integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==
 
-commander@^4.0.1:
-  version "4.1.0"
-  resolved "https://registry.yarnpkg.com/commander/-/commander-4.1.0.tgz#545983a0603fe425bc672d66c9e3c89c42121a83"
-  integrity sha512-NIQrwvv9V39FHgGFm36+U9SMQzbiHvU79k+iADraJTpmrFFfx7Ds0IvDoAdZsDrknlkRk14OYoWXb57uTh7/sw==
+commander@^5.1.0:
+  version "5.1.0"
+  resolved "https://registry.yarnpkg.com/commander/-/commander-5.1.0.tgz#46abbd1652f8e059bddaef99bbdcb2ad9cf179ae"
+  integrity sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==
 
 compare-versions@^3.6.0:
   version "3.6.0"
@@ -1586,6 +1879,41 @@ concat-map@0.0.1:
   resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
   integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=
 
+concat-stream@^1.5.0, concat-stream@^1.5.2:
+  version "1.6.2"
+  resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.2.tgz#904bdf194cd3122fc675c77fc4ac3d4ff0fd1a34"
+  integrity sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==
+  dependencies:
+    buffer-from "^1.0.0"
+    inherits "^2.0.3"
+    readable-stream "^2.2.2"
+    typedarray "^0.0.6"
+
+config-chain@~1.1.11:
+  version "1.1.12"
+  resolved "https://registry.yarnpkg.com/config-chain/-/config-chain-1.1.12.tgz#0fde8d091200eb5e808caf25fe618c02f48e4efa"
+  integrity sha512-a1eOIcu8+7lUInge4Rpf/n4Krkf3Dd9lqhljRzII1/Zno/kRtUWnznPO3jOKBmTEktkt3fkxisUcivoj0ebzoA==
+  dependencies:
+    ini "^1.3.4"
+    proto-list "~1.2.1"
+
+configstore@^3.0.0:
+  version "3.1.2"
+  resolved "https://registry.yarnpkg.com/configstore/-/configstore-3.1.2.tgz#c6f25defaeef26df12dd33414b001fe81a543f8f"
+  integrity sha512-vtv5HtGjcYUgFrXc6Kx747B83MRRVS5R1VTEQoXvuP+kMI+if6uywV0nDGoiydJRy4yk7h9od5Og0kxx4zUXmw==
+  dependencies:
+    dot-prop "^4.1.0"
+    graceful-fs "^4.1.2"
+    make-dir "^1.0.0"
+    unique-string "^1.0.0"
+    write-file-atomic "^2.0.0"
+    xdg-basedir "^3.0.0"
+
+console-control-strings@^1.0.0, console-control-strings@^1.1.0, console-control-strings@~1.1.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e"
+  integrity sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=
+
 contains-path@^0.1.0:
   version "0.1.0"
   resolved "https://registry.yarnpkg.com/contains-path/-/contains-path-0.1.0.tgz#fe8cf184ff6670b6baef01a9d4861a5cbec4120a"
@@ -1620,15 +1948,27 @@ cookie@0.4.0:
   resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.0.tgz#beb437e7022b3b6d49019d088665303ebe9c14ba"
   integrity sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==
 
+copy-concurrently@^1.0.0:
+  version "1.0.5"
+  resolved "https://registry.yarnpkg.com/copy-concurrently/-/copy-concurrently-1.0.5.tgz#92297398cae34937fcafd6ec8139c18051f0b5e0"
+  integrity sha512-f2domd9fsVDFtaFcbaRZuYXwtdmnzqbADSwhSWYxYB/Q8zsdUUFMXVRwXGDMWmbEzAn1kdRrtI1T/KTFOL4X2A==
+  dependencies:
+    aproba "^1.1.1"
+    fs-write-stream-atomic "^1.0.8"
+    iferr "^0.1.5"
+    mkdirp "^0.5.1"
+    rimraf "^2.5.4"
+    run-queue "^1.0.0"
+
 copy-descriptor@^0.1.0:
   version "0.1.1"
   resolved "https://registry.yarnpkg.com/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d"
   integrity sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=
 
 core-js-pure@^3.0.0:
-  version "3.6.4"
-  resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.6.4.tgz#4bf1ba866e25814f149d4e9aaa08c36173506e3a"
-  integrity sha512-epIhRLkXdgv32xIUFaaAry2wdxZYBi6bgM7cB136dzzXXa+dFyRLTZeLUJxnd8ShrmyVXBub63n2NHo2JAt8Cw==
+  version "3.6.5"
+  resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.6.5.tgz#c79e75f5e38dbc85a662d91eea52b8256d53b813"
+  integrity sha512-lacdXOimsiD0QyNf9BC/mxivNJ/ybBGJXQFKzRekp1WTHoVUWsUHEn+2T8GJAzzIhyOuXA+gOxCVN3l+5PLPUA==
 
 core-util-is@1.0.2, core-util-is@~1.0.0:
   version "1.0.2"
@@ -1646,7 +1986,23 @@ cosmiconfig@^6.0.0:
     path-type "^4.0.0"
     yaml "^1.7.2"
 
-cross-spawn@^6.0.0, cross-spawn@^6.0.5:
+create-error-class@^3.0.0:
+  version "3.0.2"
+  resolved "https://registry.yarnpkg.com/create-error-class/-/create-error-class-3.0.2.tgz#06be7abef947a3f14a30fd610671d401bca8b7b6"
+  integrity sha1-Br56vvlHo/FKMP1hBnHUAbyot7Y=
+  dependencies:
+    capture-stack-trace "^1.0.0"
+
+cross-spawn@^5.0.1:
+  version "5.1.0"
+  resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-5.1.0.tgz#e8bd0efee58fcff6f8f94510a0a554bbfa235449"
+  integrity sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk=
+  dependencies:
+    lru-cache "^4.0.1"
+    shebang-command "^1.2.0"
+    which "^1.2.9"
+
+cross-spawn@^6.0.0:
   version "6.0.5"
   resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4"
   integrity sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==
@@ -1657,16 +2013,21 @@ cross-spawn@^6.0.0, cross-spawn@^6.0.5:
     shebang-command "^1.2.0"
     which "^1.2.9"
 
-cross-spawn@^7.0.0:
-  version "7.0.1"
-  resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.1.tgz#0ab56286e0f7c24e153d04cc2aa027e43a9a5d14"
-  integrity sha512-u7v4o84SwFpD32Z8IIcPZ6z1/ie24O6RU3RbtL5Y316l3KuHVPx9ItBgWQ6VlfAFnRnTtMUrsQ9MUUTuEZjogg==
+cross-spawn@^7.0.0, cross-spawn@^7.0.2:
+  version "7.0.3"
+  resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6"
+  integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==
   dependencies:
     path-key "^3.1.0"
     shebang-command "^2.0.0"
     which "^2.0.1"
 
-cssom@^0.4.1:
+crypto-random-string@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/crypto-random-string/-/crypto-random-string-1.0.0.tgz#a230f64f568310e1498009940790ec99545bca7e"
+  integrity sha1-ojD2T1aDEOFJgAmUB5DsmVRbyn4=
+
+cssom@^0.4.4:
   version "0.4.4"
   resolved "https://registry.yarnpkg.com/cssom/-/cssom-0.4.4.tgz#5a66cf93d2d0b661d80bf6a44fb65f5c2e4e0a10"
   integrity sha512-p3pvU7r1MyyqbTk+WbNJIgJjG2VmTIaB10rI93LzVPrmDJKkzKYMtxxyAvQXR/NS6otuzveI7+7BBq3SjBS2mw==
@@ -1676,14 +2037,19 @@ cssom@~0.3.6:
   resolved "https://registry.yarnpkg.com/cssom/-/cssom-0.3.8.tgz#9f1276f5b2b463f2114d3f2c75250af8c1a36f4a"
   integrity sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==
 
-cssstyle@^2.0.0:
-  version "2.2.0"
-  resolved "https://registry.yarnpkg.com/cssstyle/-/cssstyle-2.2.0.tgz#e4c44debccd6b7911ed617a4395e5754bba59992"
-  integrity sha512-sEb3XFPx3jNnCAMtqrXPDeSgQr+jojtCeNf8cvMNMh1cG970+lljssvQDzPq6lmmJu2Vhqood/gtEomBiHOGnA==
+cssstyle@^2.2.0:
+  version "2.3.0"
+  resolved "https://registry.yarnpkg.com/cssstyle/-/cssstyle-2.3.0.tgz#ff665a0ddbdc31864b09647f34163443d90b0852"
+  integrity sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A==
   dependencies:
     cssom "~0.3.6"
 
-damerau-levenshtein@^1.0.4:
+cyclist@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/cyclist/-/cyclist-1.0.1.tgz#596e9698fd0c80e12038c2b82d6eb1b35b6224d9"
+  integrity sha1-WW6WmP0MgOEgOMK4LW6xs1tiJNk=
+
+damerau-levenshtein@^1.0.6:
   version "1.0.6"
   resolved "https://registry.yarnpkg.com/damerau-levenshtein/-/damerau-levenshtein-1.0.6.tgz#143c1641cb3d85c60c32329e26899adea8701791"
   integrity sha512-JVrozIeElnj3QzfUIt8tB8YMluBJom4Vw9qTPpjGYQ9fYlB3D/rb6OordUxf3xeFB35LKWs0xqcO5U6ySvBtug==
@@ -1695,19 +2061,14 @@ dashdash@^1.12.0:
   dependencies:
     assert-plus "^1.0.0"
 
-data-urls@^1.1.0:
-  version "1.1.0"
-  resolved "https://registry.yarnpkg.com/data-urls/-/data-urls-1.1.0.tgz#15ee0582baa5e22bb59c77140da8f9c76963bbfe"
-  integrity sha512-YTWYI9se1P55u58gL5GkQHW4P6VJBJ5iBT+B5a7i2Tjadhv52paJG0qHX4A0OR6/t52odI64KP2YvFpkDOi3eQ==
+data-urls@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/data-urls/-/data-urls-2.0.0.tgz#156485a72963a970f5d5821aaf642bef2bf2db9b"
+  integrity sha512-X5eWTSXO/BJmpdIKCRuKUgSCgAN0OwliVK3yPKbwIWU1Tdw5BRajxlzMidvh+gwko9AfQ9zIj52pzF91Q3YAvQ==
   dependencies:
-    abab "^2.0.0"
-    whatwg-mimetype "^2.2.0"
-    whatwg-url "^7.0.0"
-
-date-fns@^1.27.2:
-  version "1.30.1"
-  resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-1.30.1.tgz#2e71bf0b119153dbb4cc4e88d9ea5acfb50dc05c"
-  integrity sha512-hBSVCvSmWC+QypYObzwGOd9wqdDpOt+0wl0KbU+R+uuZBS1jN8VsD1ss3irQDknRj5NvxiTF6oj/nDRnN/UQNw==
+    abab "^2.0.3"
+    whatwg-mimetype "^2.3.0"
+    whatwg-url "^8.0.0"
 
 debug@2.6.9, debug@^2.2.0, debug@^2.3.3, debug@^2.6.9:
   version "2.6.9"
@@ -1716,6 +2077,20 @@ debug@2.6.9, debug@^2.2.0, debug@^2.3.3, debug@^2.6.9:
   dependencies:
     ms "2.0.0"
 
+debug@3.1.0:
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261"
+  integrity sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==
+  dependencies:
+    ms "2.0.0"
+
+debug@^3.1.0:
+  version "3.2.6"
+  resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.6.tgz#e83d17de16d8a7efb7717edbe5fb10135eee629b"
+  integrity sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==
+  dependencies:
+    ms "^2.1.1"
+
 debug@^4.0.1, debug@^4.1.0, debug@^4.1.1:
   version "4.1.1"
   resolved "https://registry.yarnpkg.com/debug/-/debug-4.1.1.tgz#3b72260255109c6b589cee050f1d516139664791"
@@ -1723,11 +2098,21 @@ debug@^4.0.1, debug@^4.1.0, debug@^4.1.1:
   dependencies:
     ms "^2.1.1"
 
-decamelize@^1.2.0:
+debuglog@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/debuglog/-/debuglog-1.0.1.tgz#aa24ffb9ac3df9a2351837cfb2d279360cd78492"
+  integrity sha1-qiT/uaw9+aI1GDfPstJ5NgzXhJI=
+
+decamelize@^1.1.1, decamelize@^1.2.0:
   version "1.2.0"
   resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290"
   integrity sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=
 
+decimal.js@^10.2.0:
+  version "10.2.0"
+  resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-10.2.0.tgz#39466113a9e036111d02f82489b5fd6b0b5ed231"
+  integrity sha512-vDPw+rDgn3bZe1+F/pyEwb1oMG2XTlRVgAa6B4KccTEpYgF8w6eQllVbQcfIJnZyvzFtFpxnpGtx8dd7DJp/Rw==
+
 decode-uri-component@^0.2.0:
   version "0.2.0"
   resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545"
@@ -1738,7 +2123,24 @@ dedent@^0.7.0:
   resolved "https://registry.yarnpkg.com/dedent/-/dedent-0.7.0.tgz#2495ddbaf6eb874abb0e1be9df22d2e5a544326c"
   integrity sha1-JJXduvbrh0q7Dhvp3yLS5aVEMmw=
 
-deep-is@~0.1.3:
+deep-equal@^1.0.1:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-1.1.1.tgz#b5c98c942ceffaf7cb051e24e1434a25a2e6076a"
+  integrity sha512-yd9c5AdiqVcR+JjcwUQb9DkhJc8ngNr0MahEBGvDiJw8puWab2yZlh+nkasOnZP+EGTAP6rRp2JzJhJZzvNF8g==
+  dependencies:
+    is-arguments "^1.0.4"
+    is-date-object "^1.0.1"
+    is-regex "^1.0.4"
+    object-is "^1.0.1"
+    object-keys "^1.1.1"
+    regexp.prototype.flags "^1.2.0"
+
+deep-extend@^0.6.0:
+  version "0.6.0"
+  resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac"
+  integrity sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==
+
+deep-is@^0.1.3, deep-is@~0.1.3:
   version "0.1.3"
   resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34"
   integrity sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=
@@ -1748,6 +2150,13 @@ deepmerge@^4.2.0, deepmerge@^4.2.2:
   resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.2.2.tgz#44d2ea3679b8f4d4ffba33f03d865fc1e7bf4955"
   integrity sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==
 
+defaults@^1.0.3:
+  version "1.0.3"
+  resolved "https://registry.yarnpkg.com/defaults/-/defaults-1.0.3.tgz#c656051e9817d9ff08ed881477f3fe4019f3ef7d"
+  integrity sha1-xlYFHpgX2f8I7YgUd/P+QBnz730=
+  dependencies:
+    clone "^1.0.2"
+
 define-properties@^1.1.2, define-properties@^1.1.3:
   version "1.1.3"
   resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.3.tgz#cf88da6cbee26fe6db7094f61d870cbd84cee9f1"
@@ -1782,6 +2191,11 @@ delayed-stream@~1.0.0:
   resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
   integrity sha1-3zrhmayt+31ECqrgsp4icrJOxhk=
 
+delegates@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a"
+  integrity sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=
+
 depd@~1.1.2:
   version "1.1.2"
   resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9"
@@ -1792,16 +2206,39 @@ destroy@~1.0.4:
   resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80"
   integrity sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=
 
+detect-indent@~5.0.0:
+  version "5.0.0"
+  resolved "https://registry.yarnpkg.com/detect-indent/-/detect-indent-5.0.0.tgz#3871cc0a6a002e8c3e5b3cf7f336264675f06b9d"
+  integrity sha1-OHHMCmoALow+Wzz38zYmRnXwa50=
+
+detect-newline@^2.1.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-2.1.0.tgz#f41f1c10be4b00e87b5f13da680759f2c5bfd3e2"
+  integrity sha1-9B8cEL5LAOh7XxPaaAdZ8sW/0+I=
+
 detect-newline@^3.0.0:
   version "3.1.0"
   resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-3.1.0.tgz#576f5dfc63ae1a192ff192d8ad3af6308991b651"
   integrity sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==
 
+dezalgo@^1.0.0, dezalgo@~1.0.3:
+  version "1.0.3"
+  resolved "https://registry.yarnpkg.com/dezalgo/-/dezalgo-1.0.3.tgz#7f742de066fc748bc8db820569dddce49bf0d456"
+  integrity sha1-f3Qt4Gb8dIvI24IFad3c5Jvw1FY=
+  dependencies:
+    asap "^2.0.0"
+    wrappy "1"
+
 diff-sequences@^25.2.6:
   version "25.2.6"
   resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-25.2.6.tgz#5f467c00edd35352b7bca46d7927d60e687a76dd"
   integrity sha512-Hq8o7+6GaZeoFjtpgvRBUknSXNeJiCx7V9Fr94ZMljNiCr9n9L8H8aJqgWOQiDDGdyn29fRNcDdRVJ5fdyihfg==
 
+diff-sequences@^26.0.0:
+  version "26.0.0"
+  resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-26.0.0.tgz#0760059a5c287637b842bd7085311db7060e88a6"
+  integrity sha512-JC/eHYEC3aSS0vZGjuoc4vHA0yAQTzhQQldXMeMF+JlxLGJlCO38Gma82NV9gk1jGFz8mDzUMeaKXvjRRdJ2dg==
+
 diff@^4.0.1:
   version "4.0.2"
   resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d"
@@ -1829,18 +2266,45 @@ doctrine@^3.0.0:
   dependencies:
     esutils "^2.0.2"
 
-domexception@^1.0.1:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/domexception/-/domexception-1.0.1.tgz#937442644ca6a31261ef36e3ec677fe805582c90"
-  integrity sha512-raigMkn7CJNNo6Ihro1fzG7wr3fHuYVytzquZKX5n0yizGsTcYgzdIUwj1X9pK0VvjeihV+XiclP+DjwbsSKug==
+domexception@^2.0.1:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/domexception/-/domexception-2.0.1.tgz#fb44aefba793e1574b0af6aed2801d057529f304"
+  integrity sha512-yxJ2mFy/sibVQlu5qHjOkf9J3K6zgmCxgJ94u2EdvDOV09H+32LtRswEcUsmUWN72pVLOEnTSRaIVVzVQgS0dg==
   dependencies:
-    webidl-conversions "^4.0.2"
+    webidl-conversions "^5.0.0"
+
+dot-prop@^4.1.0:
+  version "4.2.0"
+  resolved "https://registry.yarnpkg.com/dot-prop/-/dot-prop-4.2.0.tgz#1f19e0c2e1aa0e32797c49799f2837ac6af69c57"
+  integrity sha512-tUMXrxlExSW6U2EXiiKGSBVdYgtV8qlHL+C10TsW4PURY/ic+eaysnSkwB4kA/mBlCyy/IKDJ+Lc3wbWeaXtuQ==
+  dependencies:
+    is-obj "^1.0.0"
+
+dotenv@^5.0.1:
+  version "5.0.1"
+  resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-5.0.1.tgz#a5317459bd3d79ab88cff6e44057a6a3fbb1fcef"
+  integrity sha512-4As8uPrjfwb7VXC+WnLCbXK7y+Ueb2B3zgNCePYfhxS1PYeaO1YTeplffTEcbfLhvFNGLAz90VvJs9yomG7bow==
 
 dotenv@^8.2.0:
   version "8.2.0"
   resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-8.2.0.tgz#97e619259ada750eea3e4ea3e26bceea5424b16a"
   integrity sha512-8sJ78ElpbDJBHNeBzUbUVLsqKdccaa/BXF1uPTw3GrvQTBgrQrtObr2mUrE38vzYd8cEv+m/JBfDLioYcfXoaw==
 
+duplexer3@^0.1.4:
+  version "0.1.4"
+  resolved "https://registry.yarnpkg.com/duplexer3/-/duplexer3-0.1.4.tgz#ee01dd1cac0ed3cbc7fdbea37dc0a8f1ce002ce2"
+  integrity sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI=
+
+duplexify@^3.4.2, duplexify@^3.6.0:
+  version "3.7.1"
+  resolved "https://registry.yarnpkg.com/duplexify/-/duplexify-3.7.1.tgz#2a4df5317f6ccfd91f86d6fd25d8d8a103b88309"
+  integrity sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g==
+  dependencies:
+    end-of-stream "^1.0.0"
+    inherits "^2.0.1"
+    readable-stream "^2.0.0"
+    stream-shift "^1.0.0"
+
 ecc-jsbn@~0.1.1:
   version "0.1.2"
   resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz#3a83a904e54353287874c564b7549386849a98c9"
@@ -1849,17 +2313,17 @@ ecc-jsbn@~0.1.1:
     jsbn "~0.1.0"
     safer-buffer "^2.1.0"
 
+editor@~1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/editor/-/editor-1.0.0.tgz#60c7f87bd62bcc6a894fa8ccd6afb7823a24f742"
+  integrity sha1-YMf4e9YrzGqJT6jM1q+3gjok90I=
+
 ee-first@1.1.1:
   version "1.1.1"
   resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d"
   integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=
 
-elegant-spinner@^1.0.1:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/elegant-spinner/-/elegant-spinner-1.0.1.tgz#db043521c95d7e303fd8f345bedc3349cfb0729e"
-  integrity sha1-2wQ1IcldfjA/2PNFvtwzSc+wcp4=
-
-emoji-regex@^7.0.1, emoji-regex@^7.0.2:
+emoji-regex@^7.0.1:
   version "7.0.3"
   resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-7.0.3.tgz#933a04052860c85e83c122479c4748a8e4c72156"
   integrity sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==
@@ -1869,6 +2333,11 @@ emoji-regex@^8.0.0:
   resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37"
   integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==
 
+emoji-regex@^9.0.0:
+  version "9.0.0"
+  resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-9.0.0.tgz#48a2309cc8a1d2e9d23bc6a67c39b63032e76ea4"
+  integrity sha512-6p1NII1Vm62wni/VR/cUMauVQoxmLVb9csqQlvLz+hO2gk8U2UYDfXHQSUYIBKmZwAKz867IDqG7B+u0mj+M6w==
+
 emoji-short-name@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/emoji-short-name/-/emoji-short-name-1.0.0.tgz#82e6f543b6c68984d69bdc80eac735104fdd4af8"
@@ -1879,17 +2348,43 @@ encodeurl@~1.0.2:
   resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59"
   integrity sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=
 
-end-of-stream@^1.1.0:
+encoding@^0.1.11:
+  version "0.1.13"
+  resolved "https://registry.yarnpkg.com/encoding/-/encoding-0.1.13.tgz#56574afdd791f54a8e9b2785c0582a2d26210fa9"
+  integrity sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==
+  dependencies:
+    iconv-lite "^0.6.2"
+
+end-of-stream@^1.0.0, end-of-stream@^1.1.0:
   version "1.4.4"
   resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0"
   integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==
   dependencies:
     once "^1.4.0"
 
+enquirer@^2.3.5:
+  version "2.3.6"
+  resolved "https://registry.yarnpkg.com/enquirer/-/enquirer-2.3.6.tgz#2a7fe5dd634a1e4125a975ec994ff5456dc3734d"
+  integrity sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg==
+  dependencies:
+    ansi-colors "^4.1.1"
+
 entities@~2.0.0:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/entities/-/entities-2.0.0.tgz#68d6084cab1b079767540d80e56a39b423e4abf4"
-  integrity sha512-D9f7V0JSRwIxlRI2mjMqufDrRDnx8p+eEOz7aUM9SuvF8gsBzra0/6tbjl1m8eQHrZlYj6PxqE00hZ1SAIKPLw==
+  version "2.0.3"
+  resolved "https://registry.yarnpkg.com/entities/-/entities-2.0.3.tgz#5c487e5742ab93c15abb5da22759b8590ec03b7f"
+  integrity sha512-MyoZ0jgnLvB2X3Lg5HqpFmn1kybDiIfEQmKzTb5apr51Rb+T3KdmMiqa70T+bhGnyv7bQ6WMj2QMHpGMmlrUYQ==
+
+err-code@^1.0.0:
+  version "1.1.2"
+  resolved "https://registry.yarnpkg.com/err-code/-/err-code-1.1.2.tgz#06e0116d3028f6aef4806849eb0ea6a748ae6960"
+  integrity sha1-BuARbTAo9q70gGhJ6w6mp0iuaWA=
+
+errno@~0.1.7:
+  version "0.1.7"
+  resolved "https://registry.yarnpkg.com/errno/-/errno-0.1.7.tgz#4684d71779ad39af177e3f007996f7c67c852618"
+  integrity sha512-MfrRBDWzIWifgq6tJj60gkAwtLNb6sQPlcFrSOflcP1aFmmruKQ2wRnze/8V6kgyz7H3FF8Npzv78mZ7XLLflg==
+  dependencies:
+    prr "~1.0.1"
 
 error-ex@^1.2.0, error-ex@^1.3.1:
   version "1.3.2"
@@ -1898,22 +2393,22 @@ error-ex@^1.2.0, error-ex@^1.3.1:
   dependencies:
     is-arrayish "^0.2.1"
 
-es-abstract@^1.17.0, es-abstract@^1.17.0-next.1:
-  version "1.17.4"
-  resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.17.4.tgz#e3aedf19706b20e7c2594c35fc0d57605a79e184"
-  integrity sha512-Ae3um/gb8F0mui/jPL+QiqmglkUsaQf7FwBEHYIFkztkneosu9imhqHpBzQ3h1vit8t5iQ74t6PEVvphBZiuiQ==
+es-abstract@^1.17.0, es-abstract@^1.17.0-next.1, es-abstract@^1.17.5:
+  version "1.17.6"
+  resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.17.6.tgz#9142071707857b2cacc7b89ecb670316c3e2d52a"
+  integrity sha512-Fr89bON3WFyUi5EvAeI48QTWX0AyekGgLA8H+c+7fbfCkJwRWRMLd8CQedNEyJuoYYhmtEqY92pgte1FAhBlhw==
   dependencies:
     es-to-primitive "^1.2.1"
     function-bind "^1.1.1"
     has "^1.0.3"
     has-symbols "^1.0.1"
-    is-callable "^1.1.5"
-    is-regex "^1.0.5"
+    is-callable "^1.2.0"
+    is-regex "^1.1.0"
     object-inspect "^1.7.0"
     object-keys "^1.1.1"
     object.assign "^4.1.0"
-    string.prototype.trimleft "^2.1.1"
-    string.prototype.trimright "^2.1.1"
+    string.prototype.trimend "^1.0.1"
+    string.prototype.trimstart "^1.0.1"
 
 es-to-primitive@^1.2.1:
   version "1.2.1"
@@ -1924,32 +2419,37 @@ es-to-primitive@^1.2.1:
     is-date-object "^1.0.1"
     is-symbol "^1.0.2"
 
+es6-promise@^4.0.3:
+  version "4.2.8"
+  resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.2.8.tgz#4eb21594c972bc40553d276e510539143db53e0a"
+  integrity sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==
+
+es6-promisify@^5.0.0:
+  version "5.0.0"
+  resolved "https://registry.yarnpkg.com/es6-promisify/-/es6-promisify-5.0.0.tgz#5109d62f3e56ea967c4b63505aef08291c8a5203"
+  integrity sha1-UQnWLz5W6pZ8S2NQWu8IKRyKUgM=
+  dependencies:
+    es6-promise "^4.0.3"
+
 escape-html@~1.0.3:
   version "1.0.3"
   resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988"
   integrity sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=
 
-escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.5:
+escape-string-regexp@^1.0.5:
   version "1.0.5"
   resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4"
   integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=
 
-escodegen@^1.11.1:
-  version "1.14.1"
-  resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-1.14.1.tgz#ba01d0c8278b5e95a9a45350142026659027a457"
-  integrity sha512-Bmt7NcRySdIfNPfU2ZoXDrrXsG9ZjvDxcAlMfDUgRBjLOWTuIACXPBFJH7Z+cLb40JeQco5toikyc9t9P8E9SQ==
-  dependencies:
-    esprima "^4.0.1"
-    estraverse "^4.2.0"
-    esutils "^2.0.2"
-    optionator "^0.8.1"
-  optionalDependencies:
-    source-map "~0.6.1"
+escape-string-regexp@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz#a30304e99daa32e23b2fd20f51babd07cffca344"
+  integrity sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==
 
-escodegen@^1.8.1:
-  version "1.13.0"
-  resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-1.13.0.tgz#c7adf9bd3f3cc675bb752f202f79a720189cab29"
-  integrity sha512-eYk2dCkxR07DsHA/X2hRBj0CFAZeri/LyDMc0C8JT1Hqi6JnVpMhJ7XFITbb0+yZS3lVkaPL2oCkZ3AVmeVbMw==
+escodegen@^1.14.1, escodegen@^1.8.1:
+  version "1.14.3"
+  resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-1.14.3.tgz#4e7b81fba61581dc97582ed78cab7f0e8d63f503"
+  integrity sha512-qFcX0XJkdg+PB3xjZZG/wKSuT1PnQWx57+TVSjIMmILd2yC/6ByYElPwJnslDsuWuSAp4AwJGumarAAmJch5Kw==
   dependencies:
     esprima "^4.0.1"
     estraverse "^4.2.0"
@@ -1966,122 +2466,127 @@ eslint-ast-utils@^1.1.0:
     lodash.get "^4.4.2"
     lodash.zip "^4.2.0"
 
-eslint-config-prettier@6.10.0:
-  version "6.10.0"
-  resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-6.10.0.tgz#7b15e303bf9c956875c948f6b21500e48ded6a7f"
-  integrity sha512-AtndijGte1rPILInUdHjvKEGbIV06NuvPrqlIEaEaWtbtvJh464mDeyGMdZEQMsGvC0ZVkiex1fSNcC4HAbRGg==
+eslint-config-prettier@6.11.0:
+  version "6.11.0"
+  resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-6.11.0.tgz#f6d2238c1290d01c859a8b5c1f7d352a0b0da8b1"
+  integrity sha512-oB8cpLWSAjOVFEJhhyMZh6NOEOtBVziaqdDQ86+qhDHFbZXoRTM7pNSvFRfW/W/L/LrQ38C99J5CGuRBBzBsdA==
   dependencies:
     get-stdin "^6.0.0"
 
-eslint-import-resolver-node@^0.3.2:
-  version "0.3.3"
-  resolved "https://registry.yarnpkg.com/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.3.tgz#dbaa52b6b2816b50bc6711af75422de808e98404"
-  integrity sha512-b8crLDo0M5RSe5YG8Pu2DYBj71tSB6OvXkfzwbJU2w7y8P4/yo0MyF8jU26IEuEuHF2K5/gcAJE3LhQGqBBbVg==
+eslint-import-resolver-node@^0.3.3:
+  version "0.3.4"
+  resolved "https://registry.yarnpkg.com/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.4.tgz#85ffa81942c25012d8231096ddf679c03042c717"
+  integrity sha512-ogtf+5AB/O+nM6DIeBUNr2fuT7ot9Qg/1harBfBtaP13ekEWFQEEMP94BCB7zaNW3gyY+8SHYF00rnqYwXKWOA==
   dependencies:
     debug "^2.6.9"
     resolve "^1.13.1"
 
-eslint-module-utils@^2.4.1:
-  version "2.5.2"
-  resolved "https://registry.yarnpkg.com/eslint-module-utils/-/eslint-module-utils-2.5.2.tgz#7878f7504824e1b857dd2505b59a8e5eda26a708"
-  integrity sha512-LGScZ/JSlqGKiT8OC+cYRxseMjyqt6QO54nl281CK93unD89ijSeRV6An8Ci/2nvWVKe8K/Tqdm75RQoIOCr+Q==
+eslint-module-utils@^2.6.0:
+  version "2.6.0"
+  resolved "https://registry.yarnpkg.com/eslint-module-utils/-/eslint-module-utils-2.6.0.tgz#579ebd094f56af7797d19c9866c9c9486629bfa6"
+  integrity sha512-6j9xxegbqe8/kZY8cYpcp0xhbK0EgJlg3g9mib3/miLaExuuwc3n5UEfSnU6hWMbT0FAYVvDbL9RrRgpUeQIvA==
   dependencies:
     debug "^2.6.9"
     pkg-dir "^2.0.0"
 
-eslint-plugin-babel@5.3.0:
-  version "5.3.0"
-  resolved "https://registry.yarnpkg.com/eslint-plugin-babel/-/eslint-plugin-babel-5.3.0.tgz#2e7f251ccc249326da760c1a4c948a91c32d0023"
-  integrity sha512-HPuNzSPE75O+SnxHIafbW5QB45r2w78fxqwK3HmjqIUoPfPzVrq6rD+CINU3yzoDSzEhUkX07VUphbF73Lth/w==
+eslint-plugin-babel@5.3.1:
+  version "5.3.1"
+  resolved "https://registry.yarnpkg.com/eslint-plugin-babel/-/eslint-plugin-babel-5.3.1.tgz#75a2413ffbf17e7be57458301c60291f2cfbf560"
+  integrity sha512-VsQEr6NH3dj664+EyxJwO4FCYm/00JhYb3Sk3ft8o+fpKuIfQ9TaW6uVUfvwMXHcf/lsnRIoyFPsLMyiWCSL/g==
   dependencies:
     eslint-rule-composer "^0.3.0"
 
 eslint-plugin-es@^3.0.0:
-  version "3.0.0"
-  resolved "https://registry.yarnpkg.com/eslint-plugin-es/-/eslint-plugin-es-3.0.0.tgz#98cb1bc8ab0aa807977855e11ad9d1c9422d014b"
-  integrity sha512-6/Jb/J/ZvSebydwbBJO1R9E5ky7YeElfK56Veh7e4QGFHCXoIXGH9HhVz+ibJLM3XJ1XjP+T7rKBLUa/Y7eIng==
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/eslint-plugin-es/-/eslint-plugin-es-3.0.1.tgz#75a7cdfdccddc0589934aeeb384175f221c57893"
+  integrity sha512-GUmAsJaN4Fc7Gbtl8uOBlayo2DqhwWvEzykMHSCZHU3XdJ+NSzzZcVhXh3VxX5icqQ+oQdIEawXX8xkR3mIFmQ==
   dependencies:
     eslint-utils "^2.0.0"
     regexpp "^3.0.0"
 
-eslint-plugin-import@2.20.1:
-  version "2.20.1"
-  resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.20.1.tgz#802423196dcb11d9ce8435a5fc02a6d3b46939b3"
-  integrity sha512-qQHgFOTjguR+LnYRoToeZWT62XM55MBVXObHM6SKFd1VzDcX/vqT1kAz8ssqigh5eMj8qXcRoXXGZpPP6RfdCw==
+eslint-plugin-import@2.22.0:
+  version "2.22.0"
+  resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.22.0.tgz#92f7736fe1fde3e2de77623c838dd992ff5ffb7e"
+  integrity sha512-66Fpf1Ln6aIS5Gr/55ts19eUuoDhAbZgnr6UxK5hbDx6l/QgQgx61AePq+BV4PP2uXQFClgMVzep5zZ94qqsxg==
   dependencies:
-    array-includes "^3.0.3"
-    array.prototype.flat "^1.2.1"
+    array-includes "^3.1.1"
+    array.prototype.flat "^1.2.3"
     contains-path "^0.1.0"
     debug "^2.6.9"
     doctrine "1.5.0"
-    eslint-import-resolver-node "^0.3.2"
-    eslint-module-utils "^2.4.1"
+    eslint-import-resolver-node "^0.3.3"
+    eslint-module-utils "^2.6.0"
     has "^1.0.3"
     minimatch "^3.0.4"
-    object.values "^1.1.0"
+    object.values "^1.1.1"
     read-pkg-up "^2.0.0"
-    resolve "^1.12.0"
+    resolve "^1.17.0"
+    tsconfig-paths "^3.9.0"
 
 eslint-plugin-inferno@^7.14.3:
-  version "7.14.3"
-  resolved "https://registry.yarnpkg.com/eslint-plugin-inferno/-/eslint-plugin-inferno-7.14.3.tgz#eb5afb3ee23039261dde44cd102f65154a6eaa0a"
-  integrity sha512-BntEmVvbJSSZCV10OyiXXo620Hb6c5mnbBxfiIvAmE2KCy9Dmqu9/vSEAiOsINHwH8wjHHgnjibwB3J51TFQfw==
+  version "7.20.1"
+  resolved "https://registry.yarnpkg.com/eslint-plugin-inferno/-/eslint-plugin-inferno-7.20.1.tgz#e01f9436b5db8c89e9ece3550850751a4e2d9be9"
+  integrity sha512-Cy19MA7ea5AGoeBxDbN3Uf4m/W5pr0SpbGcrfq5UhEk7qgbguAUFF22TSXQAVBhXPQ/ZW6aB3md8H63QPAWyIw==
   dependencies:
-    array-includes "^3.0.3"
+    array-includes "^3.1.1"
     doctrine "^3.0.0"
     has "^1.0.3"
-    jsx-ast-utils "^2.2.1"
-    object.entries "^1.1.0"
-    object.fromentries "^2.0.0"
-    object.values "^1.1.0"
-    resolve "^1.12.0"
+    jsx-ast-utils "^2.3.0"
+    object.entries "^1.1.2"
+    object.fromentries "^2.0.2"
+    object.values "^1.1.1"
+    resolve "^1.17.0"
+    string.prototype.matchall "^4.0.2"
+    xregexp "^4.3.0"
 
-eslint-plugin-jane@^7.2.1:
-  version "7.2.1"
-  resolved "https://registry.yarnpkg.com/eslint-plugin-jane/-/eslint-plugin-jane-7.2.1.tgz#5ffba9ce75e0a5e5dbe3918fc0c5332d2cd89c13"
-  integrity sha512-hUmhEkHTDq6lQ4oLWZV5cLut9L67fcTiy0USbTsEOx658i9Jdikedt8NJhtamRqO5OUHBGSPU0JkOqBtVNUD+A==
+eslint-plugin-jane@^8.0.4:
+  version "8.0.4"
+  resolved "https://registry.yarnpkg.com/eslint-plugin-jane/-/eslint-plugin-jane-8.0.4.tgz#31c193cd30d2f851af91dfd58b85fe3b30aff987"
+  integrity sha512-h3vfVfARv1g8DeJiEegCeLKXXMxs/VHBrDPX4x5XECOZu/8Ngi7GUFPUguJIvYMouam9OoAbYCVQIWEOk48nBQ==
   dependencies:
-    "@typescript-eslint/eslint-plugin" "2.24.0"
-    "@typescript-eslint/parser" "2.24.0"
+    "@typescript-eslint/eslint-plugin" "3.6.1"
+    "@typescript-eslint/parser" "3.6.1"
     babel-eslint "10.1.0"
-    eslint-config-prettier "6.10.0"
-    eslint-plugin-babel "5.3.0"
-    eslint-plugin-import "2.20.1"
-    eslint-plugin-jest "23.8.2"
-    eslint-plugin-jsx-a11y "6.2.3"
-    eslint-plugin-node "11.0.0"
-    eslint-plugin-prettier "3.1.2"
+    eslint-config-prettier "6.11.0"
+    eslint-plugin-babel "5.3.1"
+    eslint-plugin-import "2.22.0"
+    eslint-plugin-jest "23.18.0"
+    eslint-plugin-jsx-a11y "6.3.1"
+    eslint-plugin-node "11.1.0"
+    eslint-plugin-prettier "3.1.4"
     eslint-plugin-promise "4.2.1"
-    eslint-plugin-react "7.19.0"
-    eslint-plugin-react-hooks "2.5.1"
-    eslint-plugin-unicorn "17.2.0"
+    eslint-plugin-react "7.20.3"
+    eslint-plugin-react-hooks "4.0.8"
+    eslint-plugin-unicorn "20.1.0"
 
-eslint-plugin-jest@23.8.2:
-  version "23.8.2"
-  resolved "https://registry.yarnpkg.com/eslint-plugin-jest/-/eslint-plugin-jest-23.8.2.tgz#6f28b41c67ef635f803ebd9e168f6b73858eb8d4"
-  integrity sha512-xwbnvOsotSV27MtAe7s8uGWOori0nUsrXh2f1EnpmXua8sDfY6VZhHAhHg2sqK7HBNycRQExF074XSZ7DvfoFg==
+eslint-plugin-jest@23.18.0:
+  version "23.18.0"
+  resolved "https://registry.yarnpkg.com/eslint-plugin-jest/-/eslint-plugin-jest-23.18.0.tgz#4813eacb181820ed13c5505f400956d176b25af8"
+  integrity sha512-wLPM/Rm1SGhxrFQ2TKM/BYsYPhn7ch6ZEK92S2o/vGkAAnDXM0I4nTIo745RIX+VlCRMFgBuJEax6XfTHMdeKg==
   dependencies:
     "@typescript-eslint/experimental-utils" "^2.5.0"
 
-eslint-plugin-jsx-a11y@6.2.3:
-  version "6.2.3"
-  resolved "https://registry.yarnpkg.com/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.2.3.tgz#b872a09d5de51af70a97db1eea7dc933043708aa"
-  integrity sha512-CawzfGt9w83tyuVekn0GDPU9ytYtxyxyFZ3aSWROmnRRFQFT2BiPJd7jvRdzNDi6oLWaS2asMeYSNMjWTV4eNg==
+eslint-plugin-jsx-a11y@6.3.1:
+  version "6.3.1"
+  resolved "https://registry.yarnpkg.com/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.3.1.tgz#99ef7e97f567cc6a5b8dd5ab95a94a67058a2660"
+  integrity sha512-i1S+P+c3HOlBJzMFORRbC58tHa65Kbo8b52/TwCwSKLohwvpfT5rm2GjGWzOHTEuq4xxf2aRlHHTtmExDQOP+g==
   dependencies:
-    "@babel/runtime" "^7.4.5"
-    aria-query "^3.0.0"
-    array-includes "^3.0.3"
+    "@babel/runtime" "^7.10.2"
+    aria-query "^4.2.2"
+    array-includes "^3.1.1"
     ast-types-flow "^0.0.7"
-    axobject-query "^2.0.2"
-    damerau-levenshtein "^1.0.4"
-    emoji-regex "^7.0.2"
+    axe-core "^3.5.4"
+    axobject-query "^2.1.2"
+    damerau-levenshtein "^1.0.6"
+    emoji-regex "^9.0.0"
     has "^1.0.3"
-    jsx-ast-utils "^2.2.1"
+    jsx-ast-utils "^2.4.1"
+    language-tags "^1.0.5"
 
-eslint-plugin-node@11.0.0:
-  version "11.0.0"
-  resolved "https://registry.yarnpkg.com/eslint-plugin-node/-/eslint-plugin-node-11.0.0.tgz#365944bb0804c5d1d501182a9bc41a0ffefed726"
-  integrity sha512-chUs/NVID+sknFiJzxoN9lM7uKSOEta8GC8365hw1nDfwIPIjjpRSwwPvQanWv8dt/pDe9EV4anmVSwdiSndNg==
+eslint-plugin-node@11.1.0:
+  version "11.1.0"
+  resolved "https://registry.yarnpkg.com/eslint-plugin-node/-/eslint-plugin-node-11.1.0.tgz#c95544416ee4ada26740a30474eefc5402dc671d"
+  integrity sha512-oUwtPJ1W0SKD0Tr+wqu92c5xuCeQqB3hSCHasn/ZgjFdA9iDGNkNf2Zi9ztY7X+hNuMib23LNGRm6+uN+KLE3g==
   dependencies:
     eslint-plugin-es "^3.0.0"
     eslint-utils "^2.0.0"
@@ -2090,10 +2595,10 @@ eslint-plugin-node@11.0.0:
     resolve "^1.10.1"
     semver "^6.1.0"
 
-eslint-plugin-prettier@3.1.2:
-  version "3.1.2"
-  resolved "https://registry.yarnpkg.com/eslint-plugin-prettier/-/eslint-plugin-prettier-3.1.2.tgz#432e5a667666ab84ce72f945c72f77d996a5c9ba"
-  integrity sha512-GlolCC9y3XZfv3RQfwGew7NnuFDKsfI4lbvRK+PIIo23SFH+LemGs4cKwzAaRa+Mdb+lQO/STaIayno8T5sJJA==
+eslint-plugin-prettier@3.1.4:
+  version "3.1.4"
+  resolved "https://registry.yarnpkg.com/eslint-plugin-prettier/-/eslint-plugin-prettier-3.1.4.tgz#168ab43154e2ea57db992a2cd097c828171f75c2"
+  integrity sha512-jZDa8z76klRqo+TdGDTFJSavwbnWK2ZpqGKNZ+VvweMW516pDUMmQ2koXvxEE4JhzNvTv+radye/bWGBmA6jmg==
   dependencies:
     prettier-linter-helpers "^1.0.0"
 
@@ -2102,103 +2607,99 @@ eslint-plugin-promise@4.2.1:
   resolved "https://registry.yarnpkg.com/eslint-plugin-promise/-/eslint-plugin-promise-4.2.1.tgz#845fd8b2260ad8f82564c1222fce44ad71d9418a"
   integrity sha512-VoM09vT7bfA7D+upt+FjeBO5eHIJQBUWki1aPvB+vbNiHS3+oGIJGIeyBtKQTME6UPXXy3vV07OL1tHd3ANuDw==
 
-eslint-plugin-react-hooks@2.5.1:
-  version "2.5.1"
-  resolved "https://registry.yarnpkg.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-2.5.1.tgz#4ef5930592588ce171abeb26f400c7fbcbc23cd0"
-  integrity sha512-Y2c4b55R+6ZzwtTppKwSmK/Kar8AdLiC2f9NADCuxbcTgPPg41Gyqa6b9GppgXSvCtkRw43ZE86CT5sejKC6/g==
+eslint-plugin-react-hooks@4.0.8:
+  version "4.0.8"
+  resolved "https://registry.yarnpkg.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.0.8.tgz#a9b1e3d57475ccd18276882eff3d6cba00da7a56"
+  integrity sha512-6SSb5AiMCPd8FDJrzah+Z4F44P2CdOaK026cXFV+o/xSRzfOiV1FNFeLl2z6xm3yqWOQEZ5OfVgiec90qV2xrQ==
 
-eslint-plugin-react@7.19.0:
-  version "7.19.0"
-  resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.19.0.tgz#6d08f9673628aa69c5559d33489e855d83551666"
-  integrity sha512-SPT8j72CGuAP+JFbT0sJHOB80TX/pu44gQ4vXH/cq+hQTiY2PuZ6IHkqXJV6x1b28GDdo1lbInjKUrrdUf0LOQ==
+eslint-plugin-react@7.20.3:
+  version "7.20.3"
+  resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.20.3.tgz#0590525e7eb83890ce71f73c2cf836284ad8c2f1"
+  integrity sha512-txbo090buDeyV0ugF3YMWrzLIUqpYTsWSDZV9xLSmExE1P/Kmgg9++PD931r+KEWS66O1c9R4srLVVHmeHpoAg==
   dependencies:
     array-includes "^3.1.1"
+    array.prototype.flatmap "^1.2.3"
     doctrine "^2.1.0"
     has "^1.0.3"
-    jsx-ast-utils "^2.2.3"
-    object.entries "^1.1.1"
+    jsx-ast-utils "^2.4.1"
+    object.entries "^1.1.2"
     object.fromentries "^2.0.2"
     object.values "^1.1.1"
     prop-types "^15.7.2"
-    resolve "^1.15.1"
-    semver "^6.3.0"
+    resolve "^1.17.0"
     string.prototype.matchall "^4.0.2"
-    xregexp "^4.3.0"
 
-eslint-plugin-unicorn@17.2.0:
-  version "17.2.0"
-  resolved "https://registry.yarnpkg.com/eslint-plugin-unicorn/-/eslint-plugin-unicorn-17.2.0.tgz#8f147ba24d417dc5de948c7df7d006108a37a540"
-  integrity sha512-0kYjrywf0kQxevFz571KrDfYMIRZ5Kq6dDgPU1EEBFeC181r+fAaPatBScWX+/hisKJ4+eCRFebxTeVylsSYmw==
+eslint-plugin-unicorn@20.1.0:
+  version "20.1.0"
+  resolved "https://registry.yarnpkg.com/eslint-plugin-unicorn/-/eslint-plugin-unicorn-20.1.0.tgz#a43f60ffc98406d72ec2a5fcc6dad24ba0192bc9"
+  integrity sha512-XQxLBJT/gnwyRR6cfYsIK1AdekQchAt5tmcsnldevGjgR2xoZsRUa5/i6e0seNHy2RoT57CkTnbVHwHF8No8LA==
   dependencies:
     ci-info "^2.0.0"
     clean-regexp "^1.0.0"
     eslint-ast-utils "^1.1.0"
-    eslint-template-visitor "^1.1.0"
+    eslint-template-visitor "^2.0.0"
+    eslint-utils "^2.0.0"
     import-modules "^2.0.0"
     lodash "^4.17.15"
+    pluralize "^8.0.0"
     read-pkg-up "^7.0.1"
-    regexp-tree "^0.1.20"
+    regexp-tree "^0.1.21"
     reserved-words "^0.1.2"
     safe-regex "^2.1.1"
-    semver "^7.1.2"
+    semver "^7.3.2"
 
 eslint-rule-composer@^0.3.0:
   version "0.3.0"
   resolved "https://registry.yarnpkg.com/eslint-rule-composer/-/eslint-rule-composer-0.3.0.tgz#79320c927b0c5c0d3d3d2b76c8b4a488f25bbaf9"
   integrity sha512-bt+Sh8CtDmn2OajxvNO+BX7Wn4CIWMpTRm3MaiKPCQcnnlm0CS2mhui6QaoeQugs+3Kj2ESKEEGJUdVafwhiCg==
 
-eslint-scope@^5.0.0:
-  version "5.0.0"
-  resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.0.0.tgz#e87c8887c73e8d1ec84f1ca591645c358bfc8fb9"
-  integrity sha512-oYrhJW7S0bxAFDvWqzvMPRm6pcgcnWc4QnofCAqRTRfQC0JcwenzGglTtsLyIuuWFfkqDG9vz67cnttSd53djw==
+eslint-scope@^5.0.0, eslint-scope@^5.1.0:
+  version "5.1.0"
+  resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.1.0.tgz#d0f971dfe59c69e0cada684b23d49dbf82600ce5"
+  integrity sha512-iiGRvtxWqgtx5m8EyQUJihBloE4EnYeGE/bz1wSPwJE6tZuJUtHlhqDM4Xj2ukE8Dyy1+HCZ4hE0fzIVMzb58w==
   dependencies:
     esrecurse "^4.1.0"
     estraverse "^4.1.1"
 
-eslint-template-visitor@^1.1.0:
-  version "1.1.0"
-  resolved "https://registry.yarnpkg.com/eslint-template-visitor/-/eslint-template-visitor-1.1.0.tgz#f090d124d1a52e05552149fc50468ed59608b166"
-  integrity sha512-Lmy6QVlmFiIGl5fPi+8ACnov3sare+0Ouf7deJAGGhmUfeWJ5fVarELUxZRpsZ9sHejiJUq8626d0dn9uvcZTw==
-  dependencies:
-    eslint-visitor-keys "^1.1.0"
-    espree "^6.1.1"
-    multimap "^1.0.2"
-
-eslint-utils@^1.4.3:
-  version "1.4.3"
-  resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-1.4.3.tgz#74fec7c54d0776b6f67e0251040b5806564e981f"
-  integrity sha512-fbBN5W2xdY45KulGXmLHZ3c3FHfVYmKg0IrAKGOkT/464PQsx2UeIzfz1RmEci+KLm1bBaAzZAh8+/E+XAeZ8Q==
+eslint-template-visitor@^2.0.0:
+  version "2.2.1"
+  resolved "https://registry.yarnpkg.com/eslint-template-visitor/-/eslint-template-visitor-2.2.1.tgz#2dccb1ab28fa7429e56ba6dd0144def2d89bc2d6"
+  integrity sha512-q3SxoBXz0XjPGkUpwGVAwIwIPIxzCAJX1uwfVc8tW3v7u/zS7WXNH3I2Mu2MDz2NgSITAyKLRaQFPHu/iyKxDQ==
   dependencies:
-    eslint-visitor-keys "^1.1.0"
+    babel-eslint "^10.1.0"
+    eslint-visitor-keys "^1.3.0"
+    esquery "^1.3.1"
+    multimap "^1.1.0"
 
-eslint-utils@^2.0.0:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-2.0.0.tgz#7be1cc70f27a72a76cd14aa698bcabed6890e1cd"
-  integrity sha512-0HCPuJv+7Wv1bACm8y5/ECVfYdfsAm9xmVb7saeFlxjPYALefjhbYoCkBjPdPzGH8wWyTpAez82Fh3VKYEZ8OA==
+eslint-utils@^2.0.0, eslint-utils@^2.1.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-2.1.0.tgz#d2de5e03424e707dc10c74068ddedae708741b27"
+  integrity sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==
   dependencies:
     eslint-visitor-keys "^1.1.0"
 
-eslint-visitor-keys@^1.0.0, eslint-visitor-keys@^1.1.0:
-  version "1.1.0"
-  resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-1.1.0.tgz#e2a82cea84ff246ad6fb57f9bde5b46621459ec2"
-  integrity sha512-8y9YjtM1JBJU/A9Kc+SbaOV4y29sSWckBwMHa+FGtVj5gN/sbnKDf6xJUl+8g7FAij9LVaP8C24DUiH/f/2Z9A==
+eslint-visitor-keys@^1.0.0, eslint-visitor-keys@^1.1.0, eslint-visitor-keys@^1.3.0:
+  version "1.3.0"
+  resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz#30ebd1ef7c2fdff01c3a4f151044af25fab0523e"
+  integrity sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==
 
-eslint@^6.5.1:
-  version "6.8.0"
-  resolved "https://registry.yarnpkg.com/eslint/-/eslint-6.8.0.tgz#62262d6729739f9275723824302fb227c8c93ffb"
-  integrity sha512-K+Iayyo2LtyYhDSYwz5D5QdWw0hCacNzyq1Y821Xna2xSJj7cijoLLYmLxTQgcgZ9mC61nryMy9S7GRbYpI5Ig==
+eslint@^7.5.0:
+  version "7.5.0"
+  resolved "https://registry.yarnpkg.com/eslint/-/eslint-7.5.0.tgz#9ecbfad62216d223b82ac9ffea7ef3444671d135"
+  integrity sha512-vlUP10xse9sWt9SGRtcr1LAC67BENcQMFeV+w5EvLEoFe3xJ8cF1Skd0msziRx/VMC+72B4DxreCE+OR12OA6Q==
   dependencies:
     "@babel/code-frame" "^7.0.0"
     ajv "^6.10.0"
-    chalk "^2.1.0"
-    cross-spawn "^6.0.5"
+    chalk "^4.0.0"
+    cross-spawn "^7.0.2"
     debug "^4.0.1"
     doctrine "^3.0.0"
-    eslint-scope "^5.0.0"
-    eslint-utils "^1.4.3"
-    eslint-visitor-keys "^1.1.0"
-    espree "^6.1.2"
-    esquery "^1.0.1"
+    enquirer "^2.3.5"
+    eslint-scope "^5.1.0"
+    eslint-utils "^2.1.0"
+    eslint-visitor-keys "^1.3.0"
+    espree "^7.2.0"
+    esquery "^1.2.0"
     esutils "^2.0.2"
     file-entry-cache "^5.0.1"
     functional-red-black-tree "^1.0.1"
@@ -2207,45 +2708,43 @@ eslint@^6.5.1:
     ignore "^4.0.6"
     import-fresh "^3.0.0"
     imurmurhash "^0.1.4"
-    inquirer "^7.0.0"
     is-glob "^4.0.0"
     js-yaml "^3.13.1"
     json-stable-stringify-without-jsonify "^1.0.1"
-    levn "^0.3.0"
-    lodash "^4.17.14"
+    levn "^0.4.1"
+    lodash "^4.17.19"
     minimatch "^3.0.4"
-    mkdirp "^0.5.1"
     natural-compare "^1.4.0"
-    optionator "^0.8.3"
+    optionator "^0.9.1"
     progress "^2.0.0"
-    regexpp "^2.0.1"
-    semver "^6.1.2"
-    strip-ansi "^5.2.0"
-    strip-json-comments "^3.0.1"
+    regexpp "^3.1.0"
+    semver "^7.2.1"
+    strip-ansi "^6.0.0"
+    strip-json-comments "^3.1.0"
     table "^5.2.3"
     text-table "^0.2.0"
     v8-compile-cache "^2.0.3"
 
-espree@^6.1.1, espree@^6.1.2:
-  version "6.1.2"
-  resolved "https://registry.yarnpkg.com/espree/-/espree-6.1.2.tgz#6c272650932b4f91c3714e5e7b5f5e2ecf47262d"
-  integrity sha512-2iUPuuPP+yW1PZaMSDM9eyVf8D5P0Hi8h83YtZ5bPc/zHYjII5khoixIUTMO794NOY8F/ThF1Bo8ncZILarUTA==
+espree@^7.2.0:
+  version "7.2.0"
+  resolved "https://registry.yarnpkg.com/espree/-/espree-7.2.0.tgz#1c263d5b513dbad0ac30c4991b93ac354e948d69"
+  integrity sha512-H+cQ3+3JYRMEIOl87e7QdHX70ocly5iW4+dttuR8iYSPr/hXKFb+7dBsZ7+u1adC4VrnPlTkv0+OwuPnDop19g==
   dependencies:
-    acorn "^7.1.0"
-    acorn-jsx "^5.1.0"
-    eslint-visitor-keys "^1.1.0"
+    acorn "^7.3.1"
+    acorn-jsx "^5.2.0"
+    eslint-visitor-keys "^1.3.0"
 
 esprima@^4.0.0, esprima@^4.0.1:
   version "4.0.1"
   resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71"
   integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==
 
-esquery@^1.0.1:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.0.1.tgz#406c51658b1f5991a5f9b62b1dc25b00e3e5c708"
-  integrity sha512-SmiyZ5zIWH9VM+SRUReLS5Q8a7GxtRdxEBVZpm98rJM7Sb+A9DVCndXfkeFUd3byderg+EbDkfnevfCwynWaNA==
+esquery@^1.2.0, esquery@^1.3.1:
+  version "1.3.1"
+  resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.3.1.tgz#b78b5828aa8e214e29fb74c4d5b752e1c033da57"
+  integrity sha512-olpvt9QG0vniUBZspVRN6lwB7hOZoTRtT+jzR+tS4ffYx2mzbw+z0XCOk44aaLYKApNX5nMm+E+P6o25ip/DHQ==
   dependencies:
-    estraverse "^4.0.0"
+    estraverse "^5.1.0"
 
 esrecurse@^4.1.0:
   version "4.2.1"
@@ -2254,11 +2753,16 @@ esrecurse@^4.1.0:
   dependencies:
     estraverse "^4.1.0"
 
-estraverse@^4.0.0, estraverse@^4.1.0, estraverse@^4.1.1, estraverse@^4.2.0:
+estraverse@^4.1.0, estraverse@^4.1.1, estraverse@^4.2.0:
   version "4.3.0"
   resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d"
   integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==
 
+estraverse@^5.1.0:
+  version "5.1.0"
+  resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.1.0.tgz#374309d39fd935ae500e7b92e8a6b4c720e59642"
+  integrity sha512-FyohXK+R0vE+y1nHLoBM7ZTyqRpqAlhdZHCWIWEviFLiGB8b04H6bQs8G+XTthacvT8VuwvteiP7RJSxMs8UEw==
+
 esutils@^2.0.2:
   version "2.0.3"
   resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64"
@@ -2281,6 +2785,19 @@ exec-sh@^0.3.2:
   resolved "https://registry.yarnpkg.com/exec-sh/-/exec-sh-0.3.4.tgz#3a018ceb526cc6f6df2bb504b2bfe8e3a4934ec5"
   integrity sha512-sEFIkc61v75sWeOe72qyrqg2Qg0OuLESziUDk/O/z2qgS15y2gWVFrI6f2Qn/qw/0/NCfCEsmNA4zOjkwEZT1A==
 
+execa@^0.7.0:
+  version "0.7.0"
+  resolved "https://registry.yarnpkg.com/execa/-/execa-0.7.0.tgz#944becd34cc41ee32a63a9faf27ad5a65fc59777"
+  integrity sha1-lEvs00zEHuMqY6n68nrVpl/Fl3c=
+  dependencies:
+    cross-spawn "^5.0.1"
+    get-stream "^3.0.0"
+    is-stream "^1.1.0"
+    npm-run-path "^2.0.0"
+    p-finally "^1.0.0"
+    signal-exit "^3.0.0"
+    strip-eof "^1.0.0"
+
 execa@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/execa/-/execa-1.0.0.tgz#c6236a5bb4df6d6f15e88e7f017798216749ddd8"
@@ -2294,10 +2811,10 @@ execa@^1.0.0:
     signal-exit "^3.0.0"
     strip-eof "^1.0.0"
 
-execa@^3.2.0, execa@^3.4.0:
-  version "3.4.0"
-  resolved "https://registry.yarnpkg.com/execa/-/execa-3.4.0.tgz#c08ed4550ef65d858fac269ffc8572446f37eb89"
-  integrity sha512-r9vdGQk4bmCuK1yKQu1KTwcT2zwfWdbdaXfCtAh+5nU/4fSX+JAb7vZGvI5naJrQlvONrEB20jeruESI69530g==
+execa@^4.0.0, execa@^4.0.1:
+  version "4.0.3"
+  resolved "https://registry.yarnpkg.com/execa/-/execa-4.0.3.tgz#0a34dabbad6d66100bd6f2c576c8669403f317f2"
+  integrity sha512-WFDXGHckXPWZX19t1kCsXzOpqX9LWYNqn4C+HqZlk/V0imTkzJZqf87ZBhvpHaftERYknpk0fjSylnXVlVgI0A==
   dependencies:
     cross-spawn "^7.0.0"
     get-stream "^5.0.0"
@@ -2306,10 +2823,14 @@ execa@^3.2.0, execa@^3.4.0:
     merge-stream "^2.0.0"
     npm-run-path "^4.0.0"
     onetime "^5.1.0"
-    p-finally "^2.0.0"
     signal-exit "^3.0.2"
     strip-final-newline "^2.0.0"
 
+exenv@^1.2.1:
+  version "1.2.2"
+  resolved "https://registry.yarnpkg.com/exenv/-/exenv-1.2.2.tgz#2ae78e85d9894158670b03d47bec1f03bd91bb9d"
+  integrity sha1-KueOhdmJQVhnCwPUe+wfA72Ru50=
+
 exit@^0.1.2:
   version "0.1.2"
   resolved "https://registry.yarnpkg.com/exit/-/exit-0.1.2.tgz#0632638f8d877cc82107d30a0fff1a17cba1cd0c"
@@ -2342,17 +2863,17 @@ expand-range@^1.8.1:
   dependencies:
     fill-range "^2.1.0"
 
-expect@^25.4.0:
-  version "25.4.0"
-  resolved "https://registry.yarnpkg.com/expect/-/expect-25.4.0.tgz#0b16c17401906d1679d173e59f0d4580b22f8dc8"
-  integrity sha512-7BDIX99BTi12/sNGJXA9KMRcby4iAmu1xccBOhyKCyEhjcVKS3hPmHdA/4nSI9QGIOkUropKqr3vv7WMDM5lvQ==
+expect@^26.1.0:
+  version "26.1.0"
+  resolved "https://registry.yarnpkg.com/expect/-/expect-26.1.0.tgz#8c62e31d0f8d5a8ebb186ee81473d15dd2fbf7c8"
+  integrity sha512-QbH4LZXDsno9AACrN9eM0zfnby9G+OsdNgZUohjg/P0mLy1O+/bzTAJGT6VSIjVCe8yKM6SzEl/ckEOFBT7Vnw==
   dependencies:
-    "@jest/types" "^25.4.0"
+    "@jest/types" "^26.1.0"
     ansi-styles "^4.0.0"
-    jest-get-type "^25.2.6"
-    jest-matcher-utils "^25.4.0"
-    jest-message-util "^25.4.0"
-    jest-regex-util "^25.2.6"
+    jest-get-type "^26.0.0"
+    jest-matcher-utils "^26.1.0"
+    jest-message-util "^26.1.0"
+    jest-regex-util "^26.0.0"
 
 express@^4.14.0:
   version "4.17.1"
@@ -2419,15 +2940,6 @@ external-editor@^2.0.4:
     iconv-lite "^0.4.17"
     tmp "^0.0.33"
 
-external-editor@^3.0.3:
-  version "3.1.0"
-  resolved "https://registry.yarnpkg.com/external-editor/-/external-editor-3.1.0.tgz#cb03f740befae03ea4d283caed2741a83f335495"
-  integrity sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==
-  dependencies:
-    chardet "^0.7.0"
-    iconv-lite "^0.4.24"
-    tmp "^0.0.33"
-
 extglob@^0.3.1:
   version "0.3.2"
   resolved "https://registry.yarnpkg.com/extglob/-/extglob-0.3.2.tgz#2e18ff3d2f49ab2765cec9023f011daa8d8349a1"
@@ -2460,9 +2972,9 @@ extsprintf@^1.2.0:
   integrity sha1-4mifjzVvrWLMplo6kcXfX5VRaS8=
 
 fast-deep-equal@^3.1.1:
-  version "3.1.1"
-  resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.1.tgz#545145077c501491e33b15ec408c294376e94ae4"
-  integrity sha512-8UEa58QDLauDNfpbrX55Q9jrGHThw2ZMdOky5Gl1CDtVeJDPVrG4Jxx1N8jw2gkWaff5UUuX1KJd+9zGe2B+ZA==
+  version "3.1.3"
+  resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525"
+  integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==
 
 fast-diff@^1.1.2:
   version "1.2.0"
@@ -2474,7 +2986,7 @@ fast-json-stable-stringify@2.x, fast-json-stable-stringify@^2.0.0:
   resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633"
   integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==
 
-fast-levenshtein@~2.0.6:
+fast-levenshtein@^2.0.6, fast-levenshtein@~2.0.6:
   version "2.0.6"
   resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917"
   integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=
@@ -2486,13 +2998,10 @@ fb-watchman@^2.0.0:
   dependencies:
     bser "2.1.1"
 
-figures@^1.7.0:
-  version "1.7.0"
-  resolved "https://registry.yarnpkg.com/figures/-/figures-1.7.0.tgz#cbe1e3affcf1cd44b80cadfed28dc793a9701d2e"
-  integrity sha1-y+Hjr/zxzUS4DK3+0o3Hk6lwHS4=
-  dependencies:
-    escape-string-regexp "^1.0.5"
-    object-assign "^4.1.0"
+figgy-pudding@^3.0.0, figgy-pudding@^3.5.1:
+  version "3.5.2"
+  resolved "https://registry.yarnpkg.com/figgy-pudding/-/figgy-pudding-3.5.2.tgz#b4eee8148abb01dcf1d1ac34367d59e12fa61d6e"
+  integrity sha512-0btnI/H8f2pavGMN8w40mlSKOfTK2SVJmBfBeVIj3kNw0swwgzyRq0d5TJVOwodFmtvpPeWPN/MCcfuWF0Ezbw==
 
 figures@^2.0.0:
   version "2.0.0"
@@ -2501,10 +3010,10 @@ figures@^2.0.0:
   dependencies:
     escape-string-regexp "^1.0.5"
 
-figures@^3.0.0:
-  version "3.1.0"
-  resolved "https://registry.yarnpkg.com/figures/-/figures-3.1.0.tgz#4b198dd07d8d71530642864af2d45dd9e459c4ec"
-  integrity sha512-ravh8VRXqHuMvZt/d8GblBeqDMkdJMBdv/2KntFH+ra5MXkO7nxNKpzQ3n6QD/2da1kH0aWmNISdvhM7gl2gVg==
+figures@^3.2.0:
+  version "3.2.0"
+  resolved "https://registry.yarnpkg.com/figures/-/figures-3.2.0.tgz#625c18bd293c604dc4a8ddb2febf0c88341746af"
+  integrity sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==
   dependencies:
     escape-string-regexp "^1.0.5"
 
@@ -2581,6 +3090,11 @@ finalhandler@~1.1.2:
     statuses "~1.5.0"
     unpipe "~1.0.0"
 
+find-npm-prefix@^1.0.2:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/find-npm-prefix/-/find-npm-prefix-1.0.2.tgz#8d8ce2c78b3b4b9e66c8acc6a37c231eb841cfdf"
+  integrity sha512-KEftzJ+H90x6pcKtdXZEPsQse8/y/UnvzRKrOSQFprnrGaFuJ62fVkP34Iu2IYuMvyauCyoLTNkJZgrrGA2wkA==
+
 find-up@^2.0.0, find-up@^2.1.0:
   version "2.1.0"
   resolved "https://registry.yarnpkg.com/find-up/-/find-up-2.1.0.tgz#45d1b7e506c717ddd482775a2b77920a3c0c57a7"
@@ -2588,6 +3102,13 @@ find-up@^2.0.0, find-up@^2.1.0:
   dependencies:
     locate-path "^2.0.0"
 
+find-up@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/find-up/-/find-up-3.0.0.tgz#49169f1d7993430646da61ecc5ae355c21c97b73"
+  integrity sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==
+  dependencies:
+    locate-path "^3.0.0"
+
 find-up@^4.0.0, find-up@^4.1.0:
   version "4.1.0"
   resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19"
@@ -2613,9 +3134,9 @@ flat-cache@^2.0.1:
     write "1.0.3"
 
 flatted@^2.0.0:
-  version "2.0.1"
-  resolved "https://registry.yarnpkg.com/flatted/-/flatted-2.0.1.tgz#69e57caa8f0eacbc281d2e2cb458d46fdb449e08"
-  integrity sha512-a1hQMktqW9Nmqr5aktAux3JMNqaucxGcjtjWnZLHX7yyPCmlSV3M54nGYbqT8K+0GhF3NBgmJCc3ma+WOgX8Jg==
+  version "2.0.2"
+  resolved "https://registry.yarnpkg.com/flatted/-/flatted-2.0.2.tgz#4575b21e2bcee7434aa9be662f4b7b5f9c2b5138"
+  integrity sha512-r5wGx7YeOwNWNlCA0wQ86zKyDLMQr+/RB8xy74M4hTphfmjlijTSSXGuH8rnvKZnfT9i+75zmd8jcKdMR4O6jA==
 
 fliplog@^0.3.13:
   version "0.3.13"
@@ -2624,6 +3145,14 @@ fliplog@^0.3.13:
   dependencies:
     chain-able "^1.0.1"
 
+flush-write-stream@^1.0.0:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/flush-write-stream/-/flush-write-stream-1.1.1.tgz#8dd7d873a1babc207d94ead0c2e0e44276ebf2e8"
+  integrity sha512-3Z4XhFZ3992uIq0XOqb9AreonueSYphE6oYbpt5+3u06JWklbsPkNv3ZKkP9Bz/r+1MWCaMoSQ28P85+1Yc77w==
+  dependencies:
+    inherits "^2.0.3"
+    readable-stream "^2.3.6"
+
 for-in@^1.0.1, for-in@^1.0.2:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80"
@@ -2676,6 +3205,22 @@ fresh@0.5.2:
   resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7"
   integrity sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=
 
+from2@^1.3.0:
+  version "1.3.0"
+  resolved "https://registry.yarnpkg.com/from2/-/from2-1.3.0.tgz#88413baaa5f9a597cfde9221d86986cd3c061dfd"
+  integrity sha1-iEE7qqX5pZfP3pIh2GmGzTwGHf0=
+  dependencies:
+    inherits "~2.0.1"
+    readable-stream "~1.1.10"
+
+from2@^2.1.0:
+  version "2.3.0"
+  resolved "https://registry.yarnpkg.com/from2/-/from2-2.3.0.tgz#8bfb5502bde4a4d36cfdeea007fcca21d7e382af"
+  integrity sha1-i/tVAr3kpNNs/e6gB/zKIdfjgq8=
+  dependencies:
+    inherits "^2.0.1"
+    readable-stream "^2.0.0"
+
 fs-extra@^7.0.0:
   version "7.0.1"
   resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-7.0.1.tgz#4f189c44aa123b895f722804f55ea23eadc348e9"
@@ -2685,15 +3230,41 @@ fs-extra@^7.0.0:
     jsonfile "^4.0.0"
     universalify "^0.1.0"
 
-fs.realpath@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
-  integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8=
+fs-minipass@^1.2.5:
+  version "1.2.7"
+  resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-1.2.7.tgz#ccff8570841e7fe4265693da88936c55aed7f7c7"
+  integrity sha512-GWSSJGFy4e9GUeCcbIkED+bgAoFyj7XF1mV8rma3QW4NIqX9Kyx79N/PF61H5udOV3aY1IaMLs6pGbH71nlCTA==
+  dependencies:
+    minipass "^2.6.0"
+
+fs-vacuum@^1.2.10, fs-vacuum@~1.2.10:
+  version "1.2.10"
+  resolved "https://registry.yarnpkg.com/fs-vacuum/-/fs-vacuum-1.2.10.tgz#b7629bec07a4031a2548fdf99f5ecf1cc8b31e36"
+  integrity sha1-t2Kb7AekAxolSP35n17PHMizHjY=
+  dependencies:
+    graceful-fs "^4.1.2"
+    path-is-inside "^1.0.1"
+    rimraf "^2.5.2"
+
+fs-write-stream-atomic@^1.0.8, fs-write-stream-atomic@~1.0.10:
+  version "1.0.10"
+  resolved "https://registry.yarnpkg.com/fs-write-stream-atomic/-/fs-write-stream-atomic-1.0.10.tgz#b47df53493ef911df75731e70a9ded0189db40c9"
+  integrity sha1-tH31NJPvkR33VzHnCp3tAYnbQMk=
+  dependencies:
+    graceful-fs "^4.1.2"
+    iferr "^0.1.5"
+    imurmurhash "^0.1.4"
+    readable-stream "1 || 2"
+
+fs.realpath@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
+  integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8=
 
 fsevents@^1.0.0:
-  version "1.2.11"
-  resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-1.2.11.tgz#67bf57f4758f02ede88fb2a1712fef4d15358be3"
-  integrity sha512-+ux3lx6peh0BpvY0JebGyZoiR4D+oYzdPZMKJwkZ+sFkNJzpL7tXc/wehS49gUAxg3tmMHPHZkA8JU2rhhgDHw==
+  version "1.2.13"
+  resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-1.2.13.tgz#f325cb0455592428bcf11b383370ef70e3bfcc38"
+  integrity sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw==
   dependencies:
     bindings "^1.5.0"
     nan "^2.12.1"
@@ -2703,6 +3274,16 @@ fsevents@^2.1.2:
   resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.1.3.tgz#fb738703ae8d2f9fe900c33836ddebee8b97f23e"
   integrity sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ==
 
+fstream@^1.0.0, fstream@^1.0.12:
+  version "1.0.12"
+  resolved "https://registry.yarnpkg.com/fstream/-/fstream-1.0.12.tgz#4e8ba8ee2d48be4f7d0de505455548eae5932045"
+  integrity sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg==
+  dependencies:
+    graceful-fs "^4.1.2"
+    inherits "~2.0.0"
+    mkdirp ">=0.5 0"
+    rimraf "2"
+
 function-bind@^1.1.1:
   version "1.1.1"
   resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d"
@@ -2764,11 +3345,52 @@ fuse.js@^3.4.5:
   resolved "https://registry.yarnpkg.com/fuse.js/-/fuse.js-3.6.1.tgz#7de85fdd6e1b3377c23ce010892656385fd9b10c"
   integrity sha512-hT9yh/tiinkmirKrlv4KWOjztdoZo1mx9Qh4KvWqC7isoXwdUY3PNWUxceF4/qO9R6riA2C29jdTOeQOIROjgw==
 
+gauge@~2.7.3:
+  version "2.7.4"
+  resolved "https://registry.yarnpkg.com/gauge/-/gauge-2.7.4.tgz#2c03405c7538c39d7eb37b317022e325fb018bf7"
+  integrity sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=
+  dependencies:
+    aproba "^1.0.3"
+    console-control-strings "^1.0.0"
+    has-unicode "^2.0.0"
+    object-assign "^4.1.0"
+    signal-exit "^3.0.0"
+    string-width "^1.0.1"
+    strip-ansi "^3.0.1"
+    wide-align "^1.1.0"
+
+genfun@^5.0.0:
+  version "5.0.0"
+  resolved "https://registry.yarnpkg.com/genfun/-/genfun-5.0.0.tgz#9dd9710a06900a5c4a5bf57aca5da4e52fe76537"
+  integrity sha512-KGDOARWVga7+rnB3z9Sd2Letx515owfk0hSxHGuqjANb1M+x2bGZGqHLiozPsYMdM2OubeMni/Hpwmjq6qIUhA==
+
 gensync@^1.0.0-beta.1:
   version "1.0.0-beta.1"
   resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.1.tgz#58f4361ff987e5ff6e1e7a210827aa371eaac269"
   integrity sha512-r8EC6NO1sngH/zdD9fiRDLdcgnbayXah+mLgManTaIZJqEC1MZstmnox8KpnI2/fxQwrp5OpCOYWLp4rBl4Jcg==
 
+gentle-fs@^2.0.1, gentle-fs@^2.3.0:
+  version "2.3.1"
+  resolved "https://registry.yarnpkg.com/gentle-fs/-/gentle-fs-2.3.1.tgz#11201bf66c18f930ddca72cf69460bdfa05727b1"
+  integrity sha512-OlwBBwqCFPcjm33rF2BjW+Pr6/ll2741l+xooiwTCeaX2CA1ZuclavyMBe0/KlR21/XGsgY6hzEQZ15BdNa13Q==
+  dependencies:
+    aproba "^1.1.2"
+    chownr "^1.1.2"
+    cmd-shim "^3.0.3"
+    fs-vacuum "^1.2.10"
+    graceful-fs "^4.1.11"
+    iferr "^0.1.5"
+    infer-owner "^1.0.4"
+    mkdirp "^0.5.1"
+    path-is-inside "^1.0.2"
+    read-cmd-shim "^1.0.1"
+    slide "^1.1.6"
+
+get-caller-file@^1.0.1:
+  version "1.0.3"
+  resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-1.0.3.tgz#f978fa4c90d1dfe7ff2d6beda2a515e713bdcf4a"
+  integrity sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w==
+
 get-caller-file@^2.0.1:
   version "2.0.5"
   resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e"
@@ -2779,11 +3401,21 @@ get-own-enumerable-property-symbols@^3.0.0:
   resolved "https://registry.yarnpkg.com/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-3.0.2.tgz#b5fde77f22cbe35f390b4e089922c50bce6ef664"
   integrity sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g==
 
+get-package-type@^0.1.0:
+  version "0.1.0"
+  resolved "https://registry.yarnpkg.com/get-package-type/-/get-package-type-0.1.0.tgz#8de2d803cff44df3bc6c456e6668b36c3926e11a"
+  integrity sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==
+
 get-stdin@^6.0.0:
   version "6.0.0"
   resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-6.0.0.tgz#9e09bf712b360ab9225e812048f71fde9c89657b"
   integrity sha512-jp4tHawyV7+fkkSKyvjuLZswblUtz+SQKzSWnBbii16BuZksJlU1wuBYXY75r+duh/llF1ur6oNwi+2ZzjKZ7g==
 
+get-stream@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-3.0.0.tgz#8e943d1358dc37555054ecbe2edb05aa174ede14"
+  integrity sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=
+
 get-stream@^4.0.0:
   version "4.1.0"
   resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-4.1.0.tgz#c1b255575f3dc21d59bfc79cd3d2b46b1c3a54b5"
@@ -2831,13 +3463,13 @@ glob-parent@^2.0.0:
     is-glob "^2.0.0"
 
 glob-parent@^5.0.0:
-  version "5.1.0"
-  resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.0.tgz#5f4c1d1e748d30cd73ad2944b3577a81b081e8c2"
-  integrity sha512-qjtRgnIVmOfnKUE3NJAQEdk+lKrxfw8t5ke7SXtfMTHcjsBfOfWXCQfdb30zfDoZQ2IRSIiidmjtbHZPZ++Ihw==
+  version "5.1.1"
+  resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.1.tgz#b6c1ef417c4e5663ea498f1c45afac6916bbc229"
+  integrity sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ==
   dependencies:
     is-glob "^4.0.1"
 
-glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6:
+glob@^7.0.3, glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6, glob@~7.1.2:
   version "7.1.6"
   resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6"
   integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==
@@ -2849,22 +3481,51 @@ glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6:
     once "^1.3.0"
     path-is-absolute "^1.0.0"
 
+global-dirs@^0.1.0:
+  version "0.1.1"
+  resolved "https://registry.yarnpkg.com/global-dirs/-/global-dirs-0.1.1.tgz#b319c0dd4607f353f3be9cca4c72fc148c49f445"
+  integrity sha1-sxnA3UYH81PzvpzKTHL8FIxJ9EU=
+  dependencies:
+    ini "^1.3.4"
+
 globals@^11.1.0:
   version "11.12.0"
   resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e"
   integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==
 
 globals@^12.1.0:
-  version "12.3.0"
-  resolved "https://registry.yarnpkg.com/globals/-/globals-12.3.0.tgz#1e564ee5c4dded2ab098b0f88f24702a3c56be13"
-  integrity sha512-wAfjdLgFsPZsklLJvOBUBmzYE8/CwhEqSBEMRXA3qxIiNtyqvjYurAtIfDh6chlEPUfmTY3MnZh5Hfh4q0UlIw==
+  version "12.4.0"
+  resolved "https://registry.yarnpkg.com/globals/-/globals-12.4.0.tgz#a18813576a41b00a24a97e7f815918c2e19925f8"
+  integrity sha512-BWICuzzDvDoH54NHKCseDanAhE3CeDorgDL5MT6LMXXj2WCnd9UC2szdk4AWLfjdgNBCXLUanXYcpBBKOSWGwg==
   dependencies:
     type-fest "^0.8.1"
 
-graceful-fs@^4.1.11, graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.3:
-  version "4.2.3"
-  resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.3.tgz#4a12ff1b60376ef09862c2093edd908328be8423"
-  integrity sha512-a30VEBm4PEdx1dRB7MFK7BejejvCvBronbLjht+sHuGYj8PHs7M/5Z+rt5lw551vZ7yfTCj4Vuyy3mSJytDWRQ==
+got@^6.7.1:
+  version "6.7.1"
+  resolved "https://registry.yarnpkg.com/got/-/got-6.7.1.tgz#240cd05785a9a18e561dc1b44b41c763ef1e8db0"
+  integrity sha1-JAzQV4WpoY5WHcG0S0HHY+8ejbA=
+  dependencies:
+    create-error-class "^3.0.0"
+    duplexer3 "^0.1.4"
+    get-stream "^3.0.0"
+    is-redirect "^1.0.0"
+    is-retry-allowed "^1.0.0"
+    is-stream "^1.0.0"
+    lowercase-keys "^1.0.0"
+    safe-buffer "^5.0.1"
+    timed-out "^4.0.0"
+    unzip-response "^2.0.1"
+    url-parse-lax "^1.0.0"
+
+graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.4:
+  version "4.2.4"
+  resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.4.tgz#2256bde14d3632958c465ebc96dc467ca07a29fb"
+  integrity sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==
+
+graceful-fs@~4.1.11:
+  version "4.1.15"
+  resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.15.tgz#ffb703e1066e8a0eeaa4c8b80ba9253eeefbfb00"
+  integrity sha512-6uHUhOPEBgQ24HM+r6b/QwWfZq+yiFcipKFrOFiBEnWdy5sdzYoi+pJeQaPI5qOLRFqWmAXUPQNsielzdLoecA==
 
 growly@^1.3.0:
   version "1.3.0"
@@ -2876,7 +3537,7 @@ har-schema@^2.0.0:
   resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92"
   integrity sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=
 
-har-validator@~5.1.0, har-validator@~5.1.3:
+har-validator@~5.1.3:
   version "5.1.3"
   resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-5.1.3.tgz#1ef89ebd3e4996557675eed9893110dc350fa080"
   integrity sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==
@@ -2884,13 +3545,6 @@ har-validator@~5.1.0, har-validator@~5.1.3:
     ajv "^6.5.5"
     har-schema "^2.0.0"
 
-has-ansi@^2.0.0:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-2.0.0.tgz#34f5049ce1ecdf2b0649af3ef24e45ed35416d91"
-  integrity sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=
-  dependencies:
-    ansi-regex "^2.0.0"
-
 has-flag@^3.0.0:
   version "3.0.0"
   resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd"
@@ -2906,6 +3560,11 @@ has-symbols@^1.0.0, has-symbols@^1.0.1:
   resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.1.tgz#9f5214758a44196c406d9bd76cebf81ec2dd31e8"
   integrity sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg==
 
+has-unicode@^2.0.0, has-unicode@~2.0.1:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9"
+  integrity sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=
+
 has-value@^0.3.1:
   version "0.3.1"
   resolved "https://registry.yarnpkg.com/has-value/-/has-value-0.3.1.tgz#7b1f58bada62ca827ec0a2078025654845995e1f"
@@ -2961,17 +3620,17 @@ hoist-non-inferno-statics@^1.1.3:
   resolved "https://registry.yarnpkg.com/hoist-non-inferno-statics/-/hoist-non-inferno-statics-1.1.3.tgz#7d870f4160bfb6a59269b45c343c027f0e30ab35"
   integrity sha1-fYcPQWC/tqWSabRcNDwCfw4wqzU=
 
-hosted-git-info@^2.1.4:
-  version "2.8.5"
-  resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.5.tgz#759cfcf2c4d156ade59b0b2dfabddc42a6b9c70c"
-  integrity sha512-kssjab8CvdXfcXMXVcvsXum4Hwdq9XGtRD3TteMEvEbq0LXyiNQr6AprqKqfeaDXze7SxWvRxdpwE6ku7ikLkg==
+hosted-git-info@^2.1.4, hosted-git-info@^2.6.0, hosted-git-info@^2.7.1:
+  version "2.8.8"
+  resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.8.tgz#7539bd4bc1e0e0a895815a2e0262420b12858488"
+  integrity sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg==
 
-html-encoding-sniffer@^1.0.2:
-  version "1.0.2"
-  resolved "https://registry.yarnpkg.com/html-encoding-sniffer/-/html-encoding-sniffer-1.0.2.tgz#e70d84b94da53aa375e11fe3a351be6642ca46f8"
-  integrity sha512-71lZziiDnsuabfdYiUeWdCVyKuqwWi23L8YeIgV9jSSZHCtb6wB1BKWooH7L3tn4/FuZJMVWyNaIDr4RGmaSYw==
+html-encoding-sniffer@^2.0.1:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/html-encoding-sniffer/-/html-encoding-sniffer-2.0.1.tgz#42a6dc4fd33f00281176e8b23759ca4e4fa185f3"
+  integrity sha512-D5JbOMBIR/TVZkubHT+OyT2705QvogUW4IBn6nHd756OwieSF9aDYFj4dv6HHEVGYbHaLETa3WggZYWWMyy3ZQ==
   dependencies:
-    whatwg-encoding "^1.0.1"
+    whatwg-encoding "^1.0.5"
 
 html-escaper@^2.0.0:
   version "2.0.2"
@@ -2985,6 +3644,11 @@ html-parse-stringify2@^2.0.1:
   dependencies:
     void-elements "^2.0.1"
 
+http-cache-semantics@^3.8.0, http-cache-semantics@^3.8.1:
+  version "3.8.1"
+  resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-3.8.1.tgz#39b0e16add9b605bf0a9ef3d9daaf4843b4cacd2"
+  integrity sha512-5ai2iksyV8ZXmnZhHH4rWPoxxistEexSi5936zIQ1bnNTW5VnA85B6P/VpXiRM017IgRvb2kKo1a//y+0wSp3w==
+
 http-errors@1.7.2:
   version "1.7.2"
   resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.2.tgz#4f5029cf13239f31036e5b2e55292bcfbcc85c8f"
@@ -3007,6 +3671,14 @@ http-errors@~1.7.2:
     statuses ">= 1.5.0 < 2"
     toidentifier "1.0.0"
 
+http-proxy-agent@^2.0.0, http-proxy-agent@^2.1.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-2.1.0.tgz#e4821beef5b2142a2026bd73926fe537631c5405"
+  integrity sha512-qwHbBLV7WviBl0rQsOzH6o5lwyOIvwp/BdFnvVxXORldu5TmjFfjzBcWUWS5kWAZhmv+JtiDhSuQCp4sBfbIgg==
+  dependencies:
+    agent-base "4"
+    debug "3.1.0"
+
 http-signature@~1.2.0:
   version "1.2.0"
   resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1"
@@ -3016,11 +3688,26 @@ http-signature@~1.2.0:
     jsprim "^1.2.2"
     sshpk "^1.7.0"
 
+https-proxy-agent@^2.1.0, https-proxy-agent@^2.2.0, https-proxy-agent@^2.2.1:
+  version "2.2.4"
+  resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-2.2.4.tgz#4ee7a737abd92678a293d9b34a1af4d0d08c787b"
+  integrity sha512-OmvfoQ53WLjtA9HeYP9RNrWMJzzAz1JGaSFr1nijg0PVR1JaD/xbJq1mdEIIlxGpXp9eSe/O2LgU9DJmTPd0Eg==
+  dependencies:
+    agent-base "^4.3.0"
+    debug "^3.1.0"
+
 human-signals@^1.1.1:
   version "1.1.1"
   resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-1.1.1.tgz#c5b1cd14f50aeae09ab6c59fe63ba3395fe4dfa3"
   integrity sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==
 
+humanize-ms@^1.2.1:
+  version "1.2.1"
+  resolved "https://registry.yarnpkg.com/humanize-ms/-/humanize-ms-1.2.1.tgz#c46e3159a293f6b896da29316d8b6fe8bb79bbed"
+  integrity sha1-xG4xWaKT9riW2ikxbYtv6Lt5u+0=
+  dependencies:
+    ms "^2.0.0"
+
 husky@^4.2.5:
   version "4.2.5"
   resolved "https://registry.yarnpkg.com/husky/-/husky-4.2.5.tgz#2b4f7622673a71579f901d9885ed448394b5fa36"
@@ -3038,33 +3725,52 @@ husky@^4.2.5:
     which-pm-runs "^1.0.0"
 
 i18next@^19.4.1:
-  version "19.4.1"
-  resolved "https://registry.yarnpkg.com/i18next/-/i18next-19.4.1.tgz#4929d15d3d01e4712350a368d005cefa50ff5455"
-  integrity sha512-dC3ue15jkLebN2je4xEjfjVYd/fSAo+UVK9f+JxvceCJRowkI+S0lGohgKejqU+FYLfvw9IAPylIIEWwR8Djrg==
+  version "19.6.3"
+  resolved "https://registry.yarnpkg.com/i18next/-/i18next-19.6.3.tgz#ce2346161b35c4c5ab691b0674119c7b349c0817"
+  integrity sha512-eYr98kw/C5z6kY21ti745p4IvbOJwY8F2T9tf/Lvy5lFnYRqE45+bppSgMPmcZZqYNT+xO0N0x6rexVR2wtZZQ==
   dependencies:
-    "@babel/runtime" "^7.3.1"
+    "@babel/runtime" "^7.10.1"
 
-iconv-lite@0.4.24, iconv-lite@^0.4.17, iconv-lite@^0.4.24:
+iconv-lite@0.4.24, iconv-lite@^0.4.17:
   version "0.4.24"
   resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b"
   integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==
   dependencies:
     safer-buffer ">= 2.1.2 < 3"
 
+iconv-lite@^0.6.2:
+  version "0.6.2"
+  resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.2.tgz#ce13d1875b0c3a674bd6a04b7f76b01b1b6ded01"
+  integrity sha512-2y91h5OpQlolefMPmUlivelittSWy0rP+oYVpn6A7GwVHNE8AWzoYOBNmlwks3LobaJxgHCYZAnyNo2GgpNRNQ==
+  dependencies:
+    safer-buffer ">= 2.1.2 < 3.0.0"
+
 ieee754@^1.1.8:
   version "1.1.13"
   resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.13.tgz#ec168558e95aa181fd87d37f55c32bbcb6708b84"
   integrity sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==
 
+iferr@^0.1.5, iferr@~0.1.5:
+  version "0.1.5"
+  resolved "https://registry.yarnpkg.com/iferr/-/iferr-0.1.5.tgz#c60eed69e6d8fdb6b3104a1fcbca1c192dc5b501"
+  integrity sha1-xg7taebY/bazEEofy8ocGS3FtQE=
+
+ignore-walk@^3.0.1:
+  version "3.0.3"
+  resolved "https://registry.yarnpkg.com/ignore-walk/-/ignore-walk-3.0.3.tgz#017e2447184bfeade7c238e4aefdd1e8f95b1e37"
+  integrity sha512-m7o6xuOaT1aqheYHKf8W6J5pYH85ZI9w077erOzLje3JsB1gkafkAhHHY19dqjulgIZHFm32Cp5uNZgcQqdJKw==
+  dependencies:
+    minimatch "^3.0.4"
+
 ignore@^4.0.6:
   version "4.0.6"
   resolved "https://registry.yarnpkg.com/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc"
   integrity sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==
 
 ignore@^5.1.1:
-  version "5.1.4"
-  resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.1.4.tgz#84b7b3dbe64552b6ef0eca99f6743dbec6d97adf"
-  integrity sha512-MzbUSahkTW1u7JpKKjY7LCARd1fU5W2rLdxlM4kdkayuCwZImjkpluF9CM1aLewYJguPDqewLam18Y6AU69A8A==
+  version "5.1.8"
+  resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.1.8.tgz#f150a8b50a34289b33e22f5889abd4d8016f0e57"
+  integrity sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw==
 
 import-fresh@^3.0.0, import-fresh@^3.1.0:
   version "3.2.1"
@@ -3074,6 +3780,11 @@ import-fresh@^3.0.0, import-fresh@^3.1.0:
     parent-module "^1.0.0"
     resolve-from "^4.0.0"
 
+import-lazy@^2.1.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/import-lazy/-/import-lazy-2.1.0.tgz#05698e3d45c88e8d7e9d92cb0584e77f096f3e43"
+  integrity sha1-BWmOPUXIjo1+nZLLBYTnfwlvPkM=
+
 import-local@^3.0.2:
   version "3.0.2"
   resolved "https://registry.yarnpkg.com/import-local/-/import-local-3.0.2.tgz#a8cfd0431d1de4a2199703d003e3e62364fa6db6"
@@ -3092,35 +3803,49 @@ imurmurhash@^0.1.4:
   resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea"
   integrity sha1-khi5srkoojixPcT7a21XbyMUU+o=
 
-indent-string@^3.0.0:
-  version "3.2.0"
-  resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-3.2.0.tgz#4a5fd6d27cc332f37e5419a504dbb837105c9289"
-  integrity sha1-Sl/W0nzDMvN+VBmlBNu4NxBckok=
+indent-string@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-4.0.0.tgz#624f8f4497d619b2d9768531d58f4122854d7251"
+  integrity sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==
+
+infer-owner@^1.0.4:
+  version "1.0.4"
+  resolved "https://registry.yarnpkg.com/infer-owner/-/infer-owner-1.0.4.tgz#c4cefcaa8e51051c2a40ba2ce8a3d27295af9467"
+  integrity sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==
+
+inferno-clone-vnode@^7.4.2:
+  version "7.4.2"
+  resolved "https://registry.yarnpkg.com/inferno-clone-vnode/-/inferno-clone-vnode-7.4.2.tgz#071577098fd8ffdffd41cf81819207effa520bc1"
+  integrity sha512-pX5agEWfU+w6vYaVyKtzgFT4jlMK+eOEKL/LkyWu2dtOy0DXx174AFm6GSgOt66W6xmxn58sbWI7ngsWmp4f2w==
+  dependencies:
+    inferno "7.4.2"
 
-inferno-clone-vnode@^7.1.12:
-  version "7.4.0"
-  resolved "https://registry.yarnpkg.com/inferno-clone-vnode/-/inferno-clone-vnode-7.4.0.tgz#44a930ef0881f79d425c1c7f4bbd206513da905a"
-  integrity sha512-rPp4tMhWZB1H2kx0MqgyPPBP4bWIXwkH+E/eNSWWtXLR5mKDGz19cguiBkR+U1uXQCi4/AkWvOVHxLQCfT/5Zw==
+inferno-create-element@^7.4.2:
+  version "7.4.2"
+  resolved "https://registry.yarnpkg.com/inferno-create-element/-/inferno-create-element-7.4.2.tgz#d3ac6c64c792f8d3c4784279825418ebd73cfe1c"
+  integrity sha512-FHNca1NR/9SRtaifr4DdAuA6dKBeWkCrYVVkTazMadJyjMtpPN3d+sKlkmdvorAjhUvdMMNcRQStrgRTpLcZyg==
   dependencies:
-    inferno "7.4.0"
+    inferno "7.4.2"
 
-inferno-create-element@^7.1.12:
-  version "7.4.0"
-  resolved "https://registry.yarnpkg.com/inferno-create-element/-/inferno-create-element-7.4.0.tgz#b431f293cdb8931f7f3604e0774500b66d6fe5c8"
-  integrity sha512-gxwU899obmELIxfhWzyHBIGbxOXUPfB1SzW+K3XGU0exWKCVIJwSpBOGpJY5tlKf4lyg1UrCmfz2JZS1i2U2vg==
+inferno-helmet@^5.2.1:
+  version "5.2.1"
+  resolved "https://registry.yarnpkg.com/inferno-helmet/-/inferno-helmet-5.2.1.tgz#3717f325760aa14abeae82a78af7213f9055a3dc"
+  integrity sha512-9xzUGENVoz8qk67s0UhHlGNGZKG9Ia0mk5KoCNgkkIcGNhk7mNIINm7jJ5OOigVetz2DwI94jHzouTggb49AJg==
   dependencies:
-    inferno "7.4.0"
+    deep-equal "^1.0.1"
+    inferno-side-effect "^1.1.5"
+    object-assign "^4.1.1"
 
-inferno-i18next@nimbusec-oss/inferno-i18next:
-  version "7.1.12"
-  resolved "https://codeload.github.com/nimbusec-oss/inferno-i18next/tar.gz/f8c1403e60be70141c558e36f12f22c106cb7463"
+"inferno-i18next@github:nimbusec-oss/inferno-i18next#semver:^7.4.2":
+  version "7.4.2"
+  resolved "https://codeload.github.com/nimbusec-oss/inferno-i18next/tar.gz/54b9be591ccd62c53799ad23e35f17144a62f909"
   dependencies:
     html-parse-stringify2 "^2.0.1"
-    inferno "^7.1.12"
-    inferno-clone-vnode "^7.1.12"
-    inferno-create-element "^7.1.12"
-    inferno-shared "^7.1.12"
-    inferno-vnode-flags "^7.1.12"
+    inferno "^7.4.2"
+    inferno-clone-vnode "^7.4.2"
+    inferno-create-element "^7.4.2"
+    inferno-shared "^7.4.2"
+    inferno-vnode-flags "^7.4.2"
 
 inferno-router@^7.4.2:
   version "7.4.2"
@@ -3132,35 +3857,25 @@ inferno-router@^7.4.2:
     inferno "7.4.2"
     path-to-regexp-es6 "1.7.0"
 
-inferno-shared@7.4.0, inferno-shared@^7.1.12:
-  version "7.4.0"
-  resolved "https://registry.yarnpkg.com/inferno-shared/-/inferno-shared-7.4.0.tgz#4491deb75348019939b160cd5655196afa13ced0"
-  integrity sha512-6aa1fC/e4SP2lOLNg4ZS5Zz2SC+DnM7WxQbggmHhLSyOqZrsPrpZSlX25LbjR9lkhMrq6cmki3yInYFGuDzlRg==
-
-inferno-shared@7.4.2:
+inferno-shared@7.4.2, inferno-shared@^7.4.2:
   version "7.4.2"
   resolved "https://registry.yarnpkg.com/inferno-shared/-/inferno-shared-7.4.2.tgz#400cf6d19a077af9e5e852e1f189726391efa273"
   integrity sha512-SULfgzj/PuyMd3rHThEXkeEdZorjcr/q+kaqbwr7C2XhIPCk4e5jcRKZujI6YCSahGA9WFwP2Mft1v5PsaaNeg==
 
-inferno-vnode-flags@7.4.0, inferno-vnode-flags@^7.1.12:
-  version "7.4.0"
-  resolved "https://registry.yarnpkg.com/inferno-vnode-flags/-/inferno-vnode-flags-7.4.0.tgz#5c049a73f3ff84a51458b06d279d6b18d09acdf0"
-  integrity sha512-TMPrvAxR2uUVSowLKnGgH34eWXErIYCdJ4d5hj8cSc8ta8knN6dj0z47UIw13qvmWfNjHgwm0C2/cm+G6fckiA==
+inferno-side-effect@^1.1.5:
+  version "1.1.5"
+  resolved "https://registry.yarnpkg.com/inferno-side-effect/-/inferno-side-effect-1.1.5.tgz#a874c80dbc73602aafc1e0f3f3f1ec216a916271"
+  integrity sha512-Q2O2qExGjBTPRfwM1suQPBN5FBHhANccCcEvz/3Dr7VcMFelqxnE0qjnlXVQ248S409nA6VtpiBwT7xBz4WyqA==
+  dependencies:
+    exenv "^1.2.1"
+    npm "^5.8.0"
+    shallowequal "^1.0.1"
 
-inferno-vnode-flags@7.4.2:
+inferno-vnode-flags@7.4.2, inferno-vnode-flags@^7.4.2:
   version "7.4.2"
   resolved "https://registry.yarnpkg.com/inferno-vnode-flags/-/inferno-vnode-flags-7.4.2.tgz#54982dabe34f308853ba17de7de4241e23769135"
   integrity sha512-sV4KqqvZH4MW9/dNbC9blHInnpQSqMWouU5VlanbJ+NhJ8ufPwsDy0/+jiA2aODpg2HFHwVMJFF1fsewPqNtLQ==
 
-inferno@7.4.0, inferno@^7.1.12:
-  version "7.4.0"
-  resolved "https://registry.yarnpkg.com/inferno/-/inferno-7.4.0.tgz#8d3dc03562c6851043a1a467fd509f222e9dbf85"
-  integrity sha512-oEXx5iQmGXOvAPj1TZyCo6ndOc4qPg9zBLigMpkApAiV1SM/bri0M1eA/kD3e9jptcof9TwLBJD9bL6E6tq2tg==
-  dependencies:
-    inferno-shared "7.4.0"
-    inferno-vnode-flags "7.4.0"
-    opencollective-postinstall "^2.0.2"
-
 inferno@7.4.2, inferno@^7.4.2:
   version "7.4.2"
   resolved "https://registry.yarnpkg.com/inferno/-/inferno-7.4.2.tgz#833cc423ee7b939fad705c59ea41924f31c92453"
@@ -3170,7 +3885,7 @@ inferno@7.4.2, inferno@^7.4.2:
     inferno-vnode-flags "7.4.2"
     opencollective-postinstall "^2.0.2"
 
-inflight@^1.0.4:
+inflight@^1.0.4, inflight@~1.0.6:
   version "1.0.6"
   resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9"
   integrity sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=
@@ -3178,7 +3893,7 @@ inflight@^1.0.4:
     once "^1.3.0"
     wrappy "1"
 
-inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@~2.0.1, inherits@~2.0.3:
+inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.0, inherits@~2.0.1, inherits@~2.0.3:
   version "2.0.4"
   resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
   integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
@@ -3188,6 +3903,25 @@ inherits@2.0.3:
   resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de"
   integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=
 
+ini@^1.3.4, ini@^1.3.5, ini@~1.3.0:
+  version "1.3.5"
+  resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927"
+  integrity sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==
+
+init-package-json@^1.10.3:
+  version "1.10.3"
+  resolved "https://registry.yarnpkg.com/init-package-json/-/init-package-json-1.10.3.tgz#45ffe2f610a8ca134f2bd1db5637b235070f6cbe"
+  integrity sha512-zKSiXKhQveNteyhcj1CoOP8tqp1QuxPIPBl8Bid99DGLFqA1p87M6lNgfjJHSBoWJJlidGOv5rWjyYKEB3g2Jw==
+  dependencies:
+    glob "^7.1.1"
+    npm-package-arg "^4.0.0 || ^5.0.0 || ^6.0.0"
+    promzard "^0.3.0"
+    read "~1.0.1"
+    read-package-json "1 || 2"
+    semver "2.x || 3.x || 4 || 5"
+    validate-npm-package-license "^3.0.1"
+    validate-npm-package-name "^3.0.0"
+
 inquirer@^3.0.6:
   version "3.3.0"
   resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-3.3.0.tgz#9dd2f2ad765dcab1ff0443b491442a20ba227dc9"
@@ -3208,25 +3942,6 @@ inquirer@^3.0.6:
     strip-ansi "^4.0.0"
     through "^2.3.6"
 
-inquirer@^7.0.0:
-  version "7.0.4"
-  resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-7.0.4.tgz#99af5bde47153abca23f5c7fc30db247f39da703"
-  integrity sha512-Bu5Td5+j11sCkqfqmUTiwv+tWisMtP0L7Q8WrqA2C/BbBhy1YTdFrvjjlrKq8oagA/tLQBski2Gcx/Sqyi2qSQ==
-  dependencies:
-    ansi-escapes "^4.2.1"
-    chalk "^2.4.2"
-    cli-cursor "^3.1.0"
-    cli-width "^2.0.0"
-    external-editor "^3.0.3"
-    figures "^3.0.0"
-    lodash "^4.17.15"
-    mute-stream "0.0.8"
-    run-async "^2.2.0"
-    rxjs "^6.5.3"
-    string-width "^4.1.0"
-    strip-ansi "^5.1.0"
-    through "^2.3.6"
-
 internal-slot@^1.0.2:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.0.2.tgz#9c2e9fb3cd8e5e4256c6f45fe310067fcfa378a3"
@@ -3236,15 +3951,25 @@ internal-slot@^1.0.2:
     has "^1.0.3"
     side-channel "^1.0.2"
 
+invert-kv@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/invert-kv/-/invert-kv-1.0.0.tgz#104a8e4aaca6d3d8cd157a8ef8bfab2d7a3ffdb6"
+  integrity sha1-EEqOSqym09jNFXqO+L+rLXo//bY=
+
 ip-regex@^2.1.0:
   version "2.1.0"
   resolved "https://registry.yarnpkg.com/ip-regex/-/ip-regex-2.1.0.tgz#fa78bf5d2e6913c911ce9f819ee5146bb6d844e9"
   integrity sha1-+ni/XS5pE8kRzp+BnuUUa7bYROk=
 
-ipaddr.js@1.9.0:
-  version "1.9.0"
-  resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.0.tgz#37df74e430a0e47550fe54a2defe30d8acd95f65"
-  integrity sha512-M4Sjn6N/+O6/IXSJseKqHoFc+5FdGJ22sXqnjTpdZweHK64MzEPAyQZyEU3R/KRv2GLoa7nNtg/C2Ev6m7z+eA==
+ip@1.1.5, ip@^1.1.4:
+  version "1.1.5"
+  resolved "https://registry.yarnpkg.com/ip/-/ip-1.1.5.tgz#bdded70114290828c0a039e72ef25f5aaec4354a"
+  integrity sha1-vd7XARQpCCjAoDnnLvJfWq7ENUo=
+
+ipaddr.js@1.9.1:
+  version "1.9.1"
+  resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3"
+  integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==
 
 is-accessor-descriptor@^0.1.6:
   version "0.1.6"
@@ -3260,6 +3985,11 @@ is-accessor-descriptor@^1.0.0:
   dependencies:
     kind-of "^6.0.0"
 
+is-arguments@^1.0.4:
+  version "1.0.4"
+  resolved "https://registry.yarnpkg.com/is-arguments/-/is-arguments-1.0.4.tgz#3faf966c7cba0ff437fb31f6250082fcf0448cf3"
+  integrity sha512-xPh0Rmt8NE65sNzvyUmWgI1tz3mKq74lGA0mL8LYZcoIzKOzDh6HmrYm3d18k60nHerC8A9Km8kYu87zfSFnLA==
+
 is-arrayish@^0.2.1:
   version "0.2.1"
   resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d"
@@ -3277,10 +4007,24 @@ is-buffer@^1.1.5:
   resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be"
   integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==
 
-is-callable@^1.1.4, is-callable@^1.1.5:
-  version "1.1.5"
-  resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.1.5.tgz#f7e46b596890456db74e7f6e976cb3273d06faab"
-  integrity sha512-ESKv5sMCJB2jnHTWZ3O5itG+O128Hsus4K4Qh1h2/cgn2vbgnLSVqfV46AeJA9D5EeeLa9w81KUXMtn34zhX+Q==
+is-builtin-module@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/is-builtin-module/-/is-builtin-module-1.0.0.tgz#540572d34f7ac3119f8f76c30cbc1b1e037affbe"
+  integrity sha1-VAVy0096wxGfj3bDDLwbHgN6/74=
+  dependencies:
+    builtin-modules "^1.0.0"
+
+is-callable@^1.1.4, is-callable@^1.2.0:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.0.tgz#83336560b54a38e35e3a2df7afd0454d691468bb"
+  integrity sha512-pyVD9AaGLxtg6srb2Ng6ynWJqkHU9bEM087AKck0w8QwDarTfNcpIYoU8x8Hv2Icm8u6kFJM18Dag8lyqGkviw==
+
+is-ci@^1.0.10:
+  version "1.2.1"
+  resolved "https://registry.yarnpkg.com/is-ci/-/is-ci-1.2.1.tgz#e3779c8ee17fccf428488f6e281187f2e632841c"
+  integrity sha512-s6tfsaQaQi3JNciBH6shVqEDvhGut0SUXr31ag8Pd8BBbVVlcGfWhpPmEOoM6RJ5TFhbypvf5yyRw/VXW1IiWg==
+  dependencies:
+    ci-info "^1.5.0"
 
 is-ci@^2.0.0:
   version "2.0.0"
@@ -3289,6 +4033,13 @@ is-ci@^2.0.0:
   dependencies:
     ci-info "^2.0.0"
 
+is-cidr@~1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/is-cidr/-/is-cidr-1.0.0.tgz#fb5aacf659255310359da32cae03e40c6a1c2afc"
+  integrity sha1-+1qs9lklUxA1naMsrgPkDGocKvw=
+  dependencies:
+    cidr-regex "1.0.6"
+
 is-data-descriptor@^0.1.4:
   version "0.1.4"
   resolved "https://registry.yarnpkg.com/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz#0b5ee648388e2c860282e793f1856fec3f301b56"
@@ -3326,6 +4077,11 @@ is-descriptor@^1.0.0, is-descriptor@^1.0.2:
     is-data-descriptor "^1.0.0"
     kind-of "^6.0.2"
 
+is-docker@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/is-docker/-/is-docker-2.0.0.tgz#2cb0df0e75e2d064fe1864c37cdeacb7b2dcf25b"
+  integrity sha512-pJEdRugimx4fBMra5z2/5iRdZ63OhYV0vr0Dwm5+xtW4D1FvRkB8hamMIhnWfyJeDdyr/aa7BDyNbtG38VxgoQ==
+
 is-dotfile@^1.0.0:
   version "1.0.3"
   resolved "https://registry.yarnpkg.com/is-dotfile/-/is-dotfile-1.0.3.tgz#a6a2f32ffd2dfb04f5ca25ecd0f6b83cf798a1e1"
@@ -3396,6 +4152,19 @@ is-glob@^4.0.0, is-glob@^4.0.1:
   dependencies:
     is-extglob "^2.1.1"
 
+is-installed-globally@^0.1.0:
+  version "0.1.0"
+  resolved "https://registry.yarnpkg.com/is-installed-globally/-/is-installed-globally-0.1.0.tgz#0dfd98f5a9111716dd535dda6492f67bf3d25a80"
+  integrity sha1-Df2Y9akRFxbdU13aZJL2e/PSWoA=
+  dependencies:
+    global-dirs "^0.1.0"
+    is-path-inside "^1.0.0"
+
+is-npm@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/is-npm/-/is-npm-1.0.0.tgz#f2fb63a65e4905b406c86072765a1a4dc793b9f4"
+  integrity sha1-8vtjpl5JBbQGyGBydloaTceTufQ=
+
 is-number@^2.0.2, is-number@^2.1.0:
   version "2.1.0"
   resolved "https://registry.yarnpkg.com/is-number/-/is-number-2.1.0.tgz#01fcbbb393463a548f2f466cce16dece49db908f"
@@ -3420,17 +4189,17 @@ is-number@^7.0.0:
   resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b"
   integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==
 
-is-obj@^1.0.1:
+is-obj@^1.0.0, is-obj@^1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/is-obj/-/is-obj-1.0.1.tgz#3e4729ac1f5fde025cd7d83a896dab9f4f67db0f"
   integrity sha1-PkcprB9f3gJc19g6iW2rn09n2w8=
 
-is-observable@^1.1.0:
-  version "1.1.0"
-  resolved "https://registry.yarnpkg.com/is-observable/-/is-observable-1.1.0.tgz#b3e986c8f44de950867cab5403f5a3465005975e"
-  integrity sha512-NqCa4Sa2d+u7BWc6CukaObG3Fh+CU9bvixbpcXYhy2VvYS7vVGIdAgnIS5Ks3A/cqk4rebLJ9s8zBstT2aKnIA==
+is-path-inside@^1.0.0:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-1.0.1.tgz#8ef5b7de50437a3fdca6b4e865ef7aa55cb48036"
+  integrity sha1-jvW33lBDej/cprToZe96pVy0gDY=
   dependencies:
-    symbol-observable "^1.1.0"
+    path-is-inside "^1.0.1"
 
 is-plain-object@^2.0.3, is-plain-object@^2.0.4:
   version "2.0.4"
@@ -3444,29 +4213,39 @@ is-posix-bracket@^0.1.0:
   resolved "https://registry.yarnpkg.com/is-posix-bracket/-/is-posix-bracket-0.1.1.tgz#3334dc79774368e92f016e6fbc0a88f5cd6e6bc4"
   integrity sha1-MzTceXdDaOkvAW5vvAqI9c1ua8Q=
 
+is-potential-custom-element-name@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.0.tgz#0c52e54bcca391bb2c494b21e8626d7336c6e397"
+  integrity sha1-DFLlS8yjkbssSUsh6GJtczbG45c=
+
 is-primitive@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/is-primitive/-/is-primitive-2.0.0.tgz#207bab91638499c07b2adf240a41a87210034575"
   integrity sha1-IHurkWOEmcB7Kt8kCkGochADRXU=
 
-is-promise@^2.1.0:
-  version "2.1.0"
-  resolved "https://registry.yarnpkg.com/is-promise/-/is-promise-2.1.0.tgz#79a2a9ece7f096e80f36d2b2f3bc16c1ff4bf3fa"
-  integrity sha1-eaKp7OfwlugPNtKy87wWwf9L8/o=
+is-redirect@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/is-redirect/-/is-redirect-1.0.0.tgz#1d03dded53bd8db0f30c26e4f95d36fc7c87dc24"
+  integrity sha1-HQPd7VO9jbDzDCbk+V02/HyH3CQ=
 
-is-regex@^1.0.5:
-  version "1.0.5"
-  resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.0.5.tgz#39d589a358bf18967f726967120b8fc1aed74eae"
-  integrity sha512-vlKW17SNq44owv5AQR3Cq0bQPEb8+kF3UKZ2fiZNOWtztYE5i0CzCZxFDwO58qAOWtxdBRVO/V5Qin1wjCqFYQ==
+is-regex@^1.0.4, is-regex@^1.1.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.0.tgz#ece38e389e490df0dc21caea2bd596f987f767ff"
+  integrity sha512-iI97M8KTWID2la5uYXlkbSDQIg4F6o1sYboZKKTDpnDQMLtUL86zxhgDet3Q2SriaYsyGqZ6Mn2SjbRKeLHdqw==
   dependencies:
-    has "^1.0.3"
+    has-symbols "^1.0.1"
 
 is-regexp@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/is-regexp/-/is-regexp-1.0.0.tgz#fd2d883545c46bac5a633e7b9a09e87fa2cb5069"
   integrity sha1-/S2INUXEa6xaYz57mgnof6LLUGk=
 
-is-stream@^1.1.0:
+is-retry-allowed@^1.0.0:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/is-retry-allowed/-/is-retry-allowed-1.2.0.tgz#d778488bd0a4666a3be8a1482b9f2baafedea8b4"
+  integrity sha512-RUbUeKwvm3XG2VYamhJL1xFktgjvPzL0Hq8C+6yrWIswDy3BIXGqCxhxkc30N9jqK311gVU137K8Ei55/zVJRg==
+
+is-stream@^1.0.0, is-stream@^1.1.0:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44"
   integrity sha1-EtSj3U5o4Lec6428hBc66A2RykQ=
@@ -3498,10 +4277,12 @@ is-windows@^1.0.2:
   resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d"
   integrity sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==
 
-is-wsl@^2.1.1:
-  version "2.1.1"
-  resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-2.1.1.tgz#4a1c152d429df3d441669498e2486d3596ebaf1d"
-  integrity sha512-umZHcSrwlDHo2TGMXv0DZ8dIUGunZ2Iv68YZnrmCiBPkZ4aaOhtv7pXJKeki9k3qJ3RJr0cDyitcl5wEH3AYog==
+is-wsl@^2.2.0:
+  version "2.2.0"
+  resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-2.2.0.tgz#74a4c76e77ca9fd3f932f290c17ea326cd157271"
+  integrity sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==
+  dependencies:
+    is-docker "^2.0.0"
 
 isarray@0.0.1:
   version "0.0.1"
@@ -3540,15 +4321,12 @@ istanbul-lib-coverage@^3.0.0:
   resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-3.0.0.tgz#f5944a37c70b550b02a78a5c3b2055b280cec8ec"
   integrity sha512-UiUIqxMgRDET6eR+o5HbfRYP1l0hqkWOs7vNxC/mggutCMUIhWMm8gAHb8tHlyfD3/l6rlgNA5cKdDzEAf6hEg==
 
-istanbul-lib-instrument@^4.0.0:
-  version "4.0.1"
-  resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-4.0.1.tgz#61f13ac2c96cfefb076fe7131156cc05907874e6"
-  integrity sha512-imIchxnodll7pvQBYOqUu88EufLCU56LMeFPZZM/fJZ1irYcYdqroaV+ACK1Ila8ls09iEYArp+nqyC6lW1Vfg==
+istanbul-lib-instrument@^4.0.0, istanbul-lib-instrument@^4.0.3:
+  version "4.0.3"
+  resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-4.0.3.tgz#873c6fff897450118222774696a3f28902d77c1d"
+  integrity sha512-BXgQl9kf4WTCPCCpmFGoJkz/+uhvm7h7PFKUYxh7qarQd3ER33vHG//qaE8eN25l07YqZPpHXU9I09l/RD5aGQ==
   dependencies:
     "@babel/core" "^7.7.5"
-    "@babel/parser" "^7.7.5"
-    "@babel/template" "^7.7.4"
-    "@babel/traverse" "^7.7.4"
     "@istanbuljs/schema" "^0.1.2"
     istanbul-lib-coverage "^3.0.0"
     semver "^6.3.0"
@@ -3579,127 +4357,142 @@ istanbul-reports@^3.0.2:
     html-escaper "^2.0.0"
     istanbul-lib-report "^3.0.0"
 
-jest-changed-files@^25.4.0:
-  version "25.4.0"
-  resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-25.4.0.tgz#e573db32c2fd47d2b90357ea2eda0622c5c5cbd6"
-  integrity sha512-VR/rfJsEs4BVMkwOTuStRyS630fidFVekdw/lBaBQjx9KK3VZFOZ2c0fsom2fRp8pMCrCTP6LGna00o/DXGlqA==
+jest-changed-files@^26.1.0:
+  version "26.1.0"
+  resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-26.1.0.tgz#de66b0f30453bca2aff98e9400f75905da495305"
+  integrity sha512-HS5MIJp3B8t0NRKGMCZkcDUZo36mVRvrDETl81aqljT1S9tqiHRSpyoOvWg9ZilzZG9TDisDNaN1IXm54fLRZw==
   dependencies:
-    "@jest/types" "^25.4.0"
-    execa "^3.2.0"
+    "@jest/types" "^26.1.0"
+    execa "^4.0.0"
     throat "^5.0.0"
 
-jest-cli@^25.4.0:
-  version "25.4.0"
-  resolved "https://registry.yarnpkg.com/jest-cli/-/jest-cli-25.4.0.tgz#5dac8be0fece6ce39f0d671395a61d1357322bab"
-  integrity sha512-usyrj1lzCJZMRN1r3QEdnn8e6E6yCx/QN7+B1sLoA68V7f3WlsxSSQfy0+BAwRiF4Hz2eHauf11GZG3PIfWTXQ==
+jest-cli@^26.1.0:
+  version "26.1.0"
+  resolved "https://registry.yarnpkg.com/jest-cli/-/jest-cli-26.1.0.tgz#eb9ec8a18cf3b6aa556d9deaa9e24be12b43ad87"
+  integrity sha512-Imumvjgi3rU7stq6SJ1JUEMaV5aAgJYXIs0jPqdUnF47N/Tk83EXfmtvNKQ+SnFVI6t6mDOvfM3aA9Sg6kQPSw==
   dependencies:
-    "@jest/core" "^25.4.0"
-    "@jest/test-result" "^25.4.0"
-    "@jest/types" "^25.4.0"
-    chalk "^3.0.0"
+    "@jest/core" "^26.1.0"
+    "@jest/test-result" "^26.1.0"
+    "@jest/types" "^26.1.0"
+    chalk "^4.0.0"
     exit "^0.1.2"
+    graceful-fs "^4.2.4"
     import-local "^3.0.2"
     is-ci "^2.0.0"
-    jest-config "^25.4.0"
-    jest-util "^25.4.0"
-    jest-validate "^25.4.0"
+    jest-config "^26.1.0"
+    jest-util "^26.1.0"
+    jest-validate "^26.1.0"
     prompts "^2.0.1"
-    realpath-native "^2.0.0"
     yargs "^15.3.1"
 
-jest-config@^25.4.0:
-  version "25.4.0"
-  resolved "https://registry.yarnpkg.com/jest-config/-/jest-config-25.4.0.tgz#56e5df3679a96ff132114b44fb147389c8c0a774"
-  integrity sha512-egT9aKYxMyMSQV1aqTgam0SkI5/I2P9qrKexN5r2uuM2+68ypnc+zPGmfUxK7p1UhE7dYH9SLBS7yb+TtmT1AA==
+jest-config@^26.1.0:
+  version "26.1.0"
+  resolved "https://registry.yarnpkg.com/jest-config/-/jest-config-26.1.0.tgz#9074f7539acc185e0113ad6d22ed589c16a37a73"
+  integrity sha512-ONTGeoMbAwGCdq4WuKkMcdMoyfs5CLzHEkzFOlVvcDXufZSaIWh/OXMLa2fwKXiOaFcqEw8qFr4VOKJQfn4CVw==
   dependencies:
     "@babel/core" "^7.1.0"
-    "@jest/test-sequencer" "^25.4.0"
-    "@jest/types" "^25.4.0"
-    babel-jest "^25.4.0"
-    chalk "^3.0.0"
+    "@jest/test-sequencer" "^26.1.0"
+    "@jest/types" "^26.1.0"
+    babel-jest "^26.1.0"
+    chalk "^4.0.0"
     deepmerge "^4.2.2"
     glob "^7.1.1"
-    jest-environment-jsdom "^25.4.0"
-    jest-environment-node "^25.4.0"
-    jest-get-type "^25.2.6"
-    jest-jasmine2 "^25.4.0"
-    jest-regex-util "^25.2.6"
-    jest-resolve "^25.4.0"
-    jest-util "^25.4.0"
-    jest-validate "^25.4.0"
+    graceful-fs "^4.2.4"
+    jest-environment-jsdom "^26.1.0"
+    jest-environment-node "^26.1.0"
+    jest-get-type "^26.0.0"
+    jest-jasmine2 "^26.1.0"
+    jest-regex-util "^26.0.0"
+    jest-resolve "^26.1.0"
+    jest-util "^26.1.0"
+    jest-validate "^26.1.0"
     micromatch "^4.0.2"
-    pretty-format "^25.4.0"
-    realpath-native "^2.0.0"
+    pretty-format "^26.1.0"
 
-jest-diff@^25.2.1, jest-diff@^25.4.0:
-  version "25.4.0"
-  resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-25.4.0.tgz#260b70f19a46c283adcad7f081cae71eb784a634"
-  integrity sha512-kklLbJVXW0y8UKOWOdYhI6TH5MG6QAxrWiBMgQaPIuhj3dNFGirKCd+/xfplBXICQ7fI+3QcqHm9p9lWu1N6ug==
+jest-diff@^25.2.1:
+  version "25.5.0"
+  resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-25.5.0.tgz#1dd26ed64f96667c068cef026b677dfa01afcfa9"
+  integrity sha512-z1kygetuPiREYdNIumRpAHY6RXiGmp70YHptjdaxTWGmA085W3iCnXNx0DhflK3vwrKmrRWyY1wUpkPMVxMK7A==
   dependencies:
     chalk "^3.0.0"
     diff-sequences "^25.2.6"
     jest-get-type "^25.2.6"
-    pretty-format "^25.4.0"
+    pretty-format "^25.5.0"
+
+jest-diff@^26.1.0:
+  version "26.1.0"
+  resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-26.1.0.tgz#00a549bdc936c9691eb4dc25d1fbd78bf456abb2"
+  integrity sha512-GZpIcom339y0OXznsEKjtkfKxNdg7bVbEofK8Q6MnevTIiR1jNhDWKhRX6X0SDXJlwn3dy59nZ1z55fLkAqPWg==
+  dependencies:
+    chalk "^4.0.0"
+    diff-sequences "^26.0.0"
+    jest-get-type "^26.0.0"
+    pretty-format "^26.1.0"
 
-jest-docblock@^25.3.0:
-  version "25.3.0"
-  resolved "https://registry.yarnpkg.com/jest-docblock/-/jest-docblock-25.3.0.tgz#8b777a27e3477cd77a168c05290c471a575623ef"
-  integrity sha512-aktF0kCar8+zxRHxQZwxMy70stc9R1mOmrLsT5VO3pIT0uzGRSDAXxSlz4NqQWpuLjPpuMhPRl7H+5FRsvIQAg==
+jest-docblock@^26.0.0:
+  version "26.0.0"
+  resolved "https://registry.yarnpkg.com/jest-docblock/-/jest-docblock-26.0.0.tgz#3e2fa20899fc928cb13bd0ff68bd3711a36889b5"
+  integrity sha512-RDZ4Iz3QbtRWycd8bUEPxQsTlYazfYn/h5R65Fc6gOfwozFhoImx+affzky/FFBuqISPTqjXomoIGJVKBWoo0w==
   dependencies:
     detect-newline "^3.0.0"
 
-jest-each@^25.4.0:
-  version "25.4.0"
-  resolved "https://registry.yarnpkg.com/jest-each/-/jest-each-25.4.0.tgz#ad4e46164764e8e77058f169a0076a7f86f6b7d4"
-  integrity sha512-lwRIJ8/vQU/6vq3nnSSUw1Y3nz5tkYSFIywGCZpUBd6WcRgpn8NmJoQICojbpZmsJOJNHm0BKdyuJ6Xdx+eDQQ==
+jest-each@^26.1.0:
+  version "26.1.0"
+  resolved "https://registry.yarnpkg.com/jest-each/-/jest-each-26.1.0.tgz#e35449875009a22d74d1bda183b306db20f286f7"
+  integrity sha512-lYiSo4Igr81q6QRsVQq9LIkJW0hZcKxkIkHzNeTMPENYYDw/W/Raq28iJ0sLlNFYz2qxxeLnc5K2gQoFYlu2bA==
   dependencies:
-    "@jest/types" "^25.4.0"
-    chalk "^3.0.0"
-    jest-get-type "^25.2.6"
-    jest-util "^25.4.0"
-    pretty-format "^25.4.0"
-
-jest-environment-jsdom@^25.4.0:
-  version "25.4.0"
-  resolved "https://registry.yarnpkg.com/jest-environment-jsdom/-/jest-environment-jsdom-25.4.0.tgz#bbfc7f85bb6ade99089062a830c79cb454565cf0"
-  integrity sha512-KTitVGMDrn2+pt7aZ8/yUTuS333w3pWt1Mf88vMntw7ZSBNDkRS6/4XLbFpWXYfWfp1FjcjQTOKzbK20oIehWQ==
-  dependencies:
-    "@jest/environment" "^25.4.0"
-    "@jest/fake-timers" "^25.4.0"
-    "@jest/types" "^25.4.0"
-    jest-mock "^25.4.0"
-    jest-util "^25.4.0"
-    jsdom "^15.2.1"
-
-jest-environment-node@^25.4.0:
-  version "25.4.0"
-  resolved "https://registry.yarnpkg.com/jest-environment-node/-/jest-environment-node-25.4.0.tgz#188aef01ae6418e001c03fdd1c299961e1439082"
-  integrity sha512-wryZ18vsxEAKFH7Z74zi/y/SyI1j6UkVZ6QsllBuT/bWlahNfQjLNwFsgh/5u7O957dYFoXj4yfma4n4X6kU9A==
-  dependencies:
-    "@jest/environment" "^25.4.0"
-    "@jest/fake-timers" "^25.4.0"
-    "@jest/types" "^25.4.0"
-    jest-mock "^25.4.0"
-    jest-util "^25.4.0"
-    semver "^6.3.0"
+    "@jest/types" "^26.1.0"
+    chalk "^4.0.0"
+    jest-get-type "^26.0.0"
+    jest-util "^26.1.0"
+    pretty-format "^26.1.0"
+
+jest-environment-jsdom@^26.1.0:
+  version "26.1.0"
+  resolved "https://registry.yarnpkg.com/jest-environment-jsdom/-/jest-environment-jsdom-26.1.0.tgz#9dc7313ffe1b59761dad1fedb76e2503e5d37c5b"
+  integrity sha512-dWfiJ+spunVAwzXbdVqPH1LbuJW/kDL+FyqgA5YzquisHqTi0g9hquKif9xKm7c1bKBj6wbmJuDkeMCnxZEpUw==
+  dependencies:
+    "@jest/environment" "^26.1.0"
+    "@jest/fake-timers" "^26.1.0"
+    "@jest/types" "^26.1.0"
+    jest-mock "^26.1.0"
+    jest-util "^26.1.0"
+    jsdom "^16.2.2"
+
+jest-environment-node@^26.1.0:
+  version "26.1.0"
+  resolved "https://registry.yarnpkg.com/jest-environment-node/-/jest-environment-node-26.1.0.tgz#8bb387b3eefb132eab7826f9a808e4e05618960b"
+  integrity sha512-DNm5x1aQH0iRAe9UYAkZenuzuJ69VKzDCAYISFHQ5i9e+2Tbeu2ONGY7YStubCLH8a1wdKBgqScYw85+ySxqxg==
+  dependencies:
+    "@jest/environment" "^26.1.0"
+    "@jest/fake-timers" "^26.1.0"
+    "@jest/types" "^26.1.0"
+    jest-mock "^26.1.0"
+    jest-util "^26.1.0"
 
 jest-get-type@^25.2.6:
   version "25.2.6"
   resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-25.2.6.tgz#0b0a32fab8908b44d508be81681487dbabb8d877"
   integrity sha512-DxjtyzOHjObRM+sM1knti6or+eOgcGU4xVSb2HNP1TqO4ahsT+rqZg+nyqHWJSvWgKC5cG3QjGFBqxLghiF/Ig==
 
-jest-haste-map@^25.4.0:
-  version "25.4.0"
-  resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-25.4.0.tgz#da7c309dd7071e0a80c953ba10a0ec397efb1ae2"
-  integrity sha512-5EoCe1gXfGC7jmXbKzqxESrgRcaO3SzWXGCnvp9BcT0CFMyrB1Q6LIsjl9RmvmJGQgW297TCfrdgiy574Rl9HQ==
+jest-get-type@^26.0.0:
+  version "26.0.0"
+  resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-26.0.0.tgz#381e986a718998dbfafcd5ec05934be538db4039"
+  integrity sha512-zRc1OAPnnws1EVfykXOj19zo2EMw5Hi6HLbFCSjpuJiXtOWAYIjNsHVSbpQ8bDX7L5BGYGI8m+HmKdjHYFF0kg==
+
+jest-haste-map@^26.1.0:
+  version "26.1.0"
+  resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-26.1.0.tgz#ef31209be73f09b0d9445e7d213e1b53d0d1476a"
+  integrity sha512-WeBS54xCIz9twzkEdm6+vJBXgRBQfdbbXD0dk8lJh7gLihopABlJmIQFdWSDDtuDe4PRiObsjZSUjbJ1uhWEpA==
   dependencies:
-    "@jest/types" "^25.4.0"
+    "@jest/types" "^26.1.0"
+    "@types/graceful-fs" "^4.1.2"
     anymatch "^3.0.3"
     fb-watchman "^2.0.0"
-    graceful-fs "^4.2.3"
-    jest-serializer "^25.2.6"
-    jest-util "^25.4.0"
-    jest-worker "^25.4.0"
+    graceful-fs "^4.2.4"
+    jest-serializer "^26.1.0"
+    jest-util "^26.1.0"
+    jest-worker "^26.1.0"
     micromatch "^4.0.2"
     sane "^4.0.3"
     walker "^1.0.7"
@@ -3707,231 +4500,237 @@ jest-haste-map@^25.4.0:
   optionalDependencies:
     fsevents "^2.1.2"
 
-jest-jasmine2@^25.4.0:
-  version "25.4.0"
-  resolved "https://registry.yarnpkg.com/jest-jasmine2/-/jest-jasmine2-25.4.0.tgz#3d3d19514022e2326e836c2b66d68b4cb63c5861"
-  integrity sha512-QccxnozujVKYNEhMQ1vREiz859fPN/XklOzfQjm2j9IGytAkUbSwjFRBtQbHaNZ88cItMpw02JnHGsIdfdpwxQ==
+jest-jasmine2@^26.1.0:
+  version "26.1.0"
+  resolved "https://registry.yarnpkg.com/jest-jasmine2/-/jest-jasmine2-26.1.0.tgz#4dfe349b2b2d3c6b3a27c024fd4cb57ac0ed4b6f"
+  integrity sha512-1IPtoDKOAG+MeBrKvvuxxGPJb35MTTRSDglNdWWCndCB3TIVzbLThRBkwH9P081vXLgiJHZY8Bz3yzFS803xqQ==
   dependencies:
     "@babel/traverse" "^7.1.0"
-    "@jest/environment" "^25.4.0"
-    "@jest/source-map" "^25.2.6"
-    "@jest/test-result" "^25.4.0"
-    "@jest/types" "^25.4.0"
-    chalk "^3.0.0"
+    "@jest/environment" "^26.1.0"
+    "@jest/source-map" "^26.1.0"
+    "@jest/test-result" "^26.1.0"
+    "@jest/types" "^26.1.0"
+    chalk "^4.0.0"
     co "^4.6.0"
-    expect "^25.4.0"
+    expect "^26.1.0"
     is-generator-fn "^2.0.0"
-    jest-each "^25.4.0"
-    jest-matcher-utils "^25.4.0"
-    jest-message-util "^25.4.0"
-    jest-runtime "^25.4.0"
-    jest-snapshot "^25.4.0"
-    jest-util "^25.4.0"
-    pretty-format "^25.4.0"
+    jest-each "^26.1.0"
+    jest-matcher-utils "^26.1.0"
+    jest-message-util "^26.1.0"
+    jest-runtime "^26.1.0"
+    jest-snapshot "^26.1.0"
+    jest-util "^26.1.0"
+    pretty-format "^26.1.0"
     throat "^5.0.0"
 
-jest-leak-detector@^25.4.0:
-  version "25.4.0"
-  resolved "https://registry.yarnpkg.com/jest-leak-detector/-/jest-leak-detector-25.4.0.tgz#cf94a160c78e53d810e7b2f40b5fd7ee263375b3"
-  integrity sha512-7Y6Bqfv2xWsB+7w44dvZuLs5SQ//fzhETgOGG7Gq3TTGFdYvAgXGwV8z159RFZ6fXiCPm/szQ90CyfVos9JIFQ==
+jest-leak-detector@^26.1.0:
+  version "26.1.0"
+  resolved "https://registry.yarnpkg.com/jest-leak-detector/-/jest-leak-detector-26.1.0.tgz#039c3a07ebcd8adfa984b6ac015752c35792e0a6"
+  integrity sha512-dsMnKF+4BVOZwvQDlgn3MG+Ns4JuLv8jNvXH56bgqrrboyCbI1rQg6EI5rs+8IYagVcfVP2yZFKfWNZy0rK0Hw==
   dependencies:
-    jest-get-type "^25.2.6"
-    pretty-format "^25.4.0"
+    jest-get-type "^26.0.0"
+    pretty-format "^26.1.0"
 
-jest-matcher-utils@^25.4.0:
-  version "25.4.0"
-  resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-25.4.0.tgz#dc3e7aec402a1e567ed80b572b9ad285878895e6"
-  integrity sha512-yPMdtj7YDgXhnGbc66bowk8AkQ0YwClbbwk3Kzhn5GVDrciiCr27U4NJRbrqXbTdtxjImONITg2LiRIw650k5A==
+jest-matcher-utils@^26.1.0:
+  version "26.1.0"
+  resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-26.1.0.tgz#cf75a41bd413dda784f022de5a65a2a5c73a5c92"
+  integrity sha512-PW9JtItbYvES/xLn5mYxjMd+Rk+/kIt88EfH3N7w9KeOrHWaHrdYPnVHndGbsFGRJ2d5gKtwggCvkqbFDoouQA==
   dependencies:
-    chalk "^3.0.0"
-    jest-diff "^25.4.0"
-    jest-get-type "^25.2.6"
-    pretty-format "^25.4.0"
+    chalk "^4.0.0"
+    jest-diff "^26.1.0"
+    jest-get-type "^26.0.0"
+    pretty-format "^26.1.0"
 
-jest-message-util@^25.4.0:
-  version "25.4.0"
-  resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-25.4.0.tgz#2899e8bc43f5317acf8dfdfe89ea237d354fcdab"
-  integrity sha512-LYY9hRcVGgMeMwmdfh9tTjeux1OjZHMusq/E5f3tJN+dAoVVkJtq5ZUEPIcB7bpxDUt2zjUsrwg0EGgPQ+OhXQ==
+jest-message-util@^26.1.0:
+  version "26.1.0"
+  resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-26.1.0.tgz#52573fbb8f5cea443c4d1747804d7a238a3e233c"
+  integrity sha512-dY0+UlldiAJwNDJ08SF0HdF32g9PkbF2NRK/+2iMPU40O6q+iSn1lgog/u0UH8ksWoPv0+gNq8cjhYO2MFtT0g==
   dependencies:
     "@babel/code-frame" "^7.0.0"
-    "@jest/types" "^25.4.0"
+    "@jest/types" "^26.1.0"
     "@types/stack-utils" "^1.0.1"
-    chalk "^3.0.0"
+    chalk "^4.0.0"
+    graceful-fs "^4.2.4"
     micromatch "^4.0.2"
     slash "^3.0.0"
-    stack-utils "^1.0.1"
+    stack-utils "^2.0.2"
 
-jest-mock@^25.4.0:
-  version "25.4.0"
-  resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-25.4.0.tgz#ded7d64b5328d81d78d2138c825d3a45e30ec8ca"
-  integrity sha512-MdazSfcYAUjJjuVTTnusLPzE0pE4VXpOUzWdj8sbM+q6abUjm3bATVPXFqTXrxSieR8ocpvQ9v/QaQCftioQFg==
+jest-mock@^26.1.0:
+  version "26.1.0"
+  resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-26.1.0.tgz#80d8286da1f05a345fbad1bfd6fa49a899465d3d"
+  integrity sha512-1Rm8EIJ3ZFA8yCIie92UbxZWj9SuVmUGcyhLHyAhY6WI3NIct38nVcfOPWhJteqSn8V8e3xOMha9Ojfazfpovw==
   dependencies:
-    "@jest/types" "^25.4.0"
+    "@jest/types" "^26.1.0"
 
 jest-pnp-resolver@^1.2.1:
-  version "1.2.1"
-  resolved "https://registry.yarnpkg.com/jest-pnp-resolver/-/jest-pnp-resolver-1.2.1.tgz#ecdae604c077a7fbc70defb6d517c3c1c898923a"
-  integrity sha512-pgFw2tm54fzgYvc/OHrnysABEObZCUNFnhjoRjaVOCN8NYc032/gVjPaHD4Aq6ApkSieWtfKAFQtmDKAmhupnQ==
-
-jest-regex-util@^25.2.6:
-  version "25.2.6"
-  resolved "https://registry.yarnpkg.com/jest-regex-util/-/jest-regex-util-25.2.6.tgz#d847d38ba15d2118d3b06390056028d0f2fd3964"
-  integrity sha512-KQqf7a0NrtCkYmZZzodPftn7fL1cq3GQAFVMn5Hg8uKx/fIenLEobNanUxb7abQ1sjADHBseG/2FGpsv/wr+Qw==
-
-jest-resolve-dependencies@^25.4.0:
-  version "25.4.0"
-  resolved "https://registry.yarnpkg.com/jest-resolve-dependencies/-/jest-resolve-dependencies-25.4.0.tgz#783937544cfc40afcc7c569aa54748c4b3f83f5a"
-  integrity sha512-A0eoZXx6kLiuG1Ui7wITQPl04HwjLErKIJTt8GR3c7UoDAtzW84JtCrgrJ6Tkw6c6MwHEyAaLk7dEPml5pf48A==
-  dependencies:
-    "@jest/types" "^25.4.0"
-    jest-regex-util "^25.2.6"
-    jest-snapshot "^25.4.0"
-
-jest-resolve@^25.4.0:
-  version "25.4.0"
-  resolved "https://registry.yarnpkg.com/jest-resolve/-/jest-resolve-25.4.0.tgz#6f4540ce0d419c4c720e791e871da32ba4da7a60"
-  integrity sha512-wOsKqVDFWUiv8BtLMCC6uAJ/pHZkfFgoBTgPtmYlsprAjkxrr2U++ZnB3l5ykBMd2O24lXvf30SMAjJIW6k2aA==
-  dependencies:
-    "@jest/types" "^25.4.0"
-    browser-resolve "^1.11.3"
-    chalk "^3.0.0"
+  version "1.2.2"
+  resolved "https://registry.yarnpkg.com/jest-pnp-resolver/-/jest-pnp-resolver-1.2.2.tgz#b704ac0ae028a89108a4d040b3f919dfddc8e33c"
+  integrity sha512-olV41bKSMm8BdnuMsewT4jqlZ8+3TCARAXjZGT9jcoSnrfUnRCqnMoF9XEeoWjbzObpqF9dRhHQj0Xb9QdF6/w==
+
+jest-regex-util@^26.0.0:
+  version "26.0.0"
+  resolved "https://registry.yarnpkg.com/jest-regex-util/-/jest-regex-util-26.0.0.tgz#d25e7184b36e39fd466c3bc41be0971e821fee28"
+  integrity sha512-Gv3ZIs/nA48/Zvjrl34bf+oD76JHiGDUxNOVgUjh3j890sblXryjY4rss71fPtD/njchl6PSE2hIhvyWa1eT0A==
+
+jest-resolve-dependencies@^26.1.0:
+  version "26.1.0"
+  resolved "https://registry.yarnpkg.com/jest-resolve-dependencies/-/jest-resolve-dependencies-26.1.0.tgz#1ce36472f864a5dadf7dc82fa158e1c77955691b"
+  integrity sha512-fQVEPHHQ1JjHRDxzlLU/buuQ9om+hqW6Vo928aa4b4yvq4ZHBtRSDsLdKQLuCqn5CkTVpYZ7ARh2fbA8WkRE6g==
+  dependencies:
+    "@jest/types" "^26.1.0"
+    jest-regex-util "^26.0.0"
+    jest-snapshot "^26.1.0"
+
+jest-resolve@^26.1.0:
+  version "26.1.0"
+  resolved "https://registry.yarnpkg.com/jest-resolve/-/jest-resolve-26.1.0.tgz#a530eaa302b1f6fa0479079d1561dd69abc00e68"
+  integrity sha512-KsY1JV9FeVgEmwIISbZZN83RNGJ1CC+XUCikf/ZWJBX/tO4a4NvA21YixokhdR9UnmPKKAC4LafVixJBrwlmfg==
+  dependencies:
+    "@jest/types" "^26.1.0"
+    chalk "^4.0.0"
+    graceful-fs "^4.2.4"
     jest-pnp-resolver "^1.2.1"
+    jest-util "^26.1.0"
     read-pkg-up "^7.0.1"
-    realpath-native "^2.0.0"
-    resolve "^1.15.1"
+    resolve "^1.17.0"
     slash "^3.0.0"
 
-jest-runner@^25.4.0:
-  version "25.4.0"
-  resolved "https://registry.yarnpkg.com/jest-runner/-/jest-runner-25.4.0.tgz#6ca4a3d52e692bbc081228fa68f750012f1f29e5"
-  integrity sha512-wWQSbVgj2e/1chFdMRKZdvlmA6p1IPujhpLT7TKNtCSl1B0PGBGvJjCaiBal/twaU2yfk8VKezHWexM8IliBfA==
+jest-runner@^26.1.0:
+  version "26.1.0"
+  resolved "https://registry.yarnpkg.com/jest-runner/-/jest-runner-26.1.0.tgz#457f7fc522afe46ca6db1dccf19f87f500b3288d"
+  integrity sha512-elvP7y0fVDREnfqit0zAxiXkDRSw6dgCkzPCf1XvIMnSDZ8yogmSKJf192dpOgnUVykmQXwYYJnCx641uLTgcw==
   dependencies:
-    "@jest/console" "^25.4.0"
-    "@jest/environment" "^25.4.0"
-    "@jest/test-result" "^25.4.0"
-    "@jest/types" "^25.4.0"
-    chalk "^3.0.0"
+    "@jest/console" "^26.1.0"
+    "@jest/environment" "^26.1.0"
+    "@jest/test-result" "^26.1.0"
+    "@jest/types" "^26.1.0"
+    chalk "^4.0.0"
     exit "^0.1.2"
-    graceful-fs "^4.2.3"
-    jest-config "^25.4.0"
-    jest-docblock "^25.3.0"
-    jest-haste-map "^25.4.0"
-    jest-jasmine2 "^25.4.0"
-    jest-leak-detector "^25.4.0"
-    jest-message-util "^25.4.0"
-    jest-resolve "^25.4.0"
-    jest-runtime "^25.4.0"
-    jest-util "^25.4.0"
-    jest-worker "^25.4.0"
+    graceful-fs "^4.2.4"
+    jest-config "^26.1.0"
+    jest-docblock "^26.0.0"
+    jest-haste-map "^26.1.0"
+    jest-jasmine2 "^26.1.0"
+    jest-leak-detector "^26.1.0"
+    jest-message-util "^26.1.0"
+    jest-resolve "^26.1.0"
+    jest-runtime "^26.1.0"
+    jest-util "^26.1.0"
+    jest-worker "^26.1.0"
     source-map-support "^0.5.6"
     throat "^5.0.0"
 
-jest-runtime@^25.4.0:
-  version "25.4.0"
-  resolved "https://registry.yarnpkg.com/jest-runtime/-/jest-runtime-25.4.0.tgz#1e5227a9e2159d26ae27dcd426ca6bc041983439"
-  integrity sha512-lgNJlCDULtXu9FumnwCyWlOub8iytijwsPNa30BKrSNtgoT6NUMXOPrZvsH06U6v0wgD/Igwz13nKA2wEKU2VA==
-  dependencies:
-    "@jest/console" "^25.4.0"
-    "@jest/environment" "^25.4.0"
-    "@jest/source-map" "^25.2.6"
-    "@jest/test-result" "^25.4.0"
-    "@jest/transform" "^25.4.0"
-    "@jest/types" "^25.4.0"
+jest-runtime@^26.1.0:
+  version "26.1.0"
+  resolved "https://registry.yarnpkg.com/jest-runtime/-/jest-runtime-26.1.0.tgz#45a37af42115f123ed5c51f126c05502da2469cb"
+  integrity sha512-1qiYN+EZLmG1QV2wdEBRf+Ci8i3VSfIYLF02U18PiUDrMbhfpN/EAMMkJtT02jgJUoaEOpHAIXG6zS3QRMzRmA==
+  dependencies:
+    "@jest/console" "^26.1.0"
+    "@jest/environment" "^26.1.0"
+    "@jest/fake-timers" "^26.1.0"
+    "@jest/globals" "^26.1.0"
+    "@jest/source-map" "^26.1.0"
+    "@jest/test-result" "^26.1.0"
+    "@jest/transform" "^26.1.0"
+    "@jest/types" "^26.1.0"
     "@types/yargs" "^15.0.0"
-    chalk "^3.0.0"
+    chalk "^4.0.0"
     collect-v8-coverage "^1.0.0"
     exit "^0.1.2"
     glob "^7.1.3"
-    graceful-fs "^4.2.3"
-    jest-config "^25.4.0"
-    jest-haste-map "^25.4.0"
-    jest-message-util "^25.4.0"
-    jest-mock "^25.4.0"
-    jest-regex-util "^25.2.6"
-    jest-resolve "^25.4.0"
-    jest-snapshot "^25.4.0"
-    jest-util "^25.4.0"
-    jest-validate "^25.4.0"
-    realpath-native "^2.0.0"
+    graceful-fs "^4.2.4"
+    jest-config "^26.1.0"
+    jest-haste-map "^26.1.0"
+    jest-message-util "^26.1.0"
+    jest-mock "^26.1.0"
+    jest-regex-util "^26.0.0"
+    jest-resolve "^26.1.0"
+    jest-snapshot "^26.1.0"
+    jest-util "^26.1.0"
+    jest-validate "^26.1.0"
     slash "^3.0.0"
     strip-bom "^4.0.0"
     yargs "^15.3.1"
 
-jest-serializer@^25.2.6:
-  version "25.2.6"
-  resolved "https://registry.yarnpkg.com/jest-serializer/-/jest-serializer-25.2.6.tgz#3bb4cc14fe0d8358489dbbefbb8a4e708ce039b7"
-  integrity sha512-RMVCfZsezQS2Ww4kB5HJTMaMJ0asmC0BHlnobQC6yEtxiFKIxohFA4QSXSabKwSggaNkqxn6Z2VwdFCjhUWuiQ==
+jest-serializer@^26.1.0:
+  version "26.1.0"
+  resolved "https://registry.yarnpkg.com/jest-serializer/-/jest-serializer-26.1.0.tgz#72a394531fc9b08e173dc7d297440ac610d95022"
+  integrity sha512-eqZOQG/0+MHmr25b2Z86g7+Kzd5dG9dhCiUoyUNJPgiqi38DqbDEOlHcNijyfZoj74soGBohKBZuJFS18YTJ5w==
+  dependencies:
+    graceful-fs "^4.2.4"
 
-jest-snapshot@^25.4.0:
-  version "25.4.0"
-  resolved "https://registry.yarnpkg.com/jest-snapshot/-/jest-snapshot-25.4.0.tgz#e0b26375e2101413fd2ccb4278a5711b1922545c"
-  integrity sha512-J4CJ0X2SaGheYRZdLz9CRHn9jUknVmlks4UBeu270hPAvdsauFXOhx9SQP2JtRzhnR3cvro/9N9KP83/uvFfRg==
+jest-snapshot@^26.1.0:
+  version "26.1.0"
+  resolved "https://registry.yarnpkg.com/jest-snapshot/-/jest-snapshot-26.1.0.tgz#c36ed1e0334bd7bd2fe5ad07e93a364ead7e1349"
+  integrity sha512-YhSbU7eMTVQO/iRbNs8j0mKRxGp4plo7sJ3GzOQ0IYjvsBiwg0T1o0zGQAYepza7lYHuPTrG5J2yDd0CE2YxSw==
   dependencies:
     "@babel/types" "^7.0.0"
-    "@jest/types" "^25.4.0"
-    "@types/prettier" "^1.19.0"
-    chalk "^3.0.0"
-    expect "^25.4.0"
-    jest-diff "^25.4.0"
-    jest-get-type "^25.2.6"
-    jest-matcher-utils "^25.4.0"
-    jest-message-util "^25.4.0"
-    jest-resolve "^25.4.0"
-    make-dir "^3.0.0"
+    "@jest/types" "^26.1.0"
+    "@types/prettier" "^2.0.0"
+    chalk "^4.0.0"
+    expect "^26.1.0"
+    graceful-fs "^4.2.4"
+    jest-diff "^26.1.0"
+    jest-get-type "^26.0.0"
+    jest-haste-map "^26.1.0"
+    jest-matcher-utils "^26.1.0"
+    jest-message-util "^26.1.0"
+    jest-resolve "^26.1.0"
     natural-compare "^1.4.0"
-    pretty-format "^25.4.0"
-    semver "^6.3.0"
+    pretty-format "^26.1.0"
+    semver "^7.3.2"
 
-jest-util@^25.4.0:
-  version "25.4.0"
-  resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-25.4.0.tgz#6a093d09d86d2b41ef583e5fe7dd3976346e1acd"
-  integrity sha512-WSZD59sBtAUjLv1hMeKbNZXmMcrLRWcYqpO8Dz8b4CeCTZpfNQw2q9uwrYAD+BbJoLJlu4ezVPwtAmM/9/SlZA==
+jest-util@26.x, jest-util@^26.1.0:
+  version "26.1.0"
+  resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-26.1.0.tgz#80e85d4ba820decacf41a691c2042d5276e5d8d8"
+  integrity sha512-rNMOwFQevljfNGvbzNQAxdmXQ+NawW/J72dmddsK0E8vgxXCMtwQ/EH0BiWEIxh0hhMcTsxwAxINt7Lh46Uzbg==
   dependencies:
-    "@jest/types" "^25.4.0"
-    chalk "^3.0.0"
+    "@jest/types" "^26.1.0"
+    chalk "^4.0.0"
+    graceful-fs "^4.2.4"
     is-ci "^2.0.0"
-    make-dir "^3.0.0"
+    micromatch "^4.0.2"
 
-jest-validate@^25.4.0:
-  version "25.4.0"
-  resolved "https://registry.yarnpkg.com/jest-validate/-/jest-validate-25.4.0.tgz#2e177a93b716a137110eaf2768f3d9095abd3f38"
-  integrity sha512-hvjmes/EFVJSoeP1yOl8qR8mAtMR3ToBkZeXrD/ZS9VxRyWDqQ/E1C5ucMTeSmEOGLipvdlyipiGbHJ+R1MQ0g==
+jest-validate@^26.1.0:
+  version "26.1.0"
+  resolved "https://registry.yarnpkg.com/jest-validate/-/jest-validate-26.1.0.tgz#942c85ad3d60f78250c488a7f85d8f11a29788e7"
+  integrity sha512-WPApOOnXsiwhZtmkDsxnpye+XLb/tUISP+H6cHjfUIXvlG+eKwP+isnivsxlHCPaO9Q5wvbhloIBkdF3qUn+Nw==
   dependencies:
-    "@jest/types" "^25.4.0"
-    camelcase "^5.3.1"
-    chalk "^3.0.0"
-    jest-get-type "^25.2.6"
+    "@jest/types" "^26.1.0"
+    camelcase "^6.0.0"
+    chalk "^4.0.0"
+    jest-get-type "^26.0.0"
     leven "^3.1.0"
-    pretty-format "^25.4.0"
+    pretty-format "^26.1.0"
 
-jest-watcher@^25.4.0:
-  version "25.4.0"
-  resolved "https://registry.yarnpkg.com/jest-watcher/-/jest-watcher-25.4.0.tgz#63ec0cd5c83bb9c9d1ac95be7558dd61c995ff05"
-  integrity sha512-36IUfOSRELsKLB7k25j/wutx0aVuHFN6wO94gPNjQtQqFPa2rkOymmx9rM5EzbF3XBZZ2oqD9xbRVoYa2w86gw==
+jest-watcher@^26.1.0:
+  version "26.1.0"
+  resolved "https://registry.yarnpkg.com/jest-watcher/-/jest-watcher-26.1.0.tgz#99812a0cd931f0cb3d153180426135ab83e4d8f2"
+  integrity sha512-ffEOhJl2EvAIki613oPsSG11usqnGUzIiK7MMX6hE4422aXOcVEG3ySCTDFLn1+LZNXGPE8tuJxhp8OBJ1pgzQ==
   dependencies:
-    "@jest/test-result" "^25.4.0"
-    "@jest/types" "^25.4.0"
+    "@jest/test-result" "^26.1.0"
+    "@jest/types" "^26.1.0"
     ansi-escapes "^4.2.1"
-    chalk "^3.0.0"
-    jest-util "^25.4.0"
-    string-length "^3.1.0"
+    chalk "^4.0.0"
+    jest-util "^26.1.0"
+    string-length "^4.0.1"
 
-jest-worker@^25.4.0:
-  version "25.4.0"
-  resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-25.4.0.tgz#ee0e2ceee5a36ecddf5172d6d7e0ab00df157384"
-  integrity sha512-ghAs/1FtfYpMmYQ0AHqxV62XPvKdUDIBBApMZfly+E9JEmYh2K45G0R5dWxx986RN12pRCxsViwQVtGl+N4whw==
+jest-worker@^26.1.0:
+  version "26.1.0"
+  resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-26.1.0.tgz#65d5641af74e08ccd561c240e7db61284f82f33d"
+  integrity sha512-Z9P5pZ6UC+kakMbNJn+tA2RdVdNX5WH1x+5UCBZ9MxIK24pjYtFt96fK+UwBTrjLYm232g1xz0L3eTh51OW+yQ==
   dependencies:
     merge-stream "^2.0.0"
     supports-color "^7.0.0"
 
-jest@^25.4.0:
-  version "25.4.0"
-  resolved "https://registry.yarnpkg.com/jest/-/jest-25.4.0.tgz#fb96892c5c4e4a6b9bcb12068849cddf4c5f8cc7"
-  integrity sha512-XWipOheGB4wai5JfCYXd6vwsWNwM/dirjRoZgAa7H2wd8ODWbli2AiKjqG8AYhyx+8+5FBEdpO92VhGlBydzbw==
+jest@^26.0.7:
+  version "26.1.0"
+  resolved "https://registry.yarnpkg.com/jest/-/jest-26.1.0.tgz#2f3aa7bcffb9bfd025473f83bbbf46a3af026263"
+  integrity sha512-LIti8jppw5BcQvmNJe4w2g1N/3V68HUfAv9zDVm7v+VAtQulGhH0LnmmiVkbNE4M4I43Bj2fXPiBGKt26k9tHw==
   dependencies:
-    "@jest/core" "^25.4.0"
+    "@jest/core" "^26.1.0"
     import-local "^3.0.2"
-    jest-cli "^25.4.0"
+    jest-cli "^26.1.0"
 
 js-cookie@^2.2.0:
   version "2.2.1"
@@ -3944,9 +4743,9 @@ js-cookie@^2.2.0:
   integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==
 
 js-yaml@^3.13.1:
-  version "3.13.1"
-  resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.13.1.tgz#aff151b30bfdfa8e49e05da22e7415e9dfa37847"
-  integrity sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==
+  version "3.14.0"
+  resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.0.tgz#a7a34170f26a21bb162424d8adacb4113a69e482"
+  integrity sha512-/4IbIeHcD9VMHFqDR/gQ7EdZdLimOvW2DdcxFjdyyZ9NsbS+ccrXqVWDtab/lRl5AlUqmpBx8EhPaWR+OtY17A==
   dependencies:
     argparse "^1.0.7"
     esprima "^4.0.0"
@@ -3956,36 +4755,36 @@ jsbn@~0.1.0:
   resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513"
   integrity sha1-peZUwuWi3rXyAdls77yoDA7y9RM=
 
-jsdom@^15.2.1:
-  version "15.2.1"
-  resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-15.2.1.tgz#d2feb1aef7183f86be521b8c6833ff5296d07ec5"
-  integrity sha512-fAl1W0/7T2G5vURSyxBzrJ1LSdQn6Tr5UX/xD4PXDx/PDgwygedfW6El/KIj3xJ7FU61TTYnc/l/B7P49Eqt6g==
-  dependencies:
-    abab "^2.0.0"
-    acorn "^7.1.0"
-    acorn-globals "^4.3.2"
-    array-equal "^1.0.0"
-    cssom "^0.4.1"
-    cssstyle "^2.0.0"
-    data-urls "^1.1.0"
-    domexception "^1.0.1"
-    escodegen "^1.11.1"
-    html-encoding-sniffer "^1.0.2"
+jsdom@^16.2.2:
+  version "16.3.0"
+  resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-16.3.0.tgz#75690b7dac36c67be49c336dcd7219bbbed0810c"
+  integrity sha512-zggeX5UuEknpdZzv15+MS1dPYG0J/TftiiNunOeNxSl3qr8Z6cIlQpN0IdJa44z9aFxZRIVqRncvEhQ7X5DtZg==
+  dependencies:
+    abab "^2.0.3"
+    acorn "^7.1.1"
+    acorn-globals "^6.0.0"
+    cssom "^0.4.4"
+    cssstyle "^2.2.0"
+    data-urls "^2.0.0"
+    decimal.js "^10.2.0"
+    domexception "^2.0.1"
+    escodegen "^1.14.1"
+    html-encoding-sniffer "^2.0.1"
+    is-potential-custom-element-name "^1.0.0"
     nwsapi "^2.2.0"
-    parse5 "5.1.0"
-    pn "^1.1.0"
-    request "^2.88.0"
-    request-promise-native "^1.0.7"
-    saxes "^3.1.9"
-    symbol-tree "^3.2.2"
+    parse5 "5.1.1"
+    request "^2.88.2"
+    request-promise-native "^1.0.8"
+    saxes "^5.0.0"
+    symbol-tree "^3.2.4"
     tough-cookie "^3.0.1"
-    w3c-hr-time "^1.0.1"
-    w3c-xmlserializer "^1.1.2"
-    webidl-conversions "^4.0.2"
+    w3c-hr-time "^1.0.2"
+    w3c-xmlserializer "^2.0.0"
+    webidl-conversions "^6.1.0"
     whatwg-encoding "^1.0.5"
     whatwg-mimetype "^2.3.0"
-    whatwg-url "^7.0.0"
-    ws "^7.0.0"
+    whatwg-url "^8.0.0"
+    ws "^7.2.3"
     xml-name-validator "^3.0.0"
 
 jsesc@^2.5.1:
@@ -3998,7 +4797,7 @@ jsesc@~0.5.0:
   resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-0.5.0.tgz#e7dee66e35d6fc16f710fe91d5cf69f70f08911d"
   integrity sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0=
 
-json-parse-better-errors@^1.0.1:
+json-parse-better-errors@^1.0.0, json-parse-better-errors@^1.0.1, json-parse-better-errors@^1.0.2:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz#bb867cfb3450e69107c131d1c514bab3dc8bcaa9"
   integrity sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==
@@ -4030,6 +4829,13 @@ json5@2.x, json5@^2.1.2:
   dependencies:
     minimist "^1.2.5"
 
+json5@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.1.tgz#779fb0018604fa854eacbf6252180d83543e3dbe"
+  integrity sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==
+  dependencies:
+    minimist "^1.2.0"
+
 jsonfile@^4.0.0:
   version "4.0.0"
   resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-4.0.0.tgz#8771aae0799b64076b76640fca058f9c10e33ecb"
@@ -4037,6 +4843,11 @@ jsonfile@^4.0.0:
   optionalDependencies:
     graceful-fs "^4.1.6"
 
+jsonparse@^1.2.0:
+  version "1.3.1"
+  resolved "https://registry.yarnpkg.com/jsonparse/-/jsonparse-1.3.1.tgz#3f4dae4a91fac315f71062f8521cc239f1366280"
+  integrity sha1-P02uSpH6wxX3EGL4UhzCOfE2YoA=
+
 jsprim@^1.2.2:
   version "1.4.1"
   resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2"
@@ -4047,12 +4858,12 @@ jsprim@^1.2.2:
     json-schema "0.2.3"
     verror "1.10.0"
 
-jsx-ast-utils@^2.2.1, jsx-ast-utils@^2.2.3:
-  version "2.2.3"
-  resolved "https://registry.yarnpkg.com/jsx-ast-utils/-/jsx-ast-utils-2.2.3.tgz#8a9364e402448a3ce7f14d357738310d9248054f"
-  integrity sha512-EdIHFMm+1BPynpKOpdPqiOsvnIrInRGJD7bzPZdPkjitQEqpdpUuFpq4T0npZFKTiB3RhWFdGN+oqOJIdhDhQA==
+jsx-ast-utils@^2.3.0, jsx-ast-utils@^2.4.1:
+  version "2.4.1"
+  resolved "https://registry.yarnpkg.com/jsx-ast-utils/-/jsx-ast-utils-2.4.1.tgz#1114a4c1209481db06c690c2b4f488cc665f657e"
+  integrity sha512-z1xSldJ6imESSzOjd3NNkieVJKRlKYSOtMG8SFyCj2FIrvSaSuli/WjpBkEzCBoR9bYYYFgqJw61Xhu7Lcgk+w==
   dependencies:
-    array-includes "^3.0.3"
+    array-includes "^3.1.1"
     object.assign "^4.1.0"
 
 jwt-decode@^2.2.0:
@@ -4089,6 +4900,37 @@ kleur@^3.0.3:
   resolved "https://registry.yarnpkg.com/kleur/-/kleur-3.0.3.tgz#a79c9ecc86ee1ce3fa6206d1216c501f147fc07e"
   integrity sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==
 
+language-subtag-registry@~0.3.2:
+  version "0.3.20"
+  resolved "https://registry.yarnpkg.com/language-subtag-registry/-/language-subtag-registry-0.3.20.tgz#a00a37121894f224f763268e431c55556b0c0755"
+  integrity sha512-KPMwROklF4tEx283Xw0pNKtfTj1gZ4UByp4EsIFWLgBavJltF4TiYPc39k06zSTsLzxTVXXDSpbwaQXaFB4Qeg==
+
+language-tags@^1.0.5:
+  version "1.0.5"
+  resolved "https://registry.yarnpkg.com/language-tags/-/language-tags-1.0.5.tgz#d321dbc4da30ba8bf3024e040fa5c14661f9193a"
+  integrity sha1-0yHbxNowuovzAk4ED6XBRmH5GTo=
+  dependencies:
+    language-subtag-registry "~0.3.2"
+
+latest-version@^3.0.0:
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/latest-version/-/latest-version-3.1.0.tgz#a205383fea322b33b5ae3b18abee0dc2f356ee15"
+  integrity sha1-ogU4P+oyKzO1rjsYq+4NwvNW7hU=
+  dependencies:
+    package-json "^4.0.0"
+
+lazy-property@~1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/lazy-property/-/lazy-property-1.0.0.tgz#84ddc4b370679ba8bd4cdcfa4c06b43d57111147"
+  integrity sha1-hN3Es3Bnm6i9TNz6TAa0PVcREUc=
+
+lcid@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/lcid/-/lcid-1.0.0.tgz#308accafa0bc483a3867b4b6f2b9506251d1b835"
+  integrity sha1-MIrMr6C8SDo4Z7S28rlQYlHRuDU=
+  dependencies:
+    invert-kv "^1.0.0"
+
 lego-api@^1.0.7:
   version "1.0.8"
   resolved "https://registry.yarnpkg.com/lego-api/-/lego-api-1.0.8.tgz#5e26be726c5e11d540f89e7c6b1abf8c5834bd01"
@@ -4096,12 +4938,25 @@ lego-api@^1.0.7:
   dependencies:
     chain-able "^3.0.0"
 
+lemmy-js-client@^1.0.8:
+  version "1.0.8"
+  resolved "https://registry.yarnpkg.com/lemmy-js-client/-/lemmy-js-client-1.0.8.tgz#98e34c8e3cd07427f883f60fad376dc4d6f46e7f"
+  integrity sha512-YZxD3+8RGz7cRKdI8EIe5iQqQIMm5WzdNz6zZ7/CdkMtXUv6YuMOEv8HLTvBoGuaWIJwlMJ+23NIarxlT26IEw==
+
 leven@^3.1.0:
   version "3.1.0"
   resolved "https://registry.yarnpkg.com/leven/-/leven-3.1.0.tgz#77891de834064cccba82ae7842bb6b14a13ed7f2"
   integrity sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==
 
-levn@^0.3.0, levn@~0.3.0:
+levn@^0.4.1:
+  version "0.4.1"
+  resolved "https://registry.yarnpkg.com/levn/-/levn-0.4.1.tgz#ae4562c007473b932a6200d403268dd2fffc6ade"
+  integrity sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==
+  dependencies:
+    prelude-ls "^1.2.1"
+    type-check "~0.4.0"
+
+levn@~0.3.0:
   version "0.3.0"
   resolved "https://registry.yarnpkg.com/levn/-/levn-0.3.0.tgz#3b09924edf9f083c0490fdd4c0bc4421e04764ee"
   integrity sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=
@@ -4109,80 +4964,85 @@ levn@^0.3.0, levn@~0.3.0:
     prelude-ls "~1.1.2"
     type-check "~0.3.2"
 
+libcipm@^1.6.2:
+  version "1.6.3"
+  resolved "https://registry.yarnpkg.com/libcipm/-/libcipm-1.6.3.tgz#dc4052d710941547782d85bbdb3c77eedec733ff"
+  integrity sha512-WUEjQk1aZDECb2MFnAbx6o7sJbBJWrWwt9rbinOmpc0cLKWgYJOvKNqCUN3sl2P9LFqPsnVT4Aj5SPw4/xKI5A==
+  dependencies:
+    bin-links "^1.1.2"
+    bluebird "^3.5.1"
+    find-npm-prefix "^1.0.2"
+    graceful-fs "^4.1.11"
+    lock-verify "^2.0.2"
+    npm-lifecycle "^2.0.3"
+    npm-logical-tree "^1.2.1"
+    npm-package-arg "^6.1.0"
+    pacote "^8.1.6"
+    protoduck "^5.0.0"
+    read-package-json "^2.0.13"
+    rimraf "^2.6.2"
+    worker-farm "^1.6.0"
+
+libnpx@^10.2.0:
+  version "10.2.4"
+  resolved "https://registry.yarnpkg.com/libnpx/-/libnpx-10.2.4.tgz#ef0e3258e29aef2ec7ee3276115e20e67f67d4ee"
+  integrity sha512-BPc0D1cOjBeS8VIBKUu5F80s6njm0wbVt7CsGMrIcJ+SI7pi7V0uVPGpEMH9H5L8csOcclTxAXFE2VAsJXUhfA==
+  dependencies:
+    dotenv "^5.0.1"
+    npm-package-arg "^6.0.0"
+    rimraf "^2.6.2"
+    safe-buffer "^5.1.0"
+    update-notifier "^2.3.0"
+    which "^1.3.0"
+    y18n "^4.0.0"
+    yargs "^14.2.3"
+
 lines-and-columns@^1.1.6:
   version "1.1.6"
   resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.1.6.tgz#1c00c743b433cd0a4e80758f7b64a57440d9ff00"
   integrity sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA=
 
-linkify-it@^2.0.0:
-  version "2.2.0"
-  resolved "https://registry.yarnpkg.com/linkify-it/-/linkify-it-2.2.0.tgz#e3b54697e78bf915c70a38acd78fd09e0058b1cf"
-  integrity sha512-GnAl/knGn+i1U/wjBz3akz2stz+HrHLsxMwHQGofCDfPvlf+gDKN58UtfmUquTY4/MXeE2x7k19KQmeoZi94Iw==
+linkify-it@^3.0.1:
+  version "3.0.2"
+  resolved "https://registry.yarnpkg.com/linkify-it/-/linkify-it-3.0.2.tgz#f55eeb8bc1d3ae754049e124ab3bb56d97797fb8"
+  integrity sha512-gDBO4aHNZS6coiZCKVhSNh43F9ioIL4JwRjLZPkoLIY4yZFwg264Y5lu2x6rb1Js42Gh6Yqm2f6L2AJcnkzinQ==
   dependencies:
     uc.micro "^1.0.1"
 
 lint-staged@^10.1.3:
-  version "10.1.3"
-  resolved "https://registry.yarnpkg.com/lint-staged/-/lint-staged-10.1.3.tgz#da27713d3ac519da305381b4de87d5f866b1d2f1"
-  integrity sha512-o2OkLxgVns5RwSC5QF7waeAjJA5nz5gnUfqL311LkZcFipKV7TztrSlhNUK5nQX9H0E5NELAdduMQ+M/JPT7RQ==
+  version "10.2.11"
+  resolved "https://registry.yarnpkg.com/lint-staged/-/lint-staged-10.2.11.tgz#713c80877f2dc8b609b05bc59020234e766c9720"
+  integrity sha512-LRRrSogzbixYaZItE2APaS4l2eJMjjf5MbclRZpLJtcQJShcvUzKXsNeZgsLIZ0H0+fg2tL4B59fU9wHIHtFIA==
   dependencies:
-    chalk "^3.0.0"
-    commander "^4.0.1"
+    chalk "^4.0.0"
+    cli-truncate "2.1.0"
+    commander "^5.1.0"
     cosmiconfig "^6.0.0"
     debug "^4.1.1"
     dedent "^0.7.0"
-    execa "^3.4.0"
-    listr "^0.14.3"
-    log-symbols "^3.0.0"
+    enquirer "^2.3.5"
+    execa "^4.0.1"
+    listr2 "^2.1.0"
+    log-symbols "^4.0.0"
     micromatch "^4.0.2"
     normalize-path "^3.0.0"
     please-upgrade-node "^3.2.0"
     string-argv "0.3.1"
     stringify-object "^3.3.0"
 
-listr-silent-renderer@^1.1.1:
-  version "1.1.1"
-  resolved "https://registry.yarnpkg.com/listr-silent-renderer/-/listr-silent-renderer-1.1.1.tgz#924b5a3757153770bf1a8e3fbf74b8bbf3f9242e"
-  integrity sha1-kktaN1cVN3C/Go4/v3S4u/P5JC4=
-
-listr-update-renderer@^0.5.0:
-  version "0.5.0"
-  resolved "https://registry.yarnpkg.com/listr-update-renderer/-/listr-update-renderer-0.5.0.tgz#4ea8368548a7b8aecb7e06d8c95cb45ae2ede6a2"
-  integrity sha512-tKRsZpKz8GSGqoI/+caPmfrypiaq+OQCbd+CovEC24uk1h952lVj5sC7SqyFUm+OaJ5HN/a1YLt5cit2FMNsFA==
-  dependencies:
-    chalk "^1.1.3"
-    cli-truncate "^0.2.1"
-    elegant-spinner "^1.0.1"
-    figures "^1.7.0"
-    indent-string "^3.0.0"
-    log-symbols "^1.0.2"
-    log-update "^2.3.0"
-    strip-ansi "^3.0.1"
-
-listr-verbose-renderer@^0.5.0:
-  version "0.5.0"
-  resolved "https://registry.yarnpkg.com/listr-verbose-renderer/-/listr-verbose-renderer-0.5.0.tgz#f1132167535ea4c1261102b9f28dac7cba1e03db"
-  integrity sha512-04PDPqSlsqIOaaaGZ+41vq5FejI9auqTInicFRndCBgE3bXG8D6W1I+mWhk+1nqbHmyhla/6BUrd5OSiHwKRXw==
-  dependencies:
-    chalk "^2.4.1"
-    cli-cursor "^2.1.0"
-    date-fns "^1.27.2"
-    figures "^2.0.0"
-
-listr@^0.14.3:
-  version "0.14.3"
-  resolved "https://registry.yarnpkg.com/listr/-/listr-0.14.3.tgz#2fea909604e434be464c50bddba0d496928fa586"
-  integrity sha512-RmAl7su35BFd/xoMamRjpIE4j3v+L28o8CT5YhAXQJm1fD+1l9ngXY8JAQRJ+tFK2i5njvi0iRUKV09vPwA0iA==
+listr2@^2.1.0:
+  version "2.3.6"
+  resolved "https://registry.yarnpkg.com/listr2/-/listr2-2.3.6.tgz#4248bbbfeb321357aff3587cf3c6dbae4942b83b"
+  integrity sha512-znchYUj4fahw/SxxJ2SOEytA7enDinu0HFeIS+Z+2o8h4sMyl6cUm6J9GBvF7ICwLjQJy7lDcphwYbjEDgJgcQ==
   dependencies:
-    "@samverschueren/stream-to-observable" "^0.3.0"
-    is-observable "^1.1.0"
-    is-promise "^2.1.0"
-    is-stream "^1.1.0"
-    listr-silent-renderer "^1.1.1"
-    listr-update-renderer "^0.5.0"
-    listr-verbose-renderer "^0.5.0"
-    p-map "^2.0.0"
-    rxjs "^6.3.3"
+    chalk "^4.1.0"
+    cli-truncate "^2.1.0"
+    figures "^3.2.0"
+    indent-string "^4.0.0"
+    log-update "^4.0.0"
+    p-map "^4.0.0"
+    rxjs "^6.6.0"
+    through "^2.3.8"
 
 load-json-file@^2.0.0:
   version "2.0.0"
@@ -4202,6 +5062,14 @@ locate-path@^2.0.0:
     p-locate "^2.0.0"
     path-exists "^3.0.0"
 
+locate-path@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-3.0.0.tgz#dbec3b3ab759758071b58fe59fc41871af21400e"
+  integrity sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==
+  dependencies:
+    p-locate "^3.0.0"
+    path-exists "^3.0.0"
+
 locate-path@^5.0.0:
   version "5.0.0"
   resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-5.0.0.tgz#1afba396afd676a6d42504d0a67a3a7eb9f62aa0"
@@ -4209,12 +5077,51 @@ locate-path@^5.0.0:
   dependencies:
     p-locate "^4.1.0"
 
-lodash.get@^4.4.2:
-  version "4.4.2"
-  resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99"
-  integrity sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=
-
-lodash.memoize@4.x:
+lock-verify@^2.0.2:
+  version "2.2.1"
+  resolved "https://registry.yarnpkg.com/lock-verify/-/lock-verify-2.2.1.tgz#81107948c51ed16f97b96ff8b60675affb243fc1"
+  integrity sha512-n0Zw2DVupKfZMazy/HIFVNohJ1z8fIoZ77WBnyyBGG6ixw83uJNyrbiJvvHWe1QKkGiBCjj8RCPlymltliqEww==
+  dependencies:
+    "@iarna/cli" "^1.2.0"
+    npm-package-arg "^6.1.0"
+    semver "^5.4.1"
+
+lockfile@^1.0.4:
+  version "1.0.4"
+  resolved "https://registry.yarnpkg.com/lockfile/-/lockfile-1.0.4.tgz#07f819d25ae48f87e538e6578b6964a4981a5609"
+  integrity sha512-cvbTwETRfsFh4nHsL1eGWapU1XFi5Ot9E85sWAwia7Y7EgB7vfqcZhTKZ+l7hCGxSPoushMv5GKhT5PdLv03WA==
+  dependencies:
+    signal-exit "^3.0.2"
+
+lodash._baseuniq@~4.6.0:
+  version "4.6.0"
+  resolved "https://registry.yarnpkg.com/lodash._baseuniq/-/lodash._baseuniq-4.6.0.tgz#0ebb44e456814af7905c6212fa2c9b2d51b841e8"
+  integrity sha1-DrtE5FaBSveQXGIS+iybLVG4Qeg=
+  dependencies:
+    lodash._createset "~4.0.0"
+    lodash._root "~3.0.0"
+
+lodash._createset@~4.0.0:
+  version "4.0.3"
+  resolved "https://registry.yarnpkg.com/lodash._createset/-/lodash._createset-4.0.3.tgz#0f4659fbb09d75194fa9e2b88a6644d363c9fe26"
+  integrity sha1-D0ZZ+7CddRlPqeK4imZE02PJ/iY=
+
+lodash._root@~3.0.0:
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/lodash._root/-/lodash._root-3.0.1.tgz#fba1c4524c19ee9a5f8136b4609f017cf4ded692"
+  integrity sha1-+6HEUkwZ7ppfgTa0YJ8BfPTe1pI=
+
+lodash.clonedeep@~4.5.0:
+  version "4.5.0"
+  resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef"
+  integrity sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=
+
+lodash.get@^4.4.2:
+  version "4.4.2"
+  resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99"
+  integrity sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=
+
+lodash.memoize@4.x:
   version "4.1.2"
   resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe"
   integrity sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4=
@@ -4224,45 +5131,52 @@ lodash.sortby@^4.7.0:
   resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438"
   integrity sha1-7dFMgk4sycHgsKG0K7UhBRakJDg=
 
+lodash.union@~4.6.0:
+  version "4.6.0"
+  resolved "https://registry.yarnpkg.com/lodash.union/-/lodash.union-4.6.0.tgz#48bb5088409f16f1821666641c44dd1aaae3cd88"
+  integrity sha1-SLtQiECfFvGCFmZkHETdGqrjzYg=
+
+lodash.uniq@~4.5.0:
+  version "4.5.0"
+  resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773"
+  integrity sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=
+
+lodash.without@~4.4.0:
+  version "4.4.0"
+  resolved "https://registry.yarnpkg.com/lodash.without/-/lodash.without-4.4.0.tgz#3cd4574a00b67bae373a94b748772640507b7aac"
+  integrity sha1-PNRXSgC2e643OpS3SHcmQFB7eqw=
+
 lodash.zip@^4.2.0:
   version "4.2.0"
   resolved "https://registry.yarnpkg.com/lodash.zip/-/lodash.zip-4.2.0.tgz#ec6662e4896408ed4ab6c542a3990b72cc080020"
   integrity sha1-7GZi5IlkCO1KtsVCo5kLcswIACA=
 
-lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.3.0:
-  version "4.17.15"
-  resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548"
-  integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==
+lodash@^3.10.1:
+  version "3.10.1"
+  resolved "https://registry.yarnpkg.com/lodash/-/lodash-3.10.1.tgz#5bf45e8e49ba4189e17d482789dfd15bd140b7b6"
+  integrity sha1-W/Rejkm6QYnhfUgnid/RW9FAt7Y=
 
-log-symbols@^1.0.2:
-  version "1.0.2"
-  resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-1.0.2.tgz#376ff7b58ea3086a0f09facc74617eca501e1a18"
-  integrity sha1-N2/3tY6jCGoPCfrMdGF+ylAeGhg=
-  dependencies:
-    chalk "^1.0.0"
-
-log-symbols@^3.0.0:
-  version "3.0.0"
-  resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-3.0.0.tgz#f3a08516a5dea893336a7dee14d18a1cfdab77c4"
-  integrity sha512-dSkNGuI7iG3mfvDzUuYZyvk5dD9ocYCYzNU6CYDE6+Xqd+gwme6Z00NS3dUh8mq/73HaEtT7m6W+yUPtU6BZnQ==
-  dependencies:
-    chalk "^2.4.2"
+lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.3.0:
+  version "4.17.19"
+  resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.19.tgz#e48ddedbe30b3321783c5b4301fbd353bc1e4a4b"
+  integrity sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ==
 
-log-update@^2.3.0:
-  version "2.3.0"
-  resolved "https://registry.yarnpkg.com/log-update/-/log-update-2.3.0.tgz#88328fd7d1ce7938b29283746f0b1bc126b24708"
-  integrity sha1-iDKP19HOeTiykoN0bwsbwSayRwg=
+log-symbols@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-4.0.0.tgz#69b3cc46d20f448eccdb75ea1fa733d9e821c920"
+  integrity sha512-FN8JBzLx6CzeMrB0tg6pqlGU1wCrXW+ZXGH481kfsBqer0hToTIiHdjH4Mq8xJUbvATujKCvaREGWpGUionraA==
   dependencies:
-    ansi-escapes "^3.0.0"
-    cli-cursor "^2.0.0"
-    wrap-ansi "^3.0.1"
+    chalk "^4.0.0"
 
-lolex@^5.0.0:
-  version "5.1.2"
-  resolved "https://registry.yarnpkg.com/lolex/-/lolex-5.1.2.tgz#953694d098ce7c07bc5ed6d0e42bc6c0c6d5a367"
-  integrity sha512-h4hmjAvHTmd+25JSwrtTIuwbKdwg5NzZVRMLn9saij4SZaepCrTCxPr35H/3bjwfMJtN+t3CX8672UIkglz28A==
+log-update@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/log-update/-/log-update-4.0.0.tgz#589ecd352471f2a1c0c570287543a64dfd20e0a1"
+  integrity sha512-9fkkDevMefjg0mmzWFBW8YkFP91OrizzkW3diF7CpG+S2EYdy4+TVfGwz1zeF8x7hCx1ovSPTOE9Ngib74qqUg==
   dependencies:
-    "@sinonjs/commons" "^1.7.0"
+    ansi-escapes "^4.3.0"
+    cli-cursor "^3.1.0"
+    slice-ansi "^4.0.0"
+    wrap-ansi "^6.2.0"
 
 loose-envify@^1.2.0, loose-envify@^1.4.0:
   version "1.4.0"
@@ -4271,6 +5185,33 @@ loose-envify@^1.2.0, loose-envify@^1.4.0:
   dependencies:
     js-tokens "^3.0.0 || ^4.0.0"
 
+lowercase-keys@^1.0.0:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-1.0.1.tgz#6f9e30b47084d971a7c820ff15a6c5167b74c26f"
+  integrity sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA==
+
+lru-cache@^4.0.1, lru-cache@^4.1.1, lru-cache@^4.1.2, lru-cache@^4.1.3:
+  version "4.1.5"
+  resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.5.tgz#8bbe50ea85bed59bc9e33dcab8235ee9bcf443cd"
+  integrity sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==
+  dependencies:
+    pseudomap "^1.0.2"
+    yallist "^2.1.2"
+
+lru-cache@^5.1.1:
+  version "5.1.1"
+  resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920"
+  integrity sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==
+  dependencies:
+    yallist "^3.0.2"
+
+make-dir@^1.0.0:
+  version "1.3.0"
+  resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-1.3.0.tgz#79c1033b80515bd6d24ec9933e860ca75ee27f0c"
+  integrity sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ==
+  dependencies:
+    pify "^3.0.0"
+
 make-dir@^3.0.0:
   version "3.1.0"
   resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-3.1.0.tgz#415e967046b3a7f1d185277d84aa58203726a13f"
@@ -4278,15 +5219,61 @@ make-dir@^3.0.0:
   dependencies:
     semver "^6.0.0"
 
-make-error@1.x:
+make-error@1.x, make-error@^1.1.1:
   version "1.3.6"
   resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2"
   integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==
 
-make-error@^1.1.1:
-  version "1.3.5"
-  resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.5.tgz#efe4e81f6db28cadd605c70f29c831b58ef776c8"
-  integrity sha512-c3sIjNUow0+8swNwVpqoH4YCShKNFkMaw6oH1mNS2haDZQqkeZFlHS3dhoeEbKKmJB4vXpJucU6oH75aDYeE9g==
+"make-fetch-happen@^2.5.0 || 3 || 4", make-fetch-happen@^4.0.1:
+  version "4.0.2"
+  resolved "https://registry.yarnpkg.com/make-fetch-happen/-/make-fetch-happen-4.0.2.tgz#2d156b11696fb32bffbafe1ac1bc085dd6c78a79"
+  integrity sha512-YMJrAjHSb/BordlsDEcVcPyTbiJKkzqMf48N8dAJZT9Zjctrkb6Yg4TY9Sq2AwSIQJFn5qBBKVTYt3vP5FMIHA==
+  dependencies:
+    agentkeepalive "^3.4.1"
+    cacache "^11.3.3"
+    http-cache-semantics "^3.8.1"
+    http-proxy-agent "^2.1.0"
+    https-proxy-agent "^2.2.1"
+    lru-cache "^5.1.1"
+    mississippi "^3.0.0"
+    node-fetch-npm "^2.0.2"
+    promise-retry "^1.1.1"
+    socks-proxy-agent "^4.0.0"
+    ssri "^6.0.0"
+
+make-fetch-happen@^2.6.0:
+  version "2.6.0"
+  resolved "https://registry.yarnpkg.com/make-fetch-happen/-/make-fetch-happen-2.6.0.tgz#8474aa52198f6b1ae4f3094c04e8370d35ea8a38"
+  integrity sha512-FFq0lNI0ax+n9IWzWpH8A4JdgYiAp2DDYIZ3rsaav8JDe8I+72CzK6PQW/oom15YDZpV5bYW/9INd6nIJ2ZfZw==
+  dependencies:
+    agentkeepalive "^3.3.0"
+    cacache "^10.0.0"
+    http-cache-semantics "^3.8.0"
+    http-proxy-agent "^2.0.0"
+    https-proxy-agent "^2.1.0"
+    lru-cache "^4.1.1"
+    mississippi "^1.2.0"
+    node-fetch-npm "^2.0.2"
+    promise-retry "^1.1.1"
+    socks-proxy-agent "^3.0.1"
+    ssri "^5.0.0"
+
+make-fetch-happen@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/make-fetch-happen/-/make-fetch-happen-3.0.0.tgz#7b661d2372fc4710ab5cc8e1fa3c290eea69a961"
+  integrity sha512-FmWY7gC0mL6Z4N86vE14+m719JKE4H0A+pyiOH18B025gF/C113pyfb4gHDDYP5cqnRMHOz06JGdmffC/SES+w==
+  dependencies:
+    agentkeepalive "^3.4.1"
+    cacache "^10.0.4"
+    http-cache-semantics "^3.8.1"
+    http-proxy-agent "^2.1.0"
+    https-proxy-agent "^2.2.0"
+    lru-cache "^4.1.2"
+    mississippi "^3.0.0"
+    node-fetch-npm "^2.0.2"
+    promise-retry "^1.1.1"
+    socks-proxy-agent "^3.0.1"
+    ssri "^5.2.4"
 
 makeerror@1.0.x:
   version "1.0.11"
@@ -4307,24 +5294,34 @@ map-visit@^1.0.0:
   dependencies:
     object-visit "^1.0.0"
 
-markdown-it-container@^2.0.0:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/markdown-it-container/-/markdown-it-container-2.0.0.tgz#0019b43fd02eefece2f1960a2895fba81a404695"
-  integrity sha1-ABm0P9Au7+zi8ZYKKJX7qBpARpU=
+markdown-it-container@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/markdown-it-container/-/markdown-it-container-3.0.0.tgz#1d19b06040a020f9a827577bb7dbf67aa5de9a5b"
+  integrity sha512-y6oKTq4BB9OQuY/KLfk/O3ysFhB3IMYoIWhGJEidXt1NQFocFK2sA2t0NYZAMyMShAGL6x5OPIbrmXPIqaN9rw==
 
 markdown-it-emoji@^1.4.0:
   version "1.4.0"
   resolved "https://registry.yarnpkg.com/markdown-it-emoji/-/markdown-it-emoji-1.4.0.tgz#9bee0e9a990a963ba96df6980c4fddb05dfb4dcc"
   integrity sha1-m+4OmpkKljupbfaYDE/dsF37Tcw=
 
-markdown-it@^10.0.0:
-  version "10.0.0"
-  resolved "https://registry.yarnpkg.com/markdown-it/-/markdown-it-10.0.0.tgz#abfc64f141b1722d663402044e43927f1f50a8dc"
-  integrity sha512-YWOP1j7UbDNz+TumYP1kpwnP0aEa711cJjrAQrzd0UXlbJfc5aAq0F/PZHjiioqDC1NKgvIMX+o+9Bk7yuM2dg==
+markdown-it-sub@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/markdown-it-sub/-/markdown-it-sub-1.0.0.tgz#375fd6026eae7ddcb012497f6411195ea1e3afe8"
+  integrity sha1-N1/WAm6ufdywEkl/ZBEZXqHjr+g=
+
+markdown-it-sup@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/markdown-it-sup/-/markdown-it-sup-1.0.0.tgz#cb9c9ff91a5255ac08f3fd3d63286e15df0a1fc3"
+  integrity sha1-y5yf+RpSVawI8/09YyhuFd8KH8M=
+
+markdown-it@^11.0.0:
+  version "11.0.0"
+  resolved "https://registry.yarnpkg.com/markdown-it/-/markdown-it-11.0.0.tgz#dbfc30363e43d756ebc52c38586b91b90046b876"
+  integrity sha512-+CvOnmbSubmQFSA9dKz1BRiaSMV7rhexl3sngKqFyXSagoA3fBdJQ8oZWtRy2knXdpDXaBw44euz37DeJQ9asg==
   dependencies:
     argparse "^1.0.7"
     entities "~2.0.0"
-    linkify-it "^2.0.0"
+    linkify-it "^3.0.1"
     mdurl "^1.0.1"
     uc.micro "^1.0.5"
 
@@ -4338,11 +5335,23 @@ mdurl@^1.0.1:
   resolved "https://registry.yarnpkg.com/mdurl/-/mdurl-1.0.1.tgz#fe85b2ec75a59037f2adfec100fd6c601761152e"
   integrity sha1-/oWy7HWlkDfyrf7BAP1sYBdhFS4=
 
+meant@~1.0.1:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/meant/-/meant-1.0.2.tgz#5d0c78310a3d8ae1408a16be0fe0bd42a969f560"
+  integrity sha512-KN+1uowN/NK+sT/Lzx7WSGIj2u+3xe5n2LbwObfjOhPZiA+cCfCm6idVl0RkEfjThkw5XJ96CyRcanq6GmKtUg==
+
 media-typer@0.3.0:
   version "0.3.0"
   resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748"
   integrity sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=
 
+mem@^1.1.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/mem/-/mem-1.1.0.tgz#5edd52b485ca1d900fe64895505399a0dfa45f76"
+  integrity sha1-Xt1StIXKHZAP5kiVUFOZoN+kX3Y=
+  dependencies:
+    mimic-fn "^1.0.0"
+
 merge-descriptors@1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61"
@@ -4363,14 +5372,6 @@ methods@~1.1.2:
   resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee"
   integrity sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=
 
-micromatch@4.x, micromatch@^4.0.2:
-  version "4.0.2"
-  resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.2.tgz#4fcb0999bf9fbc2fcbdd212f6d629b9a56c39259"
-  integrity sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q==
-  dependencies:
-    braces "^3.0.1"
-    picomatch "^2.0.5"
-
 micromatch@^2.1.5:
   version "2.3.11"
   resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-2.3.11.tgz#86677c97d1720b363431d04d0d15293bd38c1565"
@@ -4409,17 +5410,25 @@ micromatch@^3.1.10, micromatch@^3.1.4:
     snapdragon "^0.8.1"
     to-regex "^3.0.2"
 
-mime-db@1.43.0:
-  version "1.43.0"
-  resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.43.0.tgz#0a12e0502650e473d735535050e7c8f4eb4fae58"
-  integrity sha512-+5dsGEEovYbT8UY9yD7eE4XTc4UwJ1jBYlgaQQF38ENsKR3wj/8q8RFZrF9WIZpB2V1ArTVFUva8sAul1NzRzQ==
+micromatch@^4.0.2:
+  version "4.0.2"
+  resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.2.tgz#4fcb0999bf9fbc2fcbdd212f6d629b9a56c39259"
+  integrity sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q==
+  dependencies:
+    braces "^3.0.1"
+    picomatch "^2.0.5"
+
+mime-db@1.44.0:
+  version "1.44.0"
+  resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.44.0.tgz#fa11c5eb0aca1334b4233cb4d52f10c5a6272f92"
+  integrity sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg==
 
 mime-types@^2.1.12, mime-types@~2.1.19, mime-types@~2.1.24:
-  version "2.1.26"
-  resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.26.tgz#9c921fc09b7e149a65dfdc0da4d20997200b0a06"
-  integrity sha512-01paPWYgLrkqAyrlDorC1uDwl2p3qZT7yl806vW7DvDoxwXi46jsjFbg+WdwotBIk6/MbEhO/dh5aZ5sNj/dWQ==
+  version "2.1.27"
+  resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.27.tgz#47949f98e279ea53119f5722e0f34e529bec009f"
+  integrity sha512-JIhqnCasI9yD+SsmkquHBxTSEuZdQX5BuQnS2Vc7puQQQ+8yiP5AY5uWhpdv4YL4VM5c6iliiYWPgJ/nJQLp7w==
   dependencies:
-    mime-db "1.43.0"
+    mime-db "1.44.0"
 
 mime@1.6.0:
   version "1.6.0"
@@ -4443,20 +5452,73 @@ minimatch@^3.0.4:
   dependencies:
     brace-expansion "^1.1.7"
 
-minimist@0.0.8:
-  version "0.0.8"
-  resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d"
-  integrity sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=
-
-minimist@^1.1.1, minimist@^1.2.5:
+minimist@^1.1.1, minimist@^1.2.0, minimist@^1.2.5:
   version "1.2.5"
   resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602"
   integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==
 
-minimist@^1.2.0:
-  version "1.2.0"
-  resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284"
-  integrity sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=
+minipass@^2.3.3, minipass@^2.6.0, minipass@^2.8.6, minipass@^2.9.0:
+  version "2.9.0"
+  resolved "https://registry.yarnpkg.com/minipass/-/minipass-2.9.0.tgz#e713762e7d3e32fed803115cf93e04bca9fcc9a6"
+  integrity sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg==
+  dependencies:
+    safe-buffer "^5.1.2"
+    yallist "^3.0.0"
+
+minizlib@^1.2.1:
+  version "1.3.3"
+  resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-1.3.3.tgz#2290de96818a34c29551c8a8d301216bd65a861d"
+  integrity sha512-6ZYMOEnmVsdCeTJVE0W9ZD+pVnE8h9Hma/iOwwRDsdQoePpoX56/8B6z3P9VNwppJuBKNRuFDRNRqRWexT9G9Q==
+  dependencies:
+    minipass "^2.9.0"
+
+mississippi@^1.2.0:
+  version "1.3.1"
+  resolved "https://registry.yarnpkg.com/mississippi/-/mississippi-1.3.1.tgz#2a8bb465e86550ac8b36a7b6f45599171d78671e"
+  integrity sha512-/6rB8YXFbAtsUVRphIRQqB0+9c7VaPHCjVtvto+JqwVxgz8Zz+I+f68/JgQ+Pb4VlZb2svA9OtdXnHHsZz7ltg==
+  dependencies:
+    concat-stream "^1.5.0"
+    duplexify "^3.4.2"
+    end-of-stream "^1.1.0"
+    flush-write-stream "^1.0.0"
+    from2 "^2.1.0"
+    parallel-transform "^1.1.0"
+    pump "^1.0.0"
+    pumpify "^1.3.3"
+    stream-each "^1.1.0"
+    through2 "^2.0.0"
+
+mississippi@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/mississippi/-/mississippi-2.0.0.tgz#3442a508fafc28500486feea99409676e4ee5a6f"
+  integrity sha512-zHo8v+otD1J10j/tC+VNoGK9keCuByhKovAvdn74dmxJl9+mWHnx6EMsDN4lgRoMI/eYo2nchAxniIbUPb5onw==
+  dependencies:
+    concat-stream "^1.5.0"
+    duplexify "^3.4.2"
+    end-of-stream "^1.1.0"
+    flush-write-stream "^1.0.0"
+    from2 "^2.1.0"
+    parallel-transform "^1.1.0"
+    pump "^2.0.1"
+    pumpify "^1.3.3"
+    stream-each "^1.1.0"
+    through2 "^2.0.0"
+
+mississippi@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/mississippi/-/mississippi-3.0.0.tgz#ea0a3291f97e0b5e8776b363d5f0a12d94c67022"
+  integrity sha512-x471SsVjUtBRtcvd4BzKE9kFC+/2TeWgKCgw0bZcw1b9l2X3QX5vCWgF+KaZaYm87Ss//rHnWryupDrgLvmSkA==
+  dependencies:
+    concat-stream "^1.5.0"
+    duplexify "^3.4.2"
+    end-of-stream "^1.1.0"
+    flush-write-stream "^1.0.0"
+    from2 "^2.1.0"
+    parallel-transform "^1.1.0"
+    pump "^3.0.0"
+    pumpify "^1.3.3"
+    stream-each "^1.1.0"
+    through2 "^2.0.0"
 
 mixin-deep@^1.2.0:
   version "1.3.2"
@@ -4471,17 +5533,29 @@ mkdirp@1.x:
   resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e"
   integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==
 
-mkdirp@^0.5.1:
-  version "0.5.1"
-  resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903"
-  integrity sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=
+"mkdirp@>=0.5 0", mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@~0.5.0, mkdirp@~0.5.1:
+  version "0.5.5"
+  resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.5.tgz#d91cefd62d1436ca0f41620e251288d420099def"
+  integrity sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==
   dependencies:
-    minimist "0.0.8"
+    minimist "^1.2.5"
 
 moment@^2.24.0:
-  version "2.24.0"
-  resolved "https://registry.yarnpkg.com/moment/-/moment-2.24.0.tgz#0d055d53f5052aa653c9f6eb68bb5d12bf5c2b5b"
-  integrity sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg==
+  version "2.27.0"
+  resolved "https://registry.yarnpkg.com/moment/-/moment-2.27.0.tgz#8bff4e3e26a236220dfe3e36de756b6ebaa0105d"
+  integrity sha512-al0MUK7cpIcglMv3YF13qSgdAIqxHTO7brRtaz3DlSULbqfazqkc5kEjNrLDOM7fsjshoFIihnU8snrP7zUvhQ==
+
+move-concurrently@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/move-concurrently/-/move-concurrently-1.0.1.tgz#be2c005fda32e0b29af1f05d7c4b33214c701f92"
+  integrity sha1-viwAX9oy4LKa8fBdfEszIUxwH5I=
+  dependencies:
+    aproba "^1.1.1"
+    copy-concurrently "^1.0.0"
+    fs-write-stream-atomic "^1.0.8"
+    mkdirp "^0.5.1"
+    rimraf "^2.5.4"
+    run-queue "^1.0.3"
 
 ms@2.0.0:
   version "2.0.0"
@@ -4493,12 +5567,12 @@ ms@2.1.1:
   resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.1.tgz#30a5864eb3ebb0a66f2ebe6d727af06a09d86e0a"
   integrity sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==
 
-ms@^2.1.1:
+ms@^2.0.0, ms@^2.1.1:
   version "2.1.2"
   resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009"
   integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==
 
-multimap@^1.0.2:
+multimap@^1.1.0:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/multimap/-/multimap-1.1.0.tgz#5263febc085a1791c33b59bb3afc6a76a2a10ca8"
   integrity sha512-0ZIR9PasPxGXmRsEF8jsDzndzHDj7tIav+JUmvIFB/WHswliFnquxECT/De7GR4yg99ky/NlRKJT82G1y271bw==
@@ -4513,15 +5587,15 @@ mute-stream@0.0.7:
   resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.7.tgz#3075ce93bc21b8fab43e1bc4da7e8115ed1e7bab"
   integrity sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s=
 
-mute-stream@0.0.8:
+mute-stream@~0.0.4:
   version "0.0.8"
   resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.8.tgz#1630c42b2251ff81e2a283de96a5497ea92e5e0d"
   integrity sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==
 
 nan@^2.12.1:
-  version "2.14.0"
-  resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.0.tgz#7818f722027b2459a86f0295d434d1fc2336c52c"
-  integrity sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg==
+  version "2.14.1"
+  resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.1.tgz#d7be34dfa3105b91494c3147089315eff8874b01"
+  integrity sha512-isWHgVjnFjh2x2yuJ/tj3JbwoHu3UC2dX5G/88Cm24yB6YopVgxvBObDY7n5xW6ExmFhJpSEQqFPvq9zaXc8Jw==
 
 nanomatch@^1.2.9:
   version "1.2.13"
@@ -4560,11 +5634,55 @@ nice-try@^1.0.4:
   resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366"
   integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==
 
+node-fetch-npm@^2.0.2:
+  version "2.0.4"
+  resolved "https://registry.yarnpkg.com/node-fetch-npm/-/node-fetch-npm-2.0.4.tgz#6507d0e17a9ec0be3bec516958a497cec54bf5a4"
+  integrity sha512-iOuIQDWDyjhv9qSDrj9aq/klt6F9z1p2otB3AV7v3zBDcL/x+OfGsvGQZZCcMZbUf4Ujw1xGNQkjvGnVT22cKg==
+  dependencies:
+    encoding "^0.1.11"
+    json-parse-better-errors "^1.0.0"
+    safe-buffer "^5.1.1"
+
 node-fetch@^2.6.0:
   version "2.6.0"
   resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.0.tgz#e633456386d4aa55863f676a7ab0daa8fdecb0fd"
   integrity sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA==
 
+node-gyp@^3.6.2:
+  version "3.8.0"
+  resolved "https://registry.yarnpkg.com/node-gyp/-/node-gyp-3.8.0.tgz#540304261c330e80d0d5edce253a68cb3964218c"
+  integrity sha512-3g8lYefrRRzvGeSowdJKAKyks8oUpLEd/DyPV4eMhVlhJ0aNaZqIrNUIPuEWWTAoPqyFkfGrM67MC69baqn6vA==
+  dependencies:
+    fstream "^1.0.0"
+    glob "^7.0.3"
+    graceful-fs "^4.1.2"
+    mkdirp "^0.5.0"
+    nopt "2 || 3"
+    npmlog "0 || 1 || 2 || 3 || 4"
+    osenv "0"
+    request "^2.87.0"
+    rimraf "2"
+    semver "~5.3.0"
+    tar "^2.0.0"
+    which "1"
+
+node-gyp@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/node-gyp/-/node-gyp-4.0.0.tgz#972654af4e5dd0cd2a19081b4b46fe0442ba6f45"
+  integrity sha512-2XiryJ8sICNo6ej8d0idXDEMKfVfFK7kekGCtJAuelGsYHQxhj13KTf95swTCN2dZ/4lTfZ84Fu31jqJEEgjWA==
+  dependencies:
+    glob "^7.0.3"
+    graceful-fs "^4.1.2"
+    mkdirp "^0.5.0"
+    nopt "2 || 3"
+    npmlog "0 || 1 || 2 || 3 || 4"
+    osenv "0"
+    request "^2.87.0"
+    rimraf "2"
+    semver "~5.3.0"
+    tar "^4.4.8"
+    which "1"
+
 node-int64@^0.4.0:
   version "0.4.0"
   resolved "https://registry.yarnpkg.com/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b"
@@ -4575,18 +5693,34 @@ node-modules-regexp@^1.0.0:
   resolved "https://registry.yarnpkg.com/node-modules-regexp/-/node-modules-regexp-1.0.0.tgz#8d9dbe28964a4ac5712e9131642107c71e90ec40"
   integrity sha1-jZ2+KJZKSsVxLpExZCEHxx6Q7EA=
 
-node-notifier@^6.0.0:
-  version "6.0.0"
-  resolved "https://registry.yarnpkg.com/node-notifier/-/node-notifier-6.0.0.tgz#cea319e06baa16deec8ce5cd7f133c4a46b68e12"
-  integrity sha512-SVfQ/wMw+DesunOm5cKqr6yDcvUTDl/yc97ybGHMrteNEY6oekXpNpS3lZwgLlwz0FLgHoiW28ZpmBHUDg37cw==
+node-notifier@^7.0.0:
+  version "7.0.2"
+  resolved "https://registry.yarnpkg.com/node-notifier/-/node-notifier-7.0.2.tgz#3a70b1b70aca5e919d0b1b022530697466d9c675"
+  integrity sha512-ux+n4hPVETuTL8+daJXTOC6uKLgMsl1RYfFv7DKRzyvzBapqco0rZZ9g72ZN8VS6V+gvNYHYa/ofcCY8fkJWsA==
   dependencies:
     growly "^1.3.0"
-    is-wsl "^2.1.1"
-    semver "^6.3.0"
+    is-wsl "^2.2.0"
+    semver "^7.3.2"
     shellwords "^0.1.1"
-    which "^1.3.1"
+    uuid "^8.2.0"
+    which "^2.0.2"
 
-normalize-package-data@^2.3.2, normalize-package-data@^2.5.0:
+"nopt@2 || 3":
+  version "3.0.6"
+  resolved "https://registry.yarnpkg.com/nopt/-/nopt-3.0.6.tgz#c6465dbf08abcd4db359317f79ac68a646b28ff9"
+  integrity sha1-xkZdvwirzU2zWTF/eaxopkayj/k=
+  dependencies:
+    abbrev "1"
+
+nopt@~4.0.1:
+  version "4.0.3"
+  resolved "https://registry.yarnpkg.com/nopt/-/nopt-4.0.3.tgz#a375cad9d02fd921278d954c2254d5aa57e15e48"
+  integrity sha512-CvaGwVMztSMJLOeXPrez7fyfObdZqNUK1cPAEzLHrTybIua9pMdmmPR5YwtfNftIOMv3DPUhFaxsZMNTQO20Kg==
+  dependencies:
+    abbrev "1"
+    osenv "^0.1.4"
+
+normalize-package-data@^2.0.0, normalize-package-data@^2.3.2, normalize-package-data@^2.4.0, normalize-package-data@^2.5.0, "normalize-package-data@~1.0.1 || ^2.0.0":
   version "2.5.0"
   resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.5.0.tgz#e66db1838b200c1dfc233225d12cb36520e234a8"
   integrity sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==
@@ -4596,6 +5730,16 @@ normalize-package-data@^2.3.2, normalize-package-data@^2.5.0:
     semver "2 || 3 || 4 || 5"
     validate-npm-package-license "^3.0.1"
 
+normalize-package-data@~2.4.0:
+  version "2.4.2"
+  resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.4.2.tgz#6b2abd85774e51f7936f1395e45acb905dc849b2"
+  integrity sha512-YcMnjqeoUckXTPKZSAsPjUPLxH85XotbpqK3w4RyCwdFQSU5FxxBys8buehkSfg0j9fKvV1hn7O0+8reEgkAiw==
+  dependencies:
+    hosted-git-info "^2.1.4"
+    is-builtin-module "^1.0.0"
+    semver "2 || 3 || 4 || 5"
+    validate-npm-package-license "^3.0.1"
+
 normalize-path@^2.0.0, normalize-path@^2.0.1, normalize-path@^2.1.1:
   version "2.1.1"
   resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-2.1.1.tgz#1ab28b556e198363a8c1a6f7e6fa20137fe6aed9"
@@ -4608,6 +5752,132 @@ normalize-path@^3.0.0:
   resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65"
   integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==
 
+npm-audit-report@^1.0.9:
+  version "1.3.3"
+  resolved "https://registry.yarnpkg.com/npm-audit-report/-/npm-audit-report-1.3.3.tgz#8226deeb253b55176ed147592a3995442f2179ed"
+  integrity sha512-8nH/JjsFfAWMvn474HB9mpmMjrnKb1Hx/oTAdjv4PT9iZBvBxiZ+wtDUapHCJwLqYGQVPaAfs+vL5+5k9QndXw==
+  dependencies:
+    cli-table3 "^0.5.0"
+    console-control-strings "^1.1.0"
+
+npm-bundled@^1.0.1:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/npm-bundled/-/npm-bundled-1.1.1.tgz#1edd570865a94cdb1bc8220775e29466c9fb234b"
+  integrity sha512-gqkfgGePhTpAEgUsGEgcq1rqPXA+tv/aVBlgEzfXwA1yiUJF7xtEt3CtVwOjNYQOVknDk0F20w58Fnm3EtG0fA==
+  dependencies:
+    npm-normalize-package-bin "^1.0.1"
+
+npm-cache-filename@~1.0.2:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/npm-cache-filename/-/npm-cache-filename-1.0.2.tgz#ded306c5b0bfc870a9e9faf823bc5f283e05ae11"
+  integrity sha1-3tMGxbC/yHCp6fr4I7xfKD4FrhE=
+
+npm-install-checks@~3.0.0:
+  version "3.0.2"
+  resolved "https://registry.yarnpkg.com/npm-install-checks/-/npm-install-checks-3.0.2.tgz#ab2e32ad27baa46720706908e5b14c1852de44d9"
+  integrity sha512-E4kzkyZDIWoin6uT5howP8VDvkM+E8IQDcHAycaAxMbwkqhIg5eEYALnXOl3Hq9MrkdQB/2/g1xwBINXdKSRkg==
+  dependencies:
+    semver "^2.3.0 || 3.x || 4 || 5"
+
+npm-lifecycle@^2.0.1, npm-lifecycle@^2.0.3:
+  version "2.1.1"
+  resolved "https://registry.yarnpkg.com/npm-lifecycle/-/npm-lifecycle-2.1.1.tgz#0027c09646f0fd346c5c93377bdaba59c6748fdf"
+  integrity sha512-+Vg6I60Z75V/09pdcH5iUo/99Q/vop35PaI99elvxk56azSVVsdsSsS/sXqKDNwbRRNN1qSxkcO45ZOu0yOWew==
+  dependencies:
+    byline "^5.0.0"
+    graceful-fs "^4.1.15"
+    node-gyp "^4.0.0"
+    resolve-from "^4.0.0"
+    slide "^1.1.6"
+    uid-number "0.0.6"
+    umask "^1.1.0"
+    which "^1.3.1"
+
+npm-logical-tree@^1.2.1:
+  version "1.2.1"
+  resolved "https://registry.yarnpkg.com/npm-logical-tree/-/npm-logical-tree-1.2.1.tgz#44610141ca24664cad35d1e607176193fd8f5b88"
+  integrity sha512-AJI/qxDB2PWI4LG1CYN579AY1vCiNyWfkiquCsJWqntRu/WwimVrC8yXeILBFHDwxfOejxewlmnvW9XXjMlYIg==
+
+npm-normalize-package-bin@^1.0.0, npm-normalize-package-bin@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/npm-normalize-package-bin/-/npm-normalize-package-bin-1.0.1.tgz#6e79a41f23fd235c0623218228da7d9c23b8f6e2"
+  integrity sha512-EPfafl6JL5/rU+ot6P3gRSCpPDW5VmIzX959Ob1+ySFUuuYHWHekXpwdUZcKP5C+DS4GEtdJluwBjnsNDl+fSA==
+
+"npm-package-arg@^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0", "npm-package-arg@^4.0.0 || ^5.0.0 || ^6.0.0", npm-package-arg@^6.0.0, npm-package-arg@^6.1.0:
+  version "6.1.1"
+  resolved "https://registry.yarnpkg.com/npm-package-arg/-/npm-package-arg-6.1.1.tgz#02168cb0a49a2b75bf988a28698de7b529df5cb7"
+  integrity sha512-qBpssaL3IOZWi5vEKUKW0cO7kzLeT+EQO9W8RsLOZf76KF9E/K9+wH0C7t06HXPpaH8WH5xF1MExLuCwbTqRUg==
+  dependencies:
+    hosted-git-info "^2.7.1"
+    osenv "^0.1.5"
+    semver "^5.6.0"
+    validate-npm-package-name "^3.0.0"
+
+npm-packlist@^1.1.10:
+  version "1.4.8"
+  resolved "https://registry.yarnpkg.com/npm-packlist/-/npm-packlist-1.4.8.tgz#56ee6cc135b9f98ad3d51c1c95da22bbb9b2ef3e"
+  integrity sha512-5+AZgwru5IevF5ZdnFglB5wNlHG1AOOuw28WhUq8/8emhBmLv6jX5by4WJCh7lW0uSYZYS6DXqIsyZVIXRZU9A==
+  dependencies:
+    ignore-walk "^3.0.1"
+    npm-bundled "^1.0.1"
+    npm-normalize-package-bin "^1.0.1"
+
+npm-packlist@~1.1.10:
+  version "1.1.12"
+  resolved "https://registry.yarnpkg.com/npm-packlist/-/npm-packlist-1.1.12.tgz#22bde2ebc12e72ca482abd67afc51eb49377243a"
+  integrity sha512-WJKFOVMeAlsU/pjXuqVdzU0WfgtIBCupkEVwn+1Y0ERAbUfWw8R4GjgVbaKnUjRoD2FoQbHOCbOyT5Mbs9Lw4g==
+  dependencies:
+    ignore-walk "^3.0.1"
+    npm-bundled "^1.0.1"
+
+npm-pick-manifest@^2.1.0:
+  version "2.2.3"
+  resolved "https://registry.yarnpkg.com/npm-pick-manifest/-/npm-pick-manifest-2.2.3.tgz#32111d2a9562638bb2c8f2bf27f7f3092c8fae40"
+  integrity sha512-+IluBC5K201+gRU85vFlUwX3PFShZAbAgDNp2ewJdWMVSppdo/Zih0ul2Ecky/X7b51J7LrrUAP+XOmOCvYZqA==
+  dependencies:
+    figgy-pudding "^3.5.1"
+    npm-package-arg "^6.0.0"
+    semver "^5.4.1"
+
+npm-profile@^3.0.1:
+  version "3.0.2"
+  resolved "https://registry.yarnpkg.com/npm-profile/-/npm-profile-3.0.2.tgz#58d568f1b56ef769602fd0aed8c43fa0e0de0f57"
+  integrity sha512-rEJOFR6PbwOvvhGa2YTNOJQKNuc6RovJ6T50xPU7pS9h/zKPNCJ+VHZY2OFXyZvEi+UQYtHRTp8O/YM3tUD20A==
+  dependencies:
+    aproba "^1.1.2 || 2"
+    make-fetch-happen "^2.5.0 || 3 || 4"
+
+npm-registry-client@^8.5.1:
+  version "8.6.0"
+  resolved "https://registry.yarnpkg.com/npm-registry-client/-/npm-registry-client-8.6.0.tgz#7f1529f91450732e89f8518e0f21459deea3e4c4"
+  integrity sha512-Qs6P6nnopig+Y8gbzpeN/dkt+n7IyVd8f45NTMotGk6Qo7GfBmzwYx6jRLoOOgKiMnaQfYxsuyQlD8Mc3guBhg==
+  dependencies:
+    concat-stream "^1.5.2"
+    graceful-fs "^4.1.6"
+    normalize-package-data "~1.0.1 || ^2.0.0"
+    npm-package-arg "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0"
+    once "^1.3.3"
+    request "^2.74.0"
+    retry "^0.10.0"
+    safe-buffer "^5.1.1"
+    semver "2 >=2.2.1 || 3.x || 4 || 5"
+    slide "^1.1.3"
+    ssri "^5.2.4"
+  optionalDependencies:
+    npmlog "2 || ^3.1.0 || ^4.0.0"
+
+npm-registry-fetch@^1.1.0:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/npm-registry-fetch/-/npm-registry-fetch-1.1.1.tgz#710bc5947d9ee2c549375072dab6d5d17baf2eb2"
+  integrity sha512-ev+zxOXsgAqRsR8Rk+ErjgWOlbrXcqGdme94/VNdjDo1q8TSy10Pp8xgDv/ZmMk2jG/KvGtXUNG4GS3+l6xbDw==
+  dependencies:
+    bluebird "^3.5.1"
+    figgy-pudding "^3.0.0"
+    lru-cache "^4.1.2"
+    make-fetch-happen "^3.0.0"
+    npm-package-arg "^6.0.0"
+    safe-buffer "^5.1.1"
+
 npm-run-path@^2.0.0:
   version "2.0.2"
   resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-2.0.2.tgz#35a9232dfa35d7067b4cb2ddf2357b1871536c5f"
@@ -4622,6 +5892,134 @@ npm-run-path@^4.0.0:
   dependencies:
     path-key "^3.0.0"
 
+npm-user-validate@~1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/npm-user-validate/-/npm-user-validate-1.0.0.tgz#8ceca0f5cea04d4e93519ef72d0557a75122e951"
+  integrity sha1-jOyg9c6gTU6TUZ73LQVXp1Ei6VE=
+
+npm@^5.8.0:
+  version "5.10.0"
+  resolved "https://registry.yarnpkg.com/npm/-/npm-5.10.0.tgz#3bec62312c94a9b0f48f208e00b98bf0304b40db"
+  integrity sha512-lvjvjgR5wG2RJ2uqak1xtZcVAWMwVOzN5HkUlUj/n8rU1f3A0fNn+7HwOzH9Lyf0Ppyu9ApgsEpHczOSnx1cwA==
+  dependencies:
+    JSONStream "^1.3.2"
+    abbrev "~1.1.1"
+    ansi-regex "~3.0.0"
+    ansicolors "~0.3.2"
+    ansistyles "~0.1.3"
+    aproba "~1.2.0"
+    archy "~1.0.0"
+    bin-links "^1.1.0"
+    bluebird "~3.5.1"
+    byte-size "^4.0.2"
+    cacache "^10.0.4"
+    call-limit "~1.1.0"
+    chownr "~1.0.1"
+    cli-columns "^3.1.2"
+    cli-table2 "~0.2.0"
+    cmd-shim "~2.0.2"
+    columnify "~1.5.4"
+    config-chain "~1.1.11"
+    detect-indent "~5.0.0"
+    detect-newline "^2.1.0"
+    dezalgo "~1.0.3"
+    editor "~1.0.0"
+    find-npm-prefix "^1.0.2"
+    fs-vacuum "~1.2.10"
+    fs-write-stream-atomic "~1.0.10"
+    gentle-fs "^2.0.1"
+    glob "~7.1.2"
+    graceful-fs "~4.1.11"
+    has-unicode "~2.0.1"
+    hosted-git-info "^2.6.0"
+    iferr "~0.1.5"
+    inflight "~1.0.6"
+    inherits "~2.0.3"
+    ini "^1.3.5"
+    init-package-json "^1.10.3"
+    is-cidr "~1.0.0"
+    json-parse-better-errors "^1.0.2"
+    lazy-property "~1.0.0"
+    libcipm "^1.6.2"
+    libnpx "^10.2.0"
+    lock-verify "^2.0.2"
+    lockfile "^1.0.4"
+    lodash._baseuniq "~4.6.0"
+    lodash.clonedeep "~4.5.0"
+    lodash.union "~4.6.0"
+    lodash.uniq "~4.5.0"
+    lodash.without "~4.4.0"
+    lru-cache "^4.1.2"
+    meant "~1.0.1"
+    mississippi "^3.0.0"
+    mkdirp "~0.5.1"
+    move-concurrently "^1.0.1"
+    node-gyp "^3.6.2"
+    nopt "~4.0.1"
+    normalize-package-data "~2.4.0"
+    npm-audit-report "^1.0.9"
+    npm-cache-filename "~1.0.2"
+    npm-install-checks "~3.0.0"
+    npm-lifecycle "^2.0.1"
+    npm-package-arg "^6.1.0"
+    npm-packlist "~1.1.10"
+    npm-profile "^3.0.1"
+    npm-registry-client "^8.5.1"
+    npm-registry-fetch "^1.1.0"
+    npm-user-validate "~1.0.0"
+    npmlog "~4.1.2"
+    once "~1.4.0"
+    opener "~1.4.3"
+    osenv "^0.1.5"
+    pacote "^7.6.1"
+    path-is-inside "~1.0.2"
+    promise-inflight "~1.0.1"
+    qrcode-terminal "^0.12.0"
+    query-string "^6.1.0"
+    qw "~1.0.1"
+    read "~1.0.7"
+    read-cmd-shim "~1.0.1"
+    read-installed "~4.0.3"
+    read-package-json "^2.0.13"
+    read-package-tree "^5.2.1"
+    readable-stream "^2.3.6"
+    request "^2.85.0"
+    retry "^0.12.0"
+    rimraf "~2.6.2"
+    safe-buffer "^5.1.2"
+    semver "^5.5.0"
+    sha "~2.0.1"
+    slide "~1.1.6"
+    sorted-object "~2.0.1"
+    sorted-union-stream "~2.1.3"
+    ssri "^5.3.0"
+    strip-ansi "~4.0.0"
+    tar "^4.4.2"
+    text-table "~0.2.0"
+    tiny-relative-date "^1.3.0"
+    uid-number "0.0.6"
+    umask "~1.1.0"
+    unique-filename "~1.1.0"
+    unpipe "~1.0.0"
+    update-notifier "^2.5.0"
+    uuid "^3.2.1"
+    validate-npm-package-license "^3.0.3"
+    validate-npm-package-name "~3.0.0"
+    which "~1.3.0"
+    worker-farm "^1.6.0"
+    wrappy "~1.0.2"
+    write-file-atomic "^2.3.0"
+
+"npmlog@0 || 1 || 2 || 3 || 4", "npmlog@2 || ^3.1.0 || ^4.0.0", npmlog@~4.1.2:
+  version "4.1.2"
+  resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-4.1.2.tgz#08a7f2a8bf734604779a9efa4ad5cc717abb954b"
+  integrity sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==
+  dependencies:
+    are-we-there-yet "~1.1.2"
+    console-control-strings "~1.1.0"
+    gauge "~2.7.3"
+    set-blocking "~2.0.0"
+
 number-is-nan@^1.0.0:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d"
@@ -4652,9 +6050,17 @@ object-copy@^0.1.0:
     kind-of "^3.0.3"
 
 object-inspect@^1.7.0:
-  version "1.7.0"
-  resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.7.0.tgz#f4f6bd181ad77f006b5ece60bd0b6f398ff74a67"
-  integrity sha512-a7pEHdh1xKIAgTySUGgLMx/xwDZskN1Ud6egYYN3EdRW4ZMPNEDUTF+hwy2LUC+Bl+SyLXANnwz/jyh/qutKUw==
+  version "1.8.0"
+  resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.8.0.tgz#df807e5ecf53a609cc6bfe93eac3cc7be5b3a9d0"
+  integrity sha512-jLdtEOB112fORuypAyl/50VRVIBIdVQOSUUGQHzJ4xBSbit81zRarz7GThkEFZy1RceYrWYcPcBFPQwHyAc1gA==
+
+object-is@^1.0.1:
+  version "1.1.2"
+  resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.1.2.tgz#c5d2e87ff9e119f78b7a088441519e2eec1573b6"
+  integrity sha512-5lHCz+0uufF6wZ7CRFWJN3hp8Jqblpgve06U5CMQ3f//6iDjPr2PEo9MWCjEssDsa+UZEL4PkFpr+BMop6aKzQ==
+  dependencies:
+    define-properties "^1.1.3"
+    es-abstract "^1.17.5"
 
 object-keys@^1.0.11, object-keys@^1.0.12, object-keys@^1.1.1:
   version "1.1.1"
@@ -4678,17 +6084,16 @@ object.assign@^4.1.0:
     has-symbols "^1.0.0"
     object-keys "^1.0.11"
 
-object.entries@^1.1.0, object.entries@^1.1.1:
-  version "1.1.1"
-  resolved "https://registry.yarnpkg.com/object.entries/-/object.entries-1.1.1.tgz#ee1cf04153de02bb093fec33683900f57ce5399b"
-  integrity sha512-ilqR7BgdyZetJutmDPfXCDffGa0/Yzl2ivVNpbx/g4UeWrCdRnFDUBrKJGLhGieRHDATnyZXWBeCb29k9CJysQ==
+object.entries@^1.1.2:
+  version "1.1.2"
+  resolved "https://registry.yarnpkg.com/object.entries/-/object.entries-1.1.2.tgz#bc73f00acb6b6bb16c203434b10f9a7e797d3add"
+  integrity sha512-BQdB9qKmb/HyNdMNWVr7O3+z5MUIx3aiegEIJqjMBbBf0YT9RRxTJSim4mzFqtyr7PDAHigq0N9dO0m0tRakQA==
   dependencies:
     define-properties "^1.1.3"
-    es-abstract "^1.17.0-next.1"
-    function-bind "^1.1.1"
+    es-abstract "^1.17.5"
     has "^1.0.3"
 
-object.fromentries@^2.0.0, object.fromentries@^2.0.2:
+object.fromentries@^2.0.2:
   version "2.0.2"
   resolved "https://registry.yarnpkg.com/object.fromentries/-/object.fromentries-2.0.2.tgz#4a09c9b9bb3843dd0f89acdb517a794d4f355ac9"
   integrity sha512-r3ZiBH7MQppDJVLx6fhD618GKNG40CZYH9wgwdhKxBDDbQgjeWGGd4AtkZad84d291YxvWe7bJGuE65Anh0dxQ==
@@ -4698,6 +6103,14 @@ object.fromentries@^2.0.0, object.fromentries@^2.0.2:
     function-bind "^1.1.1"
     has "^1.0.3"
 
+object.getownpropertydescriptors@^2.0.3:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.0.tgz#369bf1f9592d8ab89d712dced5cb81c7c5352649"
+  integrity sha512-Z53Oah9A3TdLoblT7VKJaTDdXdT+lQO+cNpKVnya5JDe9uLvzu1YyY1yFDFrcxrlRgWrEFH0jJtD/IbuwjcEVg==
+  dependencies:
+    define-properties "^1.1.3"
+    es-abstract "^1.17.0-next.1"
+
 object.omit@^2.0.0:
   version "2.0.1"
   resolved "https://registry.yarnpkg.com/object.omit/-/object.omit-2.0.1.tgz#1a9c744829f39dbb858c76ca3579ae2a54ebd1fa"
@@ -4713,7 +6126,7 @@ object.pick@^1.3.0:
   dependencies:
     isobject "^3.0.1"
 
-object.values@^1.1.0, object.values@^1.1.1:
+object.values@^1.1.1:
   version "1.1.1"
   resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.1.1.tgz#68a99ecde356b7e9295a3c5e0ce31dc8c953de5e"
   integrity sha512-WTa54g2K8iu0kmS/us18jEmdv1a4Wi//BZ/DTVYEcH0XhLM5NYdpDHja3gt57VrZLcNAO2WGA+KpWsDBaHt6eA==
@@ -4730,7 +6143,7 @@ on-finished@~2.3.0:
   dependencies:
     ee-first "1.1.1"
 
-once@^1.3.0, once@^1.3.1, once@^1.4.0:
+once@^1.3.0, once@^1.3.1, once@^1.3.3, once@^1.4.0, once@~1.4.0:
   version "1.4.0"
   resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
   integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E=
@@ -4752,11 +6165,16 @@ onetime@^5.1.0:
     mimic-fn "^2.1.0"
 
 opencollective-postinstall@^2.0.2:
-  version "2.0.2"
-  resolved "https://registry.yarnpkg.com/opencollective-postinstall/-/opencollective-postinstall-2.0.2.tgz#5657f1bede69b6e33a45939b061eb53d3c6c3a89"
-  integrity sha512-pVOEP16TrAO2/fjej1IdOyupJY8KDUM1CvsaScRbw6oddvpQoOfGk4ywha0HKKVAD6RkW4x6Q+tNBwhf3Bgpuw==
+  version "2.0.3"
+  resolved "https://registry.yarnpkg.com/opencollective-postinstall/-/opencollective-postinstall-2.0.3.tgz#7a0fff978f6dbfa4d006238fbac98ed4198c3259"
+  integrity sha512-8AV/sCtuzUeTo8gQK5qDZzARrulB3egtLzFgteqB2tcT4Mw7B8Kt7JcDHmltjz6FOAHsvTevk70gZEbhM4ZS9Q==
+
+opener@~1.4.3:
+  version "1.4.3"
+  resolved "https://registry.yarnpkg.com/opener/-/opener-1.4.3.tgz#5c6da2c5d7e5831e8ffa3964950f8d6674ac90b8"
+  integrity sha1-XG2ixdflgx6P+jlklQ+NZnSskLg=
 
-optionator@^0.8.1, optionator@^0.8.3:
+optionator@^0.8.1:
   version "0.8.3"
   resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.3.tgz#84fa1d036fe9d3c7e21d99884b601167ec8fb495"
   integrity sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==
@@ -4768,16 +6186,50 @@ optionator@^0.8.1, optionator@^0.8.3:
     type-check "~0.3.2"
     word-wrap "~1.2.3"
 
+optionator@^0.9.1:
+  version "0.9.1"
+  resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.1.tgz#4f236a6373dae0566a6d43e1326674f50c291499"
+  integrity sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==
+  dependencies:
+    deep-is "^0.1.3"
+    fast-levenshtein "^2.0.6"
+    levn "^0.4.1"
+    prelude-ls "^1.2.1"
+    type-check "^0.4.0"
+    word-wrap "^1.2.3"
+
 options@>=0.0.5:
   version "0.0.6"
   resolved "https://registry.yarnpkg.com/options/-/options-0.0.6.tgz#ec22d312806bb53e731773e7cdaefcf1c643128f"
   integrity sha1-7CLTEoBrtT5zF3Pnza788cZDEo8=
 
-os-tmpdir@~1.0.2:
+os-homedir@^1.0.0:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3"
+  integrity sha1-/7xJiDNuDoM94MFox+8VISGqf7M=
+
+os-locale@^2.0.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/os-locale/-/os-locale-2.1.0.tgz#42bc2900a6b5b8bd17376c8e882b65afccf24bf2"
+  integrity sha512-3sslG3zJbEYcaC4YVAvDorjGxc7tv6KVATnLPZONiljsUncvihe9BQoVCEs0RZ1kmf4Hk9OBqlZfJZWI4GanKA==
+  dependencies:
+    execa "^0.7.0"
+    lcid "^1.0.0"
+    mem "^1.1.0"
+
+os-tmpdir@^1.0.0, os-tmpdir@~1.0.2:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274"
   integrity sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=
 
+osenv@0, osenv@^0.1.4, osenv@^0.1.5:
+  version "0.1.5"
+  resolved "https://registry.yarnpkg.com/osenv/-/osenv-0.1.5.tgz#85cdfafaeb28e8677f416e287592b5f3f49ea410"
+  integrity sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==
+  dependencies:
+    os-homedir "^1.0.0"
+    os-tmpdir "^1.0.0"
+
 p-each-series@^2.1.0:
   version "2.1.0"
   resolved "https://registry.yarnpkg.com/p-each-series/-/p-each-series-2.1.0.tgz#961c8dd3f195ea96c747e636b262b800a6b1af48"
@@ -4788,11 +6240,6 @@ p-finally@^1.0.0:
   resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae"
   integrity sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=
 
-p-finally@^2.0.0:
-  version "2.0.1"
-  resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-2.0.1.tgz#bd6fcaa9c559a096b680806f4d657b3f0f240561"
-  integrity sha512-vpm09aKwq6H9phqRQzecoDpD8TmVyGw70qmWlyq5onxY7tqyTTFVvxMykxQSQKILBSFlbXpypIw2T1Ml7+DDtw==
-
 p-limit@^1.1.0:
   version "1.3.0"
   resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-1.3.0.tgz#b86bd5f0c25690911c7590fcbfc2010d54b3ccb8"
@@ -4800,10 +6247,10 @@ p-limit@^1.1.0:
   dependencies:
     p-try "^1.0.0"
 
-p-limit@^2.2.0:
-  version "2.2.2"
-  resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.2.2.tgz#61279b67721f5287aa1c13a9a7fbbc48c9291b1e"
-  integrity sha512-WGR+xHecKTr7EbUEhyLSh5Dube9JtdiG78ufaeLxTgpudf/20KqyMioIUZJAezlTIi6evxuoUs9YXc11cU+yzQ==
+p-limit@^2.0.0, p-limit@^2.2.0:
+  version "2.3.0"
+  resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1"
+  integrity sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==
   dependencies:
     p-try "^2.0.0"
 
@@ -4814,6 +6261,13 @@ p-locate@^2.0.0:
   dependencies:
     p-limit "^1.1.0"
 
+p-locate@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-3.0.0.tgz#322d69a05c0264b25997d9f40cd8a891ab0064a4"
+  integrity sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==
+  dependencies:
+    p-limit "^2.0.0"
+
 p-locate@^4.1.0:
   version "4.1.0"
   resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-4.1.0.tgz#a3428bb7088b3a60292f66919278b7c297ad4f07"
@@ -4821,10 +6275,12 @@ p-locate@^4.1.0:
   dependencies:
     p-limit "^2.2.0"
 
-p-map@^2.0.0:
-  version "2.1.0"
-  resolved "https://registry.yarnpkg.com/p-map/-/p-map-2.1.0.tgz#310928feef9c9ecc65b68b17693018a665cea175"
-  integrity sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==
+p-map@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/p-map/-/p-map-4.0.0.tgz#bb2f95a5eda2ec168ec9274e06a747c3e2904d2b"
+  integrity sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==
+  dependencies:
+    aggregate-error "^3.0.0"
 
 p-try@^1.0.0:
   version "1.0.0"
@@ -4836,6 +6292,86 @@ p-try@^2.0.0:
   resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6"
   integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==
 
+package-json@^4.0.0:
+  version "4.0.1"
+  resolved "https://registry.yarnpkg.com/package-json/-/package-json-4.0.1.tgz#8869a0401253661c4c4ca3da6c2121ed555f5eed"
+  integrity sha1-iGmgQBJTZhxMTKPabCEh7VVfXu0=
+  dependencies:
+    got "^6.7.1"
+    registry-auth-token "^3.0.1"
+    registry-url "^3.0.3"
+    semver "^5.1.0"
+
+pacote@^7.6.1:
+  version "7.6.1"
+  resolved "https://registry.yarnpkg.com/pacote/-/pacote-7.6.1.tgz#d44621c89a5a61f173989b60236757728387c094"
+  integrity sha512-2kRIsHxjuYC1KRUIK80AFIXKWy0IgtFj76nKcaunozKAOSlfT+DFh3EfeaaKvNHCWixgi0G0rLg11lJeyEnp/Q==
+  dependencies:
+    bluebird "^3.5.1"
+    cacache "^10.0.4"
+    get-stream "^3.0.0"
+    glob "^7.1.2"
+    lru-cache "^4.1.1"
+    make-fetch-happen "^2.6.0"
+    minimatch "^3.0.4"
+    mississippi "^3.0.0"
+    mkdirp "^0.5.1"
+    normalize-package-data "^2.4.0"
+    npm-package-arg "^6.0.0"
+    npm-packlist "^1.1.10"
+    npm-pick-manifest "^2.1.0"
+    osenv "^0.1.5"
+    promise-inflight "^1.0.1"
+    promise-retry "^1.1.1"
+    protoduck "^5.0.0"
+    rimraf "^2.6.2"
+    safe-buffer "^5.1.1"
+    semver "^5.5.0"
+    ssri "^5.2.4"
+    tar "^4.4.0"
+    unique-filename "^1.1.0"
+    which "^1.3.0"
+
+pacote@^8.1.6:
+  version "8.1.6"
+  resolved "https://registry.yarnpkg.com/pacote/-/pacote-8.1.6.tgz#8e647564d38156367e7a9dc47a79ca1ab278d46e"
+  integrity sha512-wTOOfpaAQNEQNtPEx92x9Y9kRWVu45v583XT8x2oEV2xRB74+xdqMZIeGW4uFvAyZdmSBtye+wKdyyLaT8pcmw==
+  dependencies:
+    bluebird "^3.5.1"
+    cacache "^11.0.2"
+    get-stream "^3.0.0"
+    glob "^7.1.2"
+    lru-cache "^4.1.3"
+    make-fetch-happen "^4.0.1"
+    minimatch "^3.0.4"
+    minipass "^2.3.3"
+    mississippi "^3.0.0"
+    mkdirp "^0.5.1"
+    normalize-package-data "^2.4.0"
+    npm-package-arg "^6.1.0"
+    npm-packlist "^1.1.10"
+    npm-pick-manifest "^2.1.0"
+    osenv "^0.1.5"
+    promise-inflight "^1.0.1"
+    promise-retry "^1.1.1"
+    protoduck "^5.0.0"
+    rimraf "^2.6.2"
+    safe-buffer "^5.1.2"
+    semver "^5.5.0"
+    ssri "^6.0.0"
+    tar "^4.4.3"
+    unique-filename "^1.1.0"
+    which "^1.3.0"
+
+parallel-transform@^1.1.0:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/parallel-transform/-/parallel-transform-1.2.0.tgz#9049ca37d6cb2182c3b1d2c720be94d14a5814fc"
+  integrity sha512-P2vSmIu38uIlvdcU7fDkyrxj33gTUy/ABO5ZUbGowxNCopBq/OoD42bP4UmMrJoPyk4Uqf0mu3mtWBhHCZD8yg==
+  dependencies:
+    cyclist "^1.0.1"
+    inherits "^2.0.3"
+    readable-stream "^2.1.5"
+
 parent-module@^1.0.0:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2"
@@ -4861,19 +6397,19 @@ parse-json@^2.2.0:
     error-ex "^1.2.0"
 
 parse-json@^5.0.0:
-  version "5.0.0"
-  resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-5.0.0.tgz#73e5114c986d143efa3712d4ea24db9a4266f60f"
-  integrity sha512-OOY5b7PAEFV0E2Fir1KOkxchnZNCdowAJgQ5NuxjpBKTRP3pQhwkrkxqQjeoKJ+fO7bCpmIZaogI4eZGDMEGOw==
+  version "5.0.1"
+  resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-5.0.1.tgz#7cfe35c1ccd641bce3981467e6c2ece61b3b3878"
+  integrity sha512-ztoZ4/DYeXQq4E21v169sC8qWINGpcosGv9XhTDvg9/hWvx/zrFkc9BiWxR58OJLHGk28j5BL0SDLeV2WmFZlQ==
   dependencies:
     "@babel/code-frame" "^7.0.0"
     error-ex "^1.3.1"
     json-parse-better-errors "^1.0.1"
     lines-and-columns "^1.1.6"
 
-parse5@5.1.0:
-  version "5.1.0"
-  resolved "https://registry.yarnpkg.com/parse5/-/parse5-5.1.0.tgz#c59341c9723f414c452975564c7c00a68d58acd2"
-  integrity sha512-fxNG2sQjHvlVAYmzBZS9YlDp6PTSSDwa98vkD4QgVDDCAo84z5X1t5XyJQ62ImdLXx5NdIIfihey6xpum9/gRQ==
+parse5@5.1.1:
+  version "5.1.1"
+  resolved "https://registry.yarnpkg.com/parse5/-/parse5-5.1.1.tgz#f68e4e5ba1852ac2cadc00f4555fff6c2abb6178"
+  integrity sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug==
 
 parseurl@~1.3.3:
   version "1.3.3"
@@ -4900,6 +6436,11 @@ path-is-absolute@^1.0.0:
   resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f"
   integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18=
 
+path-is-inside@^1.0.1, path-is-inside@^1.0.2, path-is-inside@~1.0.2:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/path-is-inside/-/path-is-inside-1.0.2.tgz#365417dede44430d1c11af61027facf074bdfc53"
+  integrity sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM=
+
 path-key@^2.0.0, path-key@^2.0.1:
   version "2.0.1"
   resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40"
@@ -4951,21 +6492,21 @@ performance-now@^2.1.0:
   resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b"
   integrity sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=
 
-picomatch@^2.0.4:
+picomatch@^2.0.4, picomatch@^2.0.5:
   version "2.2.2"
   resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.2.2.tgz#21f333e9b6b8eaff02468f5146ea406d345f4dad"
   integrity sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg==
 
-picomatch@^2.0.5:
-  version "2.2.1"
-  resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.2.1.tgz#21bac888b6ed8601f831ce7816e335bc779f0a4a"
-  integrity sha512-ISBaA8xQNmwELC7eOjqFKMESB2VIqt4PPDD0nsS95b/9dZXvVKOlz9keMSnoGGKcOHXfTvDD6WMaRoSc9UuhRA==
-
 pify@^2.0.0:
   version "2.3.0"
   resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c"
   integrity sha1-7RQaasBDqEnqWISY59yosVMw6Qw=
 
+pify@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/pify/-/pify-3.0.0.tgz#e5a4acd2c101fdf3d9a4d07f0dbc4db49dd28176"
+  integrity sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=
+
 pirates@^4.0.1:
   version "4.0.1"
   resolved "https://registry.yarnpkg.com/pirates/-/pirates-4.0.1.tgz#643a92caf894566f91b2b986d2c66950a8e2fb87"
@@ -4994,10 +6535,10 @@ please-upgrade-node@^3.2.0:
   dependencies:
     semver-compare "^1.0.0"
 
-pn@^1.1.0:
-  version "1.1.0"
-  resolved "https://registry.yarnpkg.com/pn/-/pn-1.1.0.tgz#e2f4cef0e219f463c179ab37463e4e1ecdccbafb"
-  integrity sha512-2qHaIQr2VLRFoxe2nASzsV6ef4yOOH+Fi9FBOVH6cqeSgUnoyySPZkxzLuzd+RYOQTRpROA0ztTMqxROKSb/nA==
+pluralize@^8.0.0:
+  version "8.0.0"
+  resolved "https://registry.yarnpkg.com/pluralize/-/pluralize-8.0.0.tgz#1a6fa16a38d12a1901e0320fa017051c539ce3b1"
+  integrity sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==
 
 posix-character-classes@^0.1.0:
   version "0.1.1"
@@ -5013,11 +6554,21 @@ postcss@^6.0.1:
     source-map "^0.6.1"
     supports-color "^5.4.0"
 
+prelude-ls@^1.2.1:
+  version "1.2.1"
+  resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396"
+  integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==
+
 prelude-ls@~1.1.2:
   version "1.1.2"
   resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54"
   integrity sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=
 
+prepend-http@^1.0.1:
+  version "1.0.4"
+  resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-1.0.4.tgz#d4f4562b0ce3696e41ac52d0e002e57a635dc6dc"
+  integrity sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw=
+
 preserve@^0.2.0:
   version "0.2.0"
   resolved "https://registry.yarnpkg.com/preserve/-/preserve-0.2.0.tgz#815ed1f6ebc65926f865b310c0713bcb3315ce4b"
@@ -5031,16 +6582,26 @@ prettier-linter-helpers@^1.0.0:
     fast-diff "^1.1.2"
 
 prettier@^2.0.4:
-  version "2.0.4"
-  resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.0.4.tgz#2d1bae173e355996ee355ec9830a7a1ee05457ef"
-  integrity sha512-SVJIQ51spzFDvh4fIbCLvciiDMCrRhlN3mbZvv/+ycjvmF5E73bKdGfU8QDLNmjYJf+lsGnDBC4UUnvTe5OO0w==
+  version "2.0.5"
+  resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.0.5.tgz#d6d56282455243f2f92cc1716692c08aa31522d4"
+  integrity sha512-7PtVymN48hGcO4fGjybyBSIWDsLU4H4XlvOHfq91pz9kkGlonzwTfYkaIEwiRg/dAJF9YlbsduBAgtYLi+8cFg==
 
-pretty-format@^25.2.1, pretty-format@^25.4.0:
-  version "25.4.0"
-  resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-25.4.0.tgz#c58801bb5c4926ff4a677fe43f9b8b99812c7830"
-  integrity sha512-PI/2dpGjXK5HyXexLPZU/jw5T9Q6S1YVXxxVxco+LIqzUFHXIbKZKdUVt7GcX7QUCr31+3fzhi4gN4/wUYPVxQ==
+pretty-format@^25.2.1, pretty-format@^25.5.0:
+  version "25.5.0"
+  resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-25.5.0.tgz#7873c1d774f682c34b8d48b6743a2bf2ac55791a"
+  integrity sha512-kbo/kq2LQ/A/is0PQwsEHM7Ca6//bGPPvU6UnsdDRSKTWxT/ru/xb88v4BJf6a69H+uTytOEsTusT9ksd/1iWQ==
   dependencies:
-    "@jest/types" "^25.4.0"
+    "@jest/types" "^25.5.0"
+    ansi-regex "^5.0.0"
+    ansi-styles "^4.0.0"
+    react-is "^16.12.0"
+
+pretty-format@^26.1.0:
+  version "26.1.0"
+  resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-26.1.0.tgz#272b9cd1f1a924ab5d443dc224899d7a65cb96ec"
+  integrity sha512-GmeO1PEYdM+non4BKCj+XsPJjFOJIPnsLewqhDVoqY1xo0yNmDas7tC2XwpMrRAHR3MaE2hPo37deX5OisJ2Wg==
+  dependencies:
+    "@jest/types" "^26.1.0"
     ansi-regex "^5.0.0"
     ansi-styles "^4.0.0"
     react-is "^16.12.0"
@@ -5068,6 +6629,19 @@ progress@^2.0.0:
   resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8"
   integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==
 
+promise-inflight@^1.0.1, promise-inflight@~1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/promise-inflight/-/promise-inflight-1.0.1.tgz#98472870bf228132fcbdd868129bad12c3c029e3"
+  integrity sha1-mEcocL8igTL8vdhoEputEsPAKeM=
+
+promise-retry@^1.1.1:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/promise-retry/-/promise-retry-1.1.1.tgz#6739e968e3051da20ce6497fb2b50f6911df3d6d"
+  integrity sha1-ZznpaOMFHaIM5kl/srUPaRHfPW0=
+  dependencies:
+    err-code "^1.0.0"
+    retry "^0.10.0"
+
 prompts@^2.0.1:
   version "2.3.2"
   resolved "https://registry.yarnpkg.com/prompts/-/prompts-2.3.2.tgz#480572d89ecf39566d2bd3fe2c9fccb7c4c0b068"
@@ -5076,6 +6650,13 @@ prompts@^2.0.1:
     kleur "^3.0.3"
     sisteransi "^1.0.4"
 
+promzard@^0.3.0:
+  version "0.3.0"
+  resolved "https://registry.yarnpkg.com/promzard/-/promzard-0.3.0.tgz#26a5d6ee8c7dee4cb12208305acfb93ba382a9ee"
+  integrity sha1-JqXW7ox97kyxIggwWs+5O6OCqe4=
+  dependencies:
+    read "1"
+
 prop-types@^15.7.2:
   version "15.7.2"
   resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5"
@@ -5085,24 +6666,57 @@ prop-types@^15.7.2:
     object-assign "^4.1.1"
     react-is "^16.8.1"
 
+proto-list@~1.2.1:
+  version "1.2.4"
+  resolved "https://registry.yarnpkg.com/proto-list/-/proto-list-1.2.4.tgz#212d5bfe1318306a420f6402b8e26ff39647a849"
+  integrity sha1-IS1b/hMYMGpCD2QCuOJv85ZHqEk=
+
+protoduck@^5.0.0:
+  version "5.0.1"
+  resolved "https://registry.yarnpkg.com/protoduck/-/protoduck-5.0.1.tgz#03c3659ca18007b69a50fd82a7ebcc516261151f"
+  integrity sha512-WxoCeDCoCBY55BMvj4cAEjdVUFGRWed9ZxPlqTKYyw1nDDTQ4pqmnIMAGfJlg7Dx35uB/M+PHJPTmGOvaCaPTg==
+  dependencies:
+    genfun "^5.0.0"
+
 proxy-addr@~2.0.5:
-  version "2.0.5"
-  resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.5.tgz#34cbd64a2d81f4b1fd21e76f9f06c8a45299ee34"
-  integrity sha512-t/7RxHXPH6cJtP0pRG6smSr9QJidhB+3kXu0KgXnbGYMgzEnUxRQ4/LDdfOwZEMyIh3/xHb8PX3t+lfL9z+YVQ==
+  version "2.0.6"
+  resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.6.tgz#fdc2336505447d3f2f2c638ed272caf614bbb2bf"
+  integrity sha512-dh/frvCBVmSsDYzw6n926jv974gddhkFPfiN8hPOi30Wax25QZyZEGveluCgliBnqmuM+UJmBErbAUFIoDbjOw==
   dependencies:
     forwarded "~0.1.2"
-    ipaddr.js "1.9.0"
+    ipaddr.js "1.9.1"
 
-psl@^1.1.24:
-  version "1.7.0"
-  resolved "https://registry.yarnpkg.com/psl/-/psl-1.7.0.tgz#f1c4c47a8ef97167dea5d6bbf4816d736e884a3c"
-  integrity sha512-5NsSEDv8zY70ScRnOTn7bK7eanl2MvFrOrS/R6x+dBt5g1ghnj9Zv90kO8GwT8gxcu2ANyFprnFYB85IogIJOQ==
+prr@~1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/prr/-/prr-1.0.1.tgz#d3fc114ba06995a45ec6893f484ceb1d78f5f476"
+  integrity sha1-0/wRS6BplaRexok/SEzrHXj19HY=
+
+pseudomap@^1.0.2:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3"
+  integrity sha1-8FKijacOYYkX7wqKw0wa5aaChrM=
 
 psl@^1.1.28:
   version "1.8.0"
   resolved "https://registry.yarnpkg.com/psl/-/psl-1.8.0.tgz#9326f8bcfb013adcc005fdff056acce020e51c24"
   integrity sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ==
 
+pump@^1.0.0:
+  version "1.0.3"
+  resolved "https://registry.yarnpkg.com/pump/-/pump-1.0.3.tgz#5dfe8311c33bbf6fc18261f9f34702c47c08a954"
+  integrity sha512-8k0JupWme55+9tCVE+FS5ULT3K6AbgqrGa58lTT49RpyfwwcGedHqaC5LlQNdEAumn/wFsu6aPwkuPMioy8kqw==
+  dependencies:
+    end-of-stream "^1.1.0"
+    once "^1.3.1"
+
+pump@^2.0.0, pump@^2.0.1:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/pump/-/pump-2.0.1.tgz#12399add6e4cf7526d973cbc8b5ce2e2908b3909"
+  integrity sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA==
+  dependencies:
+    end-of-stream "^1.1.0"
+    once "^1.3.1"
+
 pump@^3.0.0:
   version "3.0.0"
   resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64"
@@ -5111,16 +6725,25 @@ pump@^3.0.0:
     end-of-stream "^1.1.0"
     once "^1.3.1"
 
-punycode@^1.4.1:
-  version "1.4.1"
-  resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e"
-  integrity sha1-wNWmOycYgArY4esPpSachN1BhF4=
+pumpify@^1.3.3:
+  version "1.5.1"
+  resolved "https://registry.yarnpkg.com/pumpify/-/pumpify-1.5.1.tgz#36513be246ab27570b1a374a5ce278bfd74370ce"
+  integrity sha512-oClZI37HvuUJJxSKKrC17bZ9Cu0ZYhEAGPsPUy9KlMUmv9dKX2o77RUmq7f3XjIxbwyGwYzbzQ1L2Ks8sIradQ==
+  dependencies:
+    duplexify "^3.6.0"
+    inherits "^2.0.3"
+    pump "^2.0.0"
 
 punycode@^2.1.0, punycode@^2.1.1:
   version "2.1.1"
   resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec"
   integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==
 
+qrcode-terminal@^0.12.0:
+  version "0.12.0"
+  resolved "https://registry.yarnpkg.com/qrcode-terminal/-/qrcode-terminal-0.12.0.tgz#bb5b699ef7f9f0505092a3748be4464fe71b5819"
+  integrity sha512-EXtzRZmC+YGmGlDFbXKxQiMZNwCLEO6BANKXG4iCtSIM0yqc/pappSx3RIKr4r0uh5JsBckOXeKrB3Iz7mdQpQ==
+
 qs@6.7.0:
   version "6.7.0"
   resolved "https://registry.yarnpkg.com/qs/-/qs-6.7.0.tgz#41dc1a015e3d581f1621776be31afb2876a9b1bc"
@@ -5131,6 +6754,20 @@ qs@~6.5.2:
   resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36"
   integrity sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==
 
+query-string@^6.1.0:
+  version "6.13.1"
+  resolved "https://registry.yarnpkg.com/query-string/-/query-string-6.13.1.tgz#d913ccfce3b4b3a713989fe6d39466d92e71ccad"
+  integrity sha512-RfoButmcK+yCta1+FuU8REvisx1oEzhMKwhLUNcepQTPGcNMp1sIqjnfCtfnvGSQZQEhaBHvccujtWoUV3TTbA==
+  dependencies:
+    decode-uri-component "^0.2.0"
+    split-on-first "^1.0.0"
+    strict-uri-encode "^2.0.0"
+
+qw@~1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/qw/-/qw-1.0.1.tgz#efbfdc740f9ad054304426acb183412cc8b996d4"
+  integrity sha1-77/cdA+a0FQwRCassYNBLMi5ltQ=
+
 randomatic@^3.0.0:
   version "3.1.1"
   resolved "https://registry.yarnpkg.com/randomatic/-/randomatic-3.1.1.tgz#b776efc59375984e36c537b2f51a1f0aff0da1ed"
@@ -5155,15 +6792,62 @@ raw-body@2.4.0:
     iconv-lite "0.4.24"
     unpipe "1.0.0"
 
-react-is@^16.12.0:
+rc@^1.0.1, rc@^1.1.6:
+  version "1.2.8"
+  resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed"
+  integrity sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==
+  dependencies:
+    deep-extend "^0.6.0"
+    ini "~1.3.0"
+    minimist "^1.2.0"
+    strip-json-comments "~2.0.1"
+
+react-is@^16.12.0, react-is@^16.8.1:
   version "16.13.1"
   resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
   integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
 
-react-is@^16.8.1:
-  version "16.12.0"
-  resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.12.0.tgz#2cc0fe0fba742d97fd527c42a13bec4eeb06241c"
-  integrity sha512-rPCkf/mWBtKc97aLL9/txD8DZdemK0vkA3JMLShjlJB3Pj3s+lpf1KaBzMfQrAmhMQB0n1cU/SUGgKKBCe837Q==
+read-cmd-shim@^1.0.1, read-cmd-shim@~1.0.1:
+  version "1.0.5"
+  resolved "https://registry.yarnpkg.com/read-cmd-shim/-/read-cmd-shim-1.0.5.tgz#87e43eba50098ba5a32d0ceb583ab8e43b961c16"
+  integrity sha512-v5yCqQ/7okKoZZkBQUAfTsQ3sVJtXdNfbPnI5cceppoxEVLYA3k+VtV2omkeo8MS94JCy4fSiUwlRBAwCVRPUA==
+  dependencies:
+    graceful-fs "^4.1.2"
+
+read-installed@~4.0.3:
+  version "4.0.3"
+  resolved "https://registry.yarnpkg.com/read-installed/-/read-installed-4.0.3.tgz#ff9b8b67f187d1e4c29b9feb31f6b223acd19067"
+  integrity sha1-/5uLZ/GH0eTCm5/rMfayI6zRkGc=
+  dependencies:
+    debuglog "^1.0.1"
+    read-package-json "^2.0.0"
+    readdir-scoped-modules "^1.0.0"
+    semver "2 || 3 || 4 || 5"
+    slide "~1.1.3"
+    util-extend "^1.0.1"
+  optionalDependencies:
+    graceful-fs "^4.1.2"
+
+"read-package-json@1 || 2", read-package-json@^2.0.0, read-package-json@^2.0.13:
+  version "2.1.1"
+  resolved "https://registry.yarnpkg.com/read-package-json/-/read-package-json-2.1.1.tgz#16aa66c59e7d4dad6288f179dd9295fd59bb98f1"
+  integrity sha512-dAiqGtVc/q5doFz6096CcnXhpYk0ZN8dEKVkGLU0CsASt8SrgF6SF7OTKAYubfvFhWaqofl+Y8HK19GR8jwW+A==
+  dependencies:
+    glob "^7.1.1"
+    json-parse-better-errors "^1.0.1"
+    normalize-package-data "^2.0.0"
+    npm-normalize-package-bin "^1.0.0"
+  optionalDependencies:
+    graceful-fs "^4.1.2"
+
+read-package-tree@^5.2.1:
+  version "5.3.1"
+  resolved "https://registry.yarnpkg.com/read-package-tree/-/read-package-tree-5.3.1.tgz#a32cb64c7f31eb8a6f31ef06f9cedf74068fe636"
+  integrity sha512-mLUDsD5JVtlZxjSlPPx1RETkNjjvQYuweKwNVt1Sn8kP5Jh44pvYuUHCp6xSVDZWbNxVxG5lyZJ921aJH61sTw==
+  dependencies:
+    read-package-json "^2.0.0"
+    readdir-scoped-modules "^1.0.0"
+    util-promisify "^2.1.0"
 
 read-pkg-up@^2.0.0:
   version "2.0.0"
@@ -5201,7 +6885,14 @@ read-pkg@^5.2.0:
     parse-json "^5.0.0"
     type-fest "^0.6.0"
 
-readable-stream@^2.0.2:
+read@1, read@~1.0.1, read@~1.0.7:
+  version "1.0.7"
+  resolved "https://registry.yarnpkg.com/read/-/read-1.0.7.tgz#b3da19bd052431a97671d44a42634adf710b40c4"
+  integrity sha1-s9oZvQUkMal2cdRKQmNK33ELQMQ=
+  dependencies:
+    mute-stream "~0.0.4"
+
+"readable-stream@1 || 2", readable-stream@^2.0.0, readable-stream@^2.0.2, readable-stream@^2.0.6, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@^2.3.6, readable-stream@~2.3.6:
   version "2.3.7"
   resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57"
   integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==
@@ -5214,6 +6905,26 @@ readable-stream@^2.0.2:
     string_decoder "~1.1.1"
     util-deprecate "~1.0.1"
 
+readable-stream@~1.1.10:
+  version "1.1.14"
+  resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.1.14.tgz#7cf4c54ef648e3813084c636dd2079e166c081d9"
+  integrity sha1-fPTFTvZI44EwhMY23SB54WbAgdk=
+  dependencies:
+    core-util-is "~1.0.0"
+    inherits "~2.0.1"
+    isarray "0.0.1"
+    string_decoder "~0.10.x"
+
+readdir-scoped-modules@^1.0.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/readdir-scoped-modules/-/readdir-scoped-modules-1.1.0.tgz#8d45407b4f870a0dcaebc0e28670d18e74514309"
+  integrity sha512-asaikDeqAQg7JifRsZn1NJZXo9E+VwlyCfbkZhwyISinqk5zNS6266HS5kah6P0SaQKGF6SkNnZVHUzHFYxYDw==
+  dependencies:
+    debuglog "^1.0.1"
+    dezalgo "^1.0.0"
+    graceful-fs "^4.1.2"
+    once "^1.3.0"
+
 readdirp@^2.0.0:
   version "2.2.1"
   resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-2.2.1.tgz#0e87622a3325aa33e892285caf8b4e846529a525"
@@ -5231,11 +6942,6 @@ realm-utils@^1.0.9:
     app-root-path "^1.3.0"
     mkdirp "^0.5.1"
 
-realpath-native@^2.0.0:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/realpath-native/-/realpath-native-2.0.0.tgz#7377ac429b6e1fd599dc38d08ed942d0d7beb866"
-  integrity sha512-v1SEYUOXXdbBZK8ZuNgO4TBjamPsiSgcFr0aP+tEKpQZK8vooEUqV6nm6Cv502mX4NF2EfsnVqtNAHG+/6Ur1Q==
-
 reconnecting-websocket@^4.4.0:
   version "4.4.0"
   resolved "https://registry.yarnpkg.com/reconnecting-websocket/-/reconnecting-websocket-4.4.0.tgz#3b0e5b96ef119e78a03135865b8bb0af1b948783"
@@ -5249,27 +6955,22 @@ redux@^4.0.4:
     loose-envify "^1.4.0"
     symbol-observable "^1.2.0"
 
-regenerate-unicode-properties@^8.1.0:
-  version "8.1.0"
-  resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-8.1.0.tgz#ef51e0f0ea4ad424b77bf7cb41f3e015c70a3f0e"
-  integrity sha512-LGZzkgtLY79GeXLm8Dp0BVLdQlWICzBnJz/ipWUgo59qBaZ+BHtq51P2q1uVZlppMuUAT37SDk39qUbjTWB7bA==
+regenerate-unicode-properties@^8.2.0:
+  version "8.2.0"
+  resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-8.2.0.tgz#e5de7111d655e7ba60c057dbe9ff37c87e65cdec"
+  integrity sha512-F9DjY1vKLo/tPePDycuH3dn9H1OTPIkVD9Kz4LODu+F2C75mgjAJ7x/gwy6ZcSNRAAkhNlJSOHRe8k3p+K9WhA==
   dependencies:
     regenerate "^1.4.0"
 
 regenerate@^1.4.0:
-  version "1.4.0"
-  resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.4.0.tgz#4a856ec4b56e4077c557589cae85e7a4c8869a11"
-  integrity sha512-1G6jJVDWrt0rK99kBjvEtziZNCICAuvIPkSiUFIQxVP06RCVpq3dmDo2oi6ABpYaDYaTRr67BEhL8r1wgEZZKg==
-
-regenerator-runtime@^0.13.2:
-  version "0.13.3"
-  resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.3.tgz#7cf6a77d8f5c6f60eb73c5fc1955b2ceb01e6bf5"
-  integrity sha512-naKIZz2GQ8JWh///G7L3X6LaQUAMp2lvb1rvwwsURe/VXwD6VMfr+/1NuNw3ag8v2kY1aQ/go5SNn79O9JU7yw==
+  version "1.4.1"
+  resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.4.1.tgz#cad92ad8e6b591773485fbe05a485caf4f457e6f"
+  integrity sha512-j2+C8+NtXQgEKWk49MMP5P/u2GhnahTtVkRIHr5R5lVRlbKvmQ+oS+A5aLKWp2ma5VkT8sh6v+v4hbH0YHR66A==
 
 regenerator-runtime@^0.13.4:
-  version "0.13.5"
-  resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.5.tgz#d878a1d094b4306d10b9096484b33ebd55e26697"
-  integrity sha512-ZS5w8CpKFinUzOwW3c83oPeVXoNsrLsaCoLtJvAClH135j/R77RuymhiSErhm2lKcwSCIpmvIWSbDkIfAqKQlA==
+  version "0.13.7"
+  resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz#cac2dacc8a1ea675feaabaeb8ae833898ae46f55"
+  integrity sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew==
 
 regex-cache@^0.4.2:
   version "0.4.4"
@@ -5286,17 +6987,12 @@ regex-not@^1.0.0, regex-not@^1.0.2:
     extend-shallow "^3.0.2"
     safe-regex "^1.1.0"
 
-regexp-tree@^0.1.20:
+regexp-tree@^0.1.21, regexp-tree@~0.1.1:
   version "0.1.21"
   resolved "https://registry.yarnpkg.com/regexp-tree/-/regexp-tree-0.1.21.tgz#55e2246b7f7d36f1b461490942fa780299c400d7"
   integrity sha512-kUUXjX4AnqnR8KRTCrayAo9PzYMRKmVoGgaz2tBuz0MF3g1ZbGebmtW0yFHfFK9CmBjQKeYIgoL22pFLBJY7sw==
 
-regexp-tree@~0.1.1:
-  version "0.1.18"
-  resolved "https://registry.yarnpkg.com/regexp-tree/-/regexp-tree-0.1.18.tgz#ed4819a9f03ec2de9613421d6eaf47512e7fdaf1"
-  integrity sha512-mKLUfTDU1GE5jGR7cn2IEPDzYjmOviZOHYAR1XGe8Lg48Mdk684waD1Fqhv2Nef+TsDVdmIj08m/GUKTMk7J2Q==
-
-regexp.prototype.flags@^1.3.0:
+regexp.prototype.flags@^1.2.0, regexp.prototype.flags@^1.3.0:
   version "1.3.0"
   resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.3.0.tgz#7aba89b3c13a64509dabcf3ca8d9fbb9bdf5cb75"
   integrity sha512-2+Q0C5g951OlYlJz6yu5/M33IcsESLlLfsyIaLJaG4FA2r4yP8MvVMJUUP/fVBkSpbbbZlS5gynbEWLipiiXiQ==
@@ -5304,37 +7000,47 @@ regexp.prototype.flags@^1.3.0:
     define-properties "^1.1.3"
     es-abstract "^1.17.0-next.1"
 
-regexpp@^2.0.1:
-  version "2.0.1"
-  resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-2.0.1.tgz#8d19d31cf632482b589049f8281f93dbcba4d07f"
-  integrity sha512-lv0M6+TkDVniA3aD1Eg0DVpfU/booSu7Eev3TDO/mZKHBfVjgCGTV4t4buppESEYDtkArYFOxTJWv6S5C+iaNw==
-
-regexpp@^3.0.0:
-  version "3.0.0"
-  resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-3.0.0.tgz#dd63982ee3300e67b41c1956f850aa680d9d330e"
-  integrity sha512-Z+hNr7RAVWxznLPuA7DIh8UNX1j9CDrUQxskw9IrBE1Dxue2lyXT+shqEIeLUjrokxIP8CMy1WkjgG3rTsd5/g==
+regexpp@^3.0.0, regexpp@^3.1.0:
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-3.1.0.tgz#206d0ad0a5648cffbdb8ae46438f3dc51c9f78e2"
+  integrity sha512-ZOIzd8yVsQQA7j8GCSlPGXwg5PfmA1mrq0JP4nGhh54LaKN3xdai/vHUDu74pKwV8OxseMS65u2NImosQcSD0Q==
 
 regexpu-core@^4.1.3:
-  version "4.6.0"
-  resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-4.6.0.tgz#2037c18b327cfce8a6fea2a4ec441f2432afb8b6"
-  integrity sha512-YlVaefl8P5BnFYOITTNzDvan1ulLOiXJzCNZxduTIosN17b87h3bvG9yHMoHaRuo88H4mQ06Aodj5VtYGGGiTg==
+  version "4.7.0"
+  resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-4.7.0.tgz#fcbf458c50431b0bb7b45d6967b8192d91f3d938"
+  integrity sha512-TQ4KXRnIn6tz6tjnrXEkD/sshygKH/j5KzK86X8MkeHyZ8qst/LZ89j3X4/8HEIfHANTFIP/AbXakeRhWIl5YQ==
   dependencies:
     regenerate "^1.4.0"
-    regenerate-unicode-properties "^8.1.0"
-    regjsgen "^0.5.0"
-    regjsparser "^0.6.0"
+    regenerate-unicode-properties "^8.2.0"
+    regjsgen "^0.5.1"
+    regjsparser "^0.6.4"
     unicode-match-property-ecmascript "^1.0.4"
-    unicode-match-property-value-ecmascript "^1.1.0"
+    unicode-match-property-value-ecmascript "^1.2.0"
 
-regjsgen@^0.5.0:
-  version "0.5.1"
-  resolved "https://registry.yarnpkg.com/regjsgen/-/regjsgen-0.5.1.tgz#48f0bf1a5ea205196929c0d9798b42d1ed98443c"
-  integrity sha512-5qxzGZjDs9w4tzT3TPhCJqWdCc3RLYwy9J2NB0nm5Lz+S273lvWcpjaTGHsT1dc6Hhfq41uSEOw8wBmxrKOuyg==
+registry-auth-token@^3.0.1:
+  version "3.4.0"
+  resolved "https://registry.yarnpkg.com/registry-auth-token/-/registry-auth-token-3.4.0.tgz#d7446815433f5d5ed6431cd5dca21048f66b397e"
+  integrity sha512-4LM6Fw8eBQdwMYcES4yTnn2TqIasbXuwDx3um+QRs7S55aMKCBKBxvPXl2RiUjHwuJLTyYfxSpmfSAjQpcuP+A==
+  dependencies:
+    rc "^1.1.6"
+    safe-buffer "^5.0.1"
 
-regjsparser@^0.6.0:
-  version "0.6.2"
-  resolved "https://registry.yarnpkg.com/regjsparser/-/regjsparser-0.6.2.tgz#fd62c753991467d9d1ffe0a9f67f27a529024b96"
-  integrity sha512-E9ghzUtoLwDekPT0DYCp+c4h+bvuUpe6rRHCTYn6eGoqj1LgKXxT6I0Il4WbjhQkOghzi/V+y03bPKvbllL93Q==
+registry-url@^3.0.3:
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/registry-url/-/registry-url-3.1.0.tgz#3d4ef870f73dde1d77f0cf9a381432444e174942"
+  integrity sha1-PU74cPc93h138M+aOBQyRE4XSUI=
+  dependencies:
+    rc "^1.0.1"
+
+regjsgen@^0.5.1:
+  version "0.5.2"
+  resolved "https://registry.yarnpkg.com/regjsgen/-/regjsgen-0.5.2.tgz#92ff295fb1deecbf6ecdab2543d207e91aa33733"
+  integrity sha512-OFFT3MfrH90xIW8OOSyUrk6QHD5E9JOTeGodiJeBS3J6IwlgzJMNE/1bZklWz5oTg+9dCMyEetclvCVXOPoN3A==
+
+regjsparser@^0.6.4:
+  version "0.6.4"
+  resolved "https://registry.yarnpkg.com/regjsparser/-/regjsparser-0.6.4.tgz#a769f8684308401a66e9b529d2436ff4d0666272"
+  integrity sha512-64O87/dPDgfk8/RQqC4gkZoGyyWFIEUTTh80CU6CWuK5vkCGyekIx+oKcEIYtP/RAxSQltCZHCNu/mdd7fqlJw==
   dependencies:
     jsesc "~0.5.0"
 
@@ -5353,49 +7059,23 @@ repeat-string@^1.5.2, repeat-string@^1.6.1:
   resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637"
   integrity sha1-jcrkcOHIirwtYA//Sndihtp15jc=
 
-request-promise-core@1.1.3:
-  version "1.1.3"
-  resolved "https://registry.yarnpkg.com/request-promise-core/-/request-promise-core-1.1.3.tgz#e9a3c081b51380dfea677336061fea879a829ee9"
-  integrity sha512-QIs2+ArIGQVp5ZYbWD5ZLCY29D5CfWizP8eWnm8FoGD1TX61veauETVQbrV60662V0oFBkrDOuaBI8XgtuyYAQ==
+request-promise-core@1.1.4:
+  version "1.1.4"
+  resolved "https://registry.yarnpkg.com/request-promise-core/-/request-promise-core-1.1.4.tgz#3eedd4223208d419867b78ce815167d10593a22f"
+  integrity sha512-TTbAfBBRdWD7aNNOoVOBH4pN/KigV6LyapYNNlAPA8JwbovRti1E88m3sYAwsLi5ryhPKsE9APwnjFTgdUjTpw==
   dependencies:
-    lodash "^4.17.15"
+    lodash "^4.17.19"
 
-request-promise-native@^1.0.7:
-  version "1.0.8"
-  resolved "https://registry.yarnpkg.com/request-promise-native/-/request-promise-native-1.0.8.tgz#a455b960b826e44e2bf8999af64dff2bfe58cb36"
-  integrity sha512-dapwLGqkHtwL5AEbfenuzjTYg35Jd6KPytsC2/TLkVMz8rm+tNt72MGUWT1RP/aYawMpN6HqbNGBQaRcBtjQMQ==
+request-promise-native@^1.0.8:
+  version "1.0.9"
+  resolved "https://registry.yarnpkg.com/request-promise-native/-/request-promise-native-1.0.9.tgz#e407120526a5efdc9a39b28a5679bf47b9d9dc28"
+  integrity sha512-wcW+sIUiWnKgNY0dqCpOZkUbF/I+YPi+f09JZIDa39Ec+q82CpSYniDp+ISgTTbKmnpJWASeJBPZmoxH84wt3g==
   dependencies:
-    request-promise-core "1.1.3"
+    request-promise-core "1.1.4"
     stealthy-require "^1.1.1"
     tough-cookie "^2.3.3"
 
-request@^2.79.0:
-  version "2.88.0"
-  resolved "https://registry.yarnpkg.com/request/-/request-2.88.0.tgz#9c2fca4f7d35b592efe57c7f0a55e81052124fef"
-  integrity sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==
-  dependencies:
-    aws-sign2 "~0.7.0"
-    aws4 "^1.8.0"
-    caseless "~0.12.0"
-    combined-stream "~1.0.6"
-    extend "~3.0.2"
-    forever-agent "~0.6.1"
-    form-data "~2.3.2"
-    har-validator "~5.1.0"
-    http-signature "~1.2.0"
-    is-typedarray "~1.0.0"
-    isstream "~0.1.2"
-    json-stringify-safe "~5.0.1"
-    mime-types "~2.1.19"
-    oauth-sign "~0.9.0"
-    performance-now "^2.1.0"
-    qs "~6.5.2"
-    safe-buffer "^5.1.2"
-    tough-cookie "~2.4.3"
-    tunnel-agent "^0.6.0"
-    uuid "^3.3.2"
-
-request@^2.88.0:
+request@^2.74.0, request@^2.79.0, request@^2.85.0, request@^2.87.0, request@^2.88.2:
   version "2.88.2"
   resolved "https://registry.yarnpkg.com/request/-/request-2.88.2.tgz#d73c918731cb5a87da047e207234146f664d12b3"
   integrity sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==
@@ -5426,6 +7106,11 @@ require-directory@^2.1.1:
   resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42"
   integrity sha1-jGStX9MNqxyXbiNE/+f3kqam30I=
 
+require-main-filename@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-1.0.1.tgz#97f717b69d48784f5f526a6c5aa8ffdda055a4d1"
+  integrity sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE=
+
 require-main-filename@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-2.0.0.tgz#d0b329ecc7cc0f61649f62215be69af54aa8989b"
@@ -5463,29 +7148,10 @@ resolve-url@^0.2.1:
   resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a"
   integrity sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=
 
-resolve@1.1.7:
-  version "1.1.7"
-  resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.1.7.tgz#203114d82ad2c5ed9e8e0411b3932875e889e97b"
-  integrity sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs=
-
-resolve@1.x, resolve@^1.3.2:
-  version "1.17.0"
-  resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.17.0.tgz#b25941b54968231cc2d1bb76a79cb7f2c0bf8444"
-  integrity sha512-ic+7JYiV8Vi2yzQGFWOkiZD5Z9z7O2Zhm9XMaTxdJExKasieFCr+yXZ/WmXsckHiKl12ar0y6XiXDx3m4RHn1w==
-  dependencies:
-    path-parse "^1.0.6"
-
-resolve@^1.10.0, resolve@^1.10.1, resolve@^1.12.0, resolve@^1.13.1:
-  version "1.15.0"
-  resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.15.0.tgz#1b7ca96073ebb52e741ffd799f6b39ea462c67f5"
-  integrity sha512-+hTmAldEGE80U2wJJDC1lebb5jWqvTYAfm3YZ1ckk1gBr0MnCqUKlwK1e+anaFljIl+F5tR5IoZcm4ZDA1zMQw==
-  dependencies:
-    path-parse "^1.0.6"
-
-resolve@^1.15.1:
-  version "1.15.1"
-  resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.15.1.tgz#27bdcdeffeaf2d6244b95bb0f9f4b4653451f3e8"
-  integrity sha512-84oo6ZTtoTUpjgNEr5SJyzQhzL72gaRodsSfyxC/AXRvwu0Yse9H8eF9IpGo7b8YetZhlI6v7ZQ6bKBFV/6S7w==
+resolve@^1.10.0, resolve@^1.10.1, resolve@^1.12.0, resolve@^1.13.1, resolve@^1.17.0, resolve@^1.3.2:
+  version "1.17.0"
+  resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.17.0.tgz#b25941b54968231cc2d1bb76a79cb7f2c0bf8444"
+  integrity sha512-ic+7JYiV8Vi2yzQGFWOkiZD5Z9z7O2Zhm9XMaTxdJExKasieFCr+yXZ/WmXsckHiKl12ar0y6XiXDx3m4RHn1w==
   dependencies:
     path-parse "^1.0.6"
 
@@ -5510,7 +7176,24 @@ ret@~0.1.10:
   resolved "https://registry.yarnpkg.com/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc"
   integrity sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==
 
-rimraf@2.6.3:
+retry@^0.10.0:
+  version "0.10.1"
+  resolved "https://registry.yarnpkg.com/retry/-/retry-0.10.1.tgz#e76388d217992c252750241d3d3956fed98d8ff4"
+  integrity sha1-52OI0heZLCUnUCQdPTlW/tmNj/Q=
+
+retry@^0.12.0:
+  version "0.12.0"
+  resolved "https://registry.yarnpkg.com/retry/-/retry-0.12.0.tgz#1b42a6266a21f07421d1b0b54b7dc167b01c013b"
+  integrity sha1-G0KmJmoh8HQh0bC1S33BZ7AcATs=
+
+rimraf@2, rimraf@^2.5.2, rimraf@^2.5.4, rimraf@^2.6.2, rimraf@^2.6.3:
+  version "2.7.1"
+  resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec"
+  integrity sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==
+  dependencies:
+    glob "^7.1.3"
+
+rimraf@2.6.3, rimraf@~2.6.2:
   version "2.6.3"
   resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.3.tgz#b2d104fe0d8fb27cf9e0a1cda8262dd3833c6cab"
   integrity sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==
@@ -5530,11 +7213,16 @@ rsvp@^4.8.4:
   integrity sha512-nfMOlASu9OnRJo1mbEk2cz0D56a1MBNrJ7orjRZQG10XDyuvwksKbuXNp6qa+kbn839HwjwhBzhFmdsaEAfauA==
 
 run-async@^2.2.0:
-  version "2.3.0"
-  resolved "https://registry.yarnpkg.com/run-async/-/run-async-2.3.0.tgz#0371ab4ae0bdd720d4166d7dfda64ff7a445a6c0"
-  integrity sha1-A3GrSuC91yDUFm19/aZP96RFpsA=
+  version "2.4.1"
+  resolved "https://registry.yarnpkg.com/run-async/-/run-async-2.4.1.tgz#8440eccf99ea3e70bd409d49aab88e10c189a455"
+  integrity sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==
+
+run-queue@^1.0.0, run-queue@^1.0.3:
+  version "1.0.3"
+  resolved "https://registry.yarnpkg.com/run-queue/-/run-queue-1.0.3.tgz#e848396f057d223f24386924618e25694161ec47"
+  integrity sha1-6Eg5bwV9Ij8kOGkkYY4laUFh7Ec=
   dependencies:
-    is-promise "^2.1.0"
+    aproba "^1.1.1"
 
 rx-lite-aggregates@^4.0.8:
   version "4.0.8"
@@ -5548,17 +7236,10 @@ rx-lite@*, rx-lite@^4.0.8:
   resolved "https://registry.yarnpkg.com/rx-lite/-/rx-lite-4.0.8.tgz#0b1e11af8bc44836f04a6407e92da42467b79444"
   integrity sha1-Cx4Rr4vESDbwSmQH6S2kJGe3lEQ=
 
-rxjs@^6.3.3, rxjs@^6.5.3:
-  version "6.5.4"
-  resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.5.4.tgz#e0777fe0d184cec7872df147f303572d414e211c"
-  integrity sha512-naMQXcgEo3csAEGvw/NydRA0fuS2nDZJiw1YUWFKU7aPPAPGZEsD4Iimit96qwCieH6y614MCLYwdkrWx7z/7Q==
-  dependencies:
-    tslib "^1.9.0"
-
-rxjs@^6.5.5:
-  version "6.5.5"
-  resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.5.5.tgz#c5c884e3094c8cfee31bf27eb87e54ccfc87f9ec"
-  integrity sha512-WfQI+1gohdf0Dai/Bbmk5L5ItH5tYqm3ki2c5GdWhKjalzjg93N3avFjVStyZZz+A2Em+ZxKH5bNghw9UeylGQ==
+rxjs@^6.5.5, rxjs@^6.6.0:
+  version "6.6.0"
+  resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.6.0.tgz#af2901eedf02e3a83ffa7f886240ff9018bbec84"
+  integrity sha512-3HMA8z/Oz61DUHe+SdOiQyzIf4tOx5oQHmMir7IZEu6TMqCLHT4LRcmNaUS0NwOz8VLvmmBduMsoaUvMaIiqzg==
   dependencies:
     tslib "^1.9.0"
 
@@ -5567,10 +7248,10 @@ safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1:
   resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d"
   integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==
 
-safe-buffer@^5.0.1, safe-buffer@^5.1.2:
-  version "5.2.0"
-  resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.0.tgz#b74daec49b1148f88c64b68d49b1e815c1f2f519"
-  integrity sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg==
+safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@^5.1.2:
+  version "5.2.1"
+  resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6"
+  integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==
 
 safe-regex@^1.1.0:
   version "1.1.0"
@@ -5586,7 +7267,7 @@ safe-regex@^2.1.1:
   dependencies:
     regexp-tree "~0.1.1"
 
-"safer-buffer@>= 2.1.2 < 3", safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0:
+"safer-buffer@>= 2.1.2 < 3", "safer-buffer@>= 2.1.2 < 3.0.0", safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0:
   version "2.1.2"
   resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
   integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
@@ -5606,37 +7287,49 @@ sane@^4.0.3:
     minimist "^1.1.1"
     walker "~1.0.5"
 
-saxes@^3.1.9:
-  version "3.1.11"
-  resolved "https://registry.yarnpkg.com/saxes/-/saxes-3.1.11.tgz#d59d1fd332ec92ad98a2e0b2ee644702384b1c5b"
-  integrity sha512-Ydydq3zC+WYDJK1+gRxRapLIED9PWeSuuS41wqyoRmzvhhh9nc+QQrVMKJYzJFULazeGhzSV0QleN2wD3boh2g==
+saxes@^5.0.0:
+  version "5.0.1"
+  resolved "https://registry.yarnpkg.com/saxes/-/saxes-5.0.1.tgz#eebab953fa3b7608dbe94e5dadb15c888fa6696d"
+  integrity sha512-5LBh1Tls8c9xgGjw3QrMwETmTMVk0oFgvrFSvWx62llR2hcEInrKNZ2GZCCuuy2lvWrdl5jhbpeqc5hRYKFOcw==
   dependencies:
-    xmlchars "^2.1.1"
+    xmlchars "^2.2.0"
 
 semver-compare@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/semver-compare/-/semver-compare-1.0.0.tgz#0dee216a1c941ab37e9efb1788f6afc5ff5537fc"
   integrity sha1-De4hahyUGrN+nvsXiPavxf9VN/w=
 
+semver-diff@^2.0.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/semver-diff/-/semver-diff-2.1.0.tgz#4bbb8437c8d37e4b0cf1a68fd726ec6d645d6d36"
+  integrity sha1-S7uEN8jTfksM8aaP1ybsbWRdbTY=
+  dependencies:
+    semver "^5.0.3"
+
 semver-regex@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/semver-regex/-/semver-regex-2.0.0.tgz#a93c2c5844539a770233379107b38c7b4ac9d338"
   integrity sha512-mUdIBBvdn0PLOeP3TEkMH7HHeUP3GjsXCwKarjv/kGmUFOYg1VqEemKhoQpWMu6X2I8kHeuVdGibLGkVK+/5Qw==
 
-"semver@2 || 3 || 4 || 5", semver@^5.4.1, semver@^5.5.0:
+"semver@2 >=2.2.1 || 3.x || 4 || 5", "semver@2 || 3 || 4 || 5", "semver@2.x || 3.x || 4 || 5", "semver@^2.3.0 || 3.x || 4 || 5", semver@^5.0.3, semver@^5.1.0, semver@^5.4.1, semver@^5.5.0, semver@^5.6.0:
   version "5.7.1"
   resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7"
   integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==
 
-semver@6.x, semver@^6.0.0, semver@^6.1.0, semver@^6.1.2, semver@^6.3.0:
+semver@7.x, semver@^7.2.1, semver@^7.3.2:
+  version "7.3.2"
+  resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.2.tgz#604962b052b81ed0786aae84389ffba70ffd3938"
+  integrity sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ==
+
+semver@^6.0.0, semver@^6.1.0, semver@^6.3.0:
   version "6.3.0"
   resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d"
   integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==
 
-semver@^7.1.2:
-  version "7.1.3"
-  resolved "https://registry.yarnpkg.com/semver/-/semver-7.1.3.tgz#e4345ce73071c53f336445cfc19efb1c311df2a6"
-  integrity sha512-ekM0zfiA9SCBlsKa2X1hxyxiI4L3B6EbVJkkdgQXnSEEaHlGdvyodMruTiulSRWMMB4NeIuYNMC9rTKTz97GxA==
+semver@~5.3.0:
+  version "5.3.0"
+  resolved "https://registry.yarnpkg.com/semver/-/semver-5.3.0.tgz#9b2ce5d3de02d17c6012ad326aa6b4d0cf54f94f"
+  integrity sha1-myzl094C0XxgEq0yaqa00M9U+U8=
 
 send@0.17.1:
   version "0.17.1"
@@ -5667,7 +7360,7 @@ serve-static@1.14.1:
     parseurl "~1.3.3"
     send "0.17.1"
 
-set-blocking@^2.0.0:
+set-blocking@^2.0.0, set-blocking@~2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7"
   integrity sha1-BF+XgtARrppoA93TgrJDkrPYkPc=
@@ -5687,6 +7380,19 @@ setprototypeof@1.1.1:
   resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.1.tgz#7e95acb24aa92f5885e0abef5ba131330d4ae683"
   integrity sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==
 
+sha@~2.0.1:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/sha/-/sha-2.0.1.tgz#6030822fbd2c9823949f8f72ed6411ee5cf25aae"
+  integrity sha1-YDCCL70smCOUn49y7WQR7lzyWq4=
+  dependencies:
+    graceful-fs "^4.1.2"
+    readable-stream "^2.0.2"
+
+shallowequal@^1.0.1:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/shallowequal/-/shallowequal-1.1.0.tgz#188d521de95b9087404fd4dcb68b13df0ae4e7f8"
+  integrity sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==
+
 shebang-command@^1.2.0:
   version "1.2.0"
   resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea"
@@ -5729,16 +7435,11 @@ side-channel@^1.0.2:
     es-abstract "^1.17.0-next.1"
     object-inspect "^1.7.0"
 
-signal-exit@^3.0.0:
+signal-exit@^3.0.0, signal-exit@^3.0.2:
   version "3.0.3"
   resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.3.tgz#a1410c2edd8f077b08b4e253c8eacfcaf057461c"
   integrity sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==
 
-signal-exit@^3.0.2:
-  version "3.0.2"
-  resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d"
-  integrity sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=
-
 sisteransi@^1.0.4:
   version "1.0.5"
   resolved "https://registry.yarnpkg.com/sisteransi/-/sisteransi-1.0.5.tgz#134d681297756437cc05ca01370d3a7a571075ed"
@@ -5749,11 +7450,6 @@ slash@^3.0.0:
   resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634"
   integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==
 
-slice-ansi@0.0.4:
-  version "0.0.4"
-  resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-0.0.4.tgz#edbf8903f66f7ce2f8eafd6ceed65e264c831b35"
-  integrity sha1-7b+JA/ZvfOL46v1s7tZeJkyDGzU=
-
 slice-ansi@^2.1.0:
   version "2.1.0"
   resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-2.1.0.tgz#cacd7693461a637a5788d92a7dd4fba068e81636"
@@ -5763,6 +7459,39 @@ slice-ansi@^2.1.0:
     astral-regex "^1.0.0"
     is-fullwidth-code-point "^2.0.0"
 
+slice-ansi@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-3.0.0.tgz#31ddc10930a1b7e0b67b08c96c2f49b77a789787"
+  integrity sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==
+  dependencies:
+    ansi-styles "^4.0.0"
+    astral-regex "^2.0.0"
+    is-fullwidth-code-point "^3.0.0"
+
+slice-ansi@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-4.0.0.tgz#500e8dd0fd55b05815086255b3195adf2a45fe6b"
+  integrity sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==
+  dependencies:
+    ansi-styles "^4.0.0"
+    astral-regex "^2.0.0"
+    is-fullwidth-code-point "^3.0.0"
+
+slide@^1.1.3, slide@^1.1.6, slide@~1.1.3, slide@~1.1.6:
+  version "1.1.6"
+  resolved "https://registry.yarnpkg.com/slide/-/slide-1.1.6.tgz#56eb027d65b4d2dce6cb2e2d32c4d4afc9e1d707"
+  integrity sha1-VusCfWW00tzmyy4tMsTUr8nh1wc=
+
+smart-buffer@^1.0.13:
+  version "1.1.15"
+  resolved "https://registry.yarnpkg.com/smart-buffer/-/smart-buffer-1.1.15.tgz#7f114b5b65fab3e2a35aa775bb12f0d1c649bf16"
+  integrity sha1-fxFLW2X6s+KjWqd1uxLw0cZJvxY=
+
+smart-buffer@^4.1.0:
+  version "4.1.0"
+  resolved "https://registry.yarnpkg.com/smart-buffer/-/smart-buffer-4.1.0.tgz#91605c25d91652f4661ea69ccf45f1b331ca21ba"
+  integrity sha512-iVICrxOzCynf/SNaBQCw34eM9jROU/s5rzIhpOvzhzuYHfJR/DhZfDkXiZSgKXfgv26HT3Yni3AV/DGw0cGnnw==
+
 snapdragon-node@^2.0.1:
   version "2.1.1"
   resolved "https://registry.yarnpkg.com/snapdragon-node/-/snapdragon-node-2.1.1.tgz#6c175f86ff14bdb0724563e8f3c1b021a286853b"
@@ -5793,10 +7522,55 @@ snapdragon@^0.8.1:
     source-map-resolve "^0.5.0"
     use "^3.1.0"
 
+socks-proxy-agent@^3.0.1:
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/socks-proxy-agent/-/socks-proxy-agent-3.0.1.tgz#2eae7cf8e2a82d34565761539a7f9718c5617659"
+  integrity sha512-ZwEDymm204mTzvdqyUqOdovVr2YRd2NYskrYrF2LXyZ9qDiMAoFESGK8CRphiO7rtbo2Y757k2Nia3x2hGtalA==
+  dependencies:
+    agent-base "^4.1.0"
+    socks "^1.1.10"
+
+socks-proxy-agent@^4.0.0:
+  version "4.0.2"
+  resolved "https://registry.yarnpkg.com/socks-proxy-agent/-/socks-proxy-agent-4.0.2.tgz#3c8991f3145b2799e70e11bd5fbc8b1963116386"
+  integrity sha512-NT6syHhI9LmuEMSK6Kd2V7gNv5KFZoLE7V5udWmn0de+3Mkj3UMA/AJPLyeNUVmElCurSHtUdM3ETpR3z770Wg==
+  dependencies:
+    agent-base "~4.2.1"
+    socks "~2.3.2"
+
+socks@^1.1.10:
+  version "1.1.10"
+  resolved "https://registry.yarnpkg.com/socks/-/socks-1.1.10.tgz#5b8b7fc7c8f341c53ed056e929b7bf4de8ba7b5a"
+  integrity sha1-W4t/x8jzQcU+0FbpKbe/Tei6e1o=
+  dependencies:
+    ip "^1.1.4"
+    smart-buffer "^1.0.13"
+
+socks@~2.3.2:
+  version "2.3.3"
+  resolved "https://registry.yarnpkg.com/socks/-/socks-2.3.3.tgz#01129f0a5d534d2b897712ed8aceab7ee65d78e3"
+  integrity sha512-o5t52PCNtVdiOvzMry7wU4aOqYWL0PeCXRWBEiJow4/i/wr+wpsJQ9awEu1EonLIqsfGd5qSgDdxEOvCdmBEpA==
+  dependencies:
+    ip "1.1.5"
+    smart-buffer "^4.1.0"
+
+sorted-object@~2.0.1:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/sorted-object/-/sorted-object-2.0.1.tgz#7d631f4bd3a798a24af1dffcfbfe83337a5df5fc"
+  integrity sha1-fWMfS9OnmKJK8d/8+/6DM3pd9fw=
+
+sorted-union-stream@~2.1.3:
+  version "2.1.3"
+  resolved "https://registry.yarnpkg.com/sorted-union-stream/-/sorted-union-stream-2.1.3.tgz#c7794c7e077880052ff71a8d4a2dbb4a9a638ac7"
+  integrity sha1-x3lMfgd4gAUv9xqNSi27Sppjisc=
+  dependencies:
+    from2 "^1.3.0"
+    stream-iterate "^1.1.0"
+
 sortpack@^2.1.4:
-  version "2.1.4"
-  resolved "https://registry.yarnpkg.com/sortpack/-/sortpack-2.1.4.tgz#a2e251c5868455135cc41d3c98a53756a6de5282"
-  integrity sha512-RGD0l9kGmuPelXMT8WMMiSv1MkUkaqElB39nMkboIaqVkYns1aaNx263B2EE5QzF1YVUOrBlXnQpd7RX68SSow==
+  version "2.1.6"
+  resolved "https://registry.yarnpkg.com/sortpack/-/sortpack-2.1.6.tgz#fe6d1abb094a34d7501d91c17713b7ce659affc6"
+  integrity sha512-TkFfOF7ModMI7nKXprNGPsTZKUdqq5cUGrIIvyvcbxuPuV4dIDzjtRhn7LtYPKOUgmbRhPbAsLBCbOZtBQeQYQ==
 
 source-map-resolve@^0.5.0:
   version "0.5.3"
@@ -5809,10 +7583,10 @@ source-map-resolve@^0.5.0:
     source-map-url "^0.4.0"
     urix "^0.1.0"
 
-source-map-support@^0.5.6, source-map-support@~0.5.12:
-  version "0.5.16"
-  resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.16.tgz#0ae069e7fe3ba7538c64c98515e35339eac5a042"
-  integrity sha512-efyLRJDr68D9hBBNIPWFjhpFzURh+KJykQwvMyW5UiZzYwoF6l4YMMDIJJEyFWxWCqfyxLzz6tSfUFR+kXXsVQ==
+source-map-support@^0.5.17, source-map-support@^0.5.6, source-map-support@~0.5.12:
+  version "0.5.19"
+  resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.19.tgz#a98b62f86dcaf4f67399648c085291ab9e8fed61"
+  integrity sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==
   dependencies:
     buffer-from "^1.0.0"
     source-map "^0.6.0"
@@ -5845,22 +7619,22 @@ sourcemap-blender@1.0.5:
     source-map "^0.7.3"
 
 spdx-correct@^3.0.0:
-  version "3.1.0"
-  resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-3.1.0.tgz#fb83e504445268f154b074e218c87c003cd31df4"
-  integrity sha512-lr2EZCctC2BNR7j7WzJ2FpDznxky1sjfxvvYEyzxNyb6lZXHODmEoJeFu4JupYlkfha1KZpJyoqiJ7pgA1qq8Q==
+  version "3.1.1"
+  resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-3.1.1.tgz#dece81ac9c1e6713e5f7d1b6f17d468fa53d89a9"
+  integrity sha512-cOYcUWwhCuHCXi49RhFRCyJEK3iPj1Ziz9DpViV3tbZOwXD49QzIN3MpOLJNxh2qwq2lJJZaKMVw9qNi4jTC0w==
   dependencies:
     spdx-expression-parse "^3.0.0"
     spdx-license-ids "^3.0.0"
 
 spdx-exceptions@^2.1.0:
-  version "2.2.0"
-  resolved "https://registry.yarnpkg.com/spdx-exceptions/-/spdx-exceptions-2.2.0.tgz#2ea450aee74f2a89bfb94519c07fcd6f41322977"
-  integrity sha512-2XQACfElKi9SlVb1CYadKDXvoajPgBVPn/gOQLrTvHdElaVhr7ZEbqJaRnJLVNeaI4cMEAgVCeBMKF6MWRDCRA==
+  version "2.3.0"
+  resolved "https://registry.yarnpkg.com/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz#3f28ce1a77a00372683eade4a433183527a2163d"
+  integrity sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==
 
 spdx-expression-parse@^3.0.0:
-  version "3.0.0"
-  resolved "https://registry.yarnpkg.com/spdx-expression-parse/-/spdx-expression-parse-3.0.0.tgz#99e119b7a5da00e05491c9fa338b7904823b41d0"
-  integrity sha512-Yg6D3XpRD4kkOmTpdgbUiEJFKghJH03fiC1OPll5h/0sO6neh2jqRDVHOQ4o/LMea0tgCkbMgea5ip/e+MkWyg==
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz#cf70f50482eefdc98e3ce0a6833e4a53ceeba679"
+  integrity sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==
   dependencies:
     spdx-exceptions "^2.1.0"
     spdx-license-ids "^3.0.0"
@@ -5870,6 +7644,11 @@ spdx-license-ids@^3.0.0:
   resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.5.tgz#3694b5804567a458d3c8045842a6358632f62654"
   integrity sha512-J+FWzZoynJEXGphVIS+XEh3kFSjZX/1i9gFBaWQcB+/tmpe2qUsSBABpcxqxnAxFdiUFEgAX1bjYGQvIZmoz9Q==
 
+split-on-first@^1.0.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/split-on-first/-/split-on-first-1.1.0.tgz#f610afeee3b12bce1d0c30425e76398b78249a5f"
+  integrity sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw==
+
 split-string@^3.0.1, split-string@^3.0.2:
   version "3.1.0"
   resolved "https://registry.yarnpkg.com/split-string/-/split-string-3.1.0.tgz#7cb09dda3a86585705c64b39a6466038682e8fe2"
@@ -5897,10 +7676,26 @@ sshpk@^1.7.0:
     safer-buffer "^2.0.2"
     tweetnacl "~0.14.0"
 
-stack-utils@^1.0.1:
-  version "1.0.2"
-  resolved "https://registry.yarnpkg.com/stack-utils/-/stack-utils-1.0.2.tgz#33eba3897788558bebfc2db059dc158ec36cebb8"
-  integrity sha512-MTX+MeG5U994cazkjd/9KNAapsHnibjMLnfXodlkXw76JEea0UiNzrqidzo1emMwk7w5Qhc9jd4Bn9TBb1MFwA==
+ssri@^5.0.0, ssri@^5.2.4, ssri@^5.3.0:
+  version "5.3.0"
+  resolved "https://registry.yarnpkg.com/ssri/-/ssri-5.3.0.tgz#ba3872c9c6d33a0704a7d71ff045e5ec48999d06"
+  integrity sha512-XRSIPqLij52MtgoQavH/x/dU1qVKtWUAAZeOHsR9c2Ddi4XerFy3mc1alf+dLJKl9EUIm/Ht+EowFkTUOA6GAQ==
+  dependencies:
+    safe-buffer "^5.1.1"
+
+ssri@^6.0.0, ssri@^6.0.1:
+  version "6.0.1"
+  resolved "https://registry.yarnpkg.com/ssri/-/ssri-6.0.1.tgz#2a3c41b28dd45b62b63676ecb74001265ae9edd8"
+  integrity sha512-3Wge10hNcT1Kur4PDFwEieXSCMCJs/7WvSACcrMYrNp+b8kDL1/0wJch5Ni2WrtwEa2IO8OsVfeKIciKCDx/QA==
+  dependencies:
+    figgy-pudding "^3.5.1"
+
+stack-utils@^2.0.2:
+  version "2.0.2"
+  resolved "https://registry.yarnpkg.com/stack-utils/-/stack-utils-2.0.2.tgz#5cf48b4557becb4638d0bc4f21d23f5d19586593"
+  integrity sha512-0H7QK2ECz3fyZMzQ8rH0j2ykpfbnd20BFtfg/SqVC2+sCTtcw0aDTGB7dk+de4U4uUeuz6nOtJcrkFFLG1B0Rg==
+  dependencies:
+    escape-string-regexp "^2.0.0"
 
 static-extend@^0.1.1:
   version "0.1.2"
@@ -5928,18 +7723,44 @@ stream-browserify@^2.0.1:
     inherits "~2.0.1"
     readable-stream "^2.0.2"
 
+stream-each@^1.1.0:
+  version "1.2.3"
+  resolved "https://registry.yarnpkg.com/stream-each/-/stream-each-1.2.3.tgz#ebe27a0c389b04fbcc233642952e10731afa9bae"
+  integrity sha512-vlMC2f8I2u/bZGqkdfLQW/13Zihpej/7PmSiMQsbYddxuTsJp8vRe2x2FvVExZg7FaOds43ROAuFJwPR4MTZLw==
+  dependencies:
+    end-of-stream "^1.1.0"
+    stream-shift "^1.0.0"
+
+stream-iterate@^1.1.0:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/stream-iterate/-/stream-iterate-1.2.0.tgz#2bd7c77296c1702a46488b8ad41f79865eecd4e1"
+  integrity sha1-K9fHcpbBcCpGSIuK1B95hl7s1OE=
+  dependencies:
+    readable-stream "^2.1.5"
+    stream-shift "^1.0.0"
+
+stream-shift@^1.0.0:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/stream-shift/-/stream-shift-1.0.1.tgz#d7088281559ab2778424279b0877da3c392d5a3d"
+  integrity sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ==
+
+strict-uri-encode@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz#b9c7330c7042862f6b142dc274bbcc5866ce3546"
+  integrity sha1-ucczDHBChi9rFC3CdLvMWGbONUY=
+
 string-argv@0.3.1:
   version "0.3.1"
   resolved "https://registry.yarnpkg.com/string-argv/-/string-argv-0.3.1.tgz#95e2fbec0427ae19184935f816d74aaa4c5c19da"
   integrity sha512-a1uQGz7IyVy9YwhqjZIZu1c8JO8dNIe20xBmSS6qu9kv++k3JGzCVmprbNN5Kn+BgzD5E7YYwg1CcjuJMRNsvg==
 
-string-length@^3.1.0:
-  version "3.1.0"
-  resolved "https://registry.yarnpkg.com/string-length/-/string-length-3.1.0.tgz#107ef8c23456e187a8abd4a61162ff4ac6e25837"
-  integrity sha512-Ttp5YvkGm5v9Ijagtaz1BnN+k9ObpvS0eIBblPMp2YWL8FBmi9qblQ9fexc2k/CXFgrTIteU3jAw3payCnwSTA==
+string-length@^4.0.1:
+  version "4.0.1"
+  resolved "https://registry.yarnpkg.com/string-length/-/string-length-4.0.1.tgz#4a973bf31ef77c4edbceadd6af2611996985f8a1"
+  integrity sha512-PKyXUd0LK0ePjSOnWn34V2uD6acUWev9uy0Ft05k0E8xRW+SKcA0F7eMr7h5xlzfn+4O3N+55rduYyet3Jk+jw==
   dependencies:
-    astral-regex "^1.0.0"
-    strip-ansi "^5.2.0"
+    char-regex "^1.0.2"
+    strip-ansi "^6.0.0"
 
 string-width@^1.0.1:
   version "1.0.2"
@@ -5950,7 +7771,7 @@ string-width@^1.0.1:
     is-fullwidth-code-point "^1.0.0"
     strip-ansi "^3.0.0"
 
-string-width@^2.1.0, string-width@^2.1.1:
+"string-width@^1.0.2 || 2", string-width@^2.0.0, string-width@^2.1.0, string-width@^2.1.1:
   version "2.1.1"
   resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e"
   integrity sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==
@@ -5958,7 +7779,7 @@ string-width@^2.1.0, string-width@^2.1.1:
     is-fullwidth-code-point "^2.0.0"
     strip-ansi "^4.0.0"
 
-string-width@^3.0.0:
+string-width@^3.0.0, string-width@^3.1.0:
   version "3.1.0"
   resolved "https://registry.yarnpkg.com/string-width/-/string-width-3.1.0.tgz#22767be21b62af1081574306f69ac51b62203961"
   integrity sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==
@@ -5988,21 +7809,26 @@ string.prototype.matchall@^4.0.2:
     regexp.prototype.flags "^1.3.0"
     side-channel "^1.0.2"
 
-string.prototype.trimleft@^2.1.1:
-  version "2.1.1"
-  resolved "https://registry.yarnpkg.com/string.prototype.trimleft/-/string.prototype.trimleft-2.1.1.tgz#9bdb8ac6abd6d602b17a4ed321870d2f8dcefc74"
-  integrity sha512-iu2AGd3PuP5Rp7x2kEZCrB2Nf41ehzh+goo8TV7z8/XDBbsvc6HQIlUl9RjkZ4oyrW1XM5UwlGl1oVEaDjg6Ag==
+string.prototype.trimend@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.1.tgz#85812a6b847ac002270f5808146064c995fb6913"
+  integrity sha512-LRPxFUaTtpqYsTeNKaFOw3R4bxIzWOnbQ837QfBylo8jIxtcbK/A/sMV7Q+OAV/vWo+7s25pOE10KYSjaSO06g==
   dependencies:
     define-properties "^1.1.3"
-    function-bind "^1.1.1"
+    es-abstract "^1.17.5"
 
-string.prototype.trimright@^2.1.1:
-  version "2.1.1"
-  resolved "https://registry.yarnpkg.com/string.prototype.trimright/-/string.prototype.trimright-2.1.1.tgz#440314b15996c866ce8a0341894d45186200c5d9"
-  integrity sha512-qFvWL3/+QIgZXVmJBfpHmxLB7xsUXz6HsUmP8+5dRaC3Q7oKUv9Vo6aMCRZC1smrtyECFsIT30PqBJ1gTjAs+g==
+string.prototype.trimstart@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.1.tgz#14af6d9f34b053f7cfc89b72f8f2ee14b9039a54"
+  integrity sha512-XxZn+QpvrBI1FOcg6dIpxUPgWCPuNXvMD72aaRaUQv1eD4e/Qy8i/hFTe0BUmD60p/QA6bh1avmuPTfNjqVWRw==
   dependencies:
     define-properties "^1.1.3"
-    function-bind "^1.1.1"
+    es-abstract "^1.17.5"
+
+string_decoder@~0.10.x:
+  version "0.10.31"
+  resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94"
+  integrity sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=
 
 string_decoder@~1.1.1:
   version "1.1.1"
@@ -6027,14 +7853,14 @@ strip-ansi@^3.0.0, strip-ansi@^3.0.1:
   dependencies:
     ansi-regex "^2.0.0"
 
-strip-ansi@^4.0.0:
+strip-ansi@^4.0.0, strip-ansi@~4.0.0:
   version "4.0.0"
   resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-4.0.0.tgz#a8479022eb1ac368a871389b635262c505ee368f"
   integrity sha1-qEeQIusaw2iocTibY1JixQXuNo8=
   dependencies:
     ansi-regex "^3.0.0"
 
-strip-ansi@^5.1.0, strip-ansi@^5.2.0:
+strip-ansi@^5.0.0, strip-ansi@^5.1.0, strip-ansi@^5.2.0:
   version "5.2.0"
   resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-5.2.0.tgz#8c9a536feb6afc962bdfa5b104a5091c1ad9c0ae"
   integrity sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==
@@ -6068,15 +7894,15 @@ strip-final-newline@^2.0.0:
   resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz#89b852fb2fcbe936f6f4b3187afb0a12c1ab58ad"
   integrity sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==
 
-strip-json-comments@^3.0.1:
-  version "3.0.1"
-  resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.0.1.tgz#85713975a91fb87bf1b305cca77395e40d2a64a7"
-  integrity sha512-VTyMAUfdm047mwKl+u79WIdrZxtFtn+nBxHeb844XBQ9uMNTuTHdx2hc5RiAJYqwTj3wc/xe5HLSdJSkJ+WfZw==
+strip-json-comments@^3.1.0:
+  version "3.1.1"
+  resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006"
+  integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==
 
-supports-color@^2.0.0:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7"
-  integrity sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=
+strip-json-comments@~2.0.1:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a"
+  integrity sha1-PFMZQukIwml8DsNEhYwobHygpgo=
 
 supports-color@^5.3.0, supports-color@^5.4.0:
   version "5.5.0"
@@ -6100,12 +7926,12 @@ supports-hyperlinks@^2.0.0:
     has-flag "^4.0.0"
     supports-color "^7.0.0"
 
-symbol-observable@^1.1.0, symbol-observable@^1.2.0:
+symbol-observable@^1.2.0:
   version "1.2.0"
   resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.2.0.tgz#c22688aed4eab3cdc2dfeacbb561660560a00804"
   integrity sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==
 
-symbol-tree@^3.2.2:
+symbol-tree@^3.2.4:
   version "3.2.4"
   resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.4.tgz#430637d248ba77e078883951fb9aa0eed7c63fa2"
   integrity sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==
@@ -6120,6 +7946,35 @@ table@^5.2.3:
     slice-ansi "^2.1.0"
     string-width "^3.0.0"
 
+tar@^2.0.0:
+  version "2.2.2"
+  resolved "https://registry.yarnpkg.com/tar/-/tar-2.2.2.tgz#0ca8848562c7299b8b446ff6a4d60cdbb23edc40"
+  integrity sha512-FCEhQ/4rE1zYv9rYXJw/msRqsnmlje5jHP6huWeBZ704jUTy02c5AZyWujpMR1ax6mVw9NyJMfuK2CMDWVIfgA==
+  dependencies:
+    block-stream "*"
+    fstream "^1.0.12"
+    inherits "2"
+
+tar@^4.4.0, tar@^4.4.2, tar@^4.4.3, tar@^4.4.8:
+  version "4.4.13"
+  resolved "https://registry.yarnpkg.com/tar/-/tar-4.4.13.tgz#43b364bc52888d555298637b10d60790254ab525"
+  integrity sha512-w2VwSrBoHa5BsSyH+KxEqeQBAllHhccyMFVHtGtdMpF4W7IRWfZjFiQceJPChOeTsSDVUpER2T8FA93pr0L+QA==
+  dependencies:
+    chownr "^1.1.1"
+    fs-minipass "^1.2.5"
+    minipass "^2.8.6"
+    minizlib "^1.2.1"
+    mkdirp "^0.5.0"
+    safe-buffer "^5.1.2"
+    yallist "^3.0.3"
+
+term-size@^1.2.0:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/term-size/-/term-size-1.2.0.tgz#458b83887f288fc56d6fffbfad262e26638efa69"
+  integrity sha1-RYuDiH8oj8Vtb/+/rSYuJmOO+mk=
+  dependencies:
+    execa "^0.7.0"
+
 terminal-link@^2.0.0:
   version "2.1.1"
   resolved "https://registry.yarnpkg.com/terminal-link/-/terminal-link-2.1.1.tgz#14a64a27ab3c0df933ea546fba55f2d078edc994"
@@ -6129,9 +7984,9 @@ terminal-link@^2.0.0:
     supports-hyperlinks "^2.0.0"
 
 terser@^4.6.11:
-  version "4.6.11"
-  resolved "https://registry.yarnpkg.com/terser/-/terser-4.6.11.tgz#12ff99fdd62a26de2a82f508515407eb6ccd8a9f"
-  integrity sha512-76Ynm7OXUG5xhOpblhytE7X58oeNSmC8xnNhjWVo8CksHit0U0kO4hfNbPrrYwowLWFgM2n9L176VNx2QaHmtA==
+  version "4.8.0"
+  resolved "https://registry.yarnpkg.com/terser/-/terser-4.8.0.tgz#63056343d7c70bb29f3af665865a46fe03a0df17"
+  integrity sha512-EAPipTNeWsb/3wLPeup1tVPaXfIaU68xMnVdPafIL1TV05OhASArYyIfFvnvJCNrR2NIOvDVNNTFRa+Re2MWyw==
   dependencies:
     commander "^2.20.0"
     source-map "~0.6.1"
@@ -6146,7 +8001,7 @@ test-exclude@^6.0.0:
     glob "^7.1.4"
     minimatch "^3.0.4"
 
-text-table@^0.2.0:
+text-table@^0.2.0, text-table@~0.2.0:
   version "0.2.0"
   resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4"
   integrity sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=
@@ -6156,27 +8011,45 @@ throat@^5.0.0:
   resolved "https://registry.yarnpkg.com/throat/-/throat-5.0.0.tgz#c5199235803aad18754a667d659b5e72ce16764b"
   integrity sha512-fcwX4mndzpLQKBS1DVYhGAcYaYt7vsHNIvQV+WXMvnow5cgjPphq5CaayLaGsjRdSCKZFNGt7/GYAuXaNOiYCA==
 
-through@^2.3.6:
+through2@^2.0.0:
+  version "2.0.5"
+  resolved "https://registry.yarnpkg.com/through2/-/through2-2.0.5.tgz#01c1e39eb31d07cb7d03a96a70823260b23132cd"
+  integrity sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==
+  dependencies:
+    readable-stream "~2.3.6"
+    xtend "~4.0.1"
+
+"through@>=2.2.7 <3", through@^2.3.6, through@^2.3.8:
   version "2.3.8"
   resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5"
   integrity sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=
 
+timed-out@^4.0.0:
+  version "4.0.1"
+  resolved "https://registry.yarnpkg.com/timed-out/-/timed-out-4.0.1.tgz#f32eacac5a175bea25d7fab565ab3ed8741ef56f"
+  integrity sha1-8y6srFoXW+ol1/q1Zas+2HQe9W8=
+
 tiny-invariant@^1.0.2:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.1.0.tgz#634c5f8efdc27714b7f386c35e6760991d230875"
   integrity sha512-ytxQvrb1cPc9WBEI/HSeYYoGD0kWnGEOR8RY6KomWLBVhqz0RgTwVO9dLrGz7dC+nN9llyI7OKAgRq8Vq4ZBSw==
 
+tiny-relative-date@^1.3.0:
+  version "1.3.0"
+  resolved "https://registry.yarnpkg.com/tiny-relative-date/-/tiny-relative-date-1.3.0.tgz#fa08aad501ed730f31cc043181d995c39a935e07"
+  integrity sha512-MOQHpzllWxDCHHaDno30hhLfbouoYlOI8YlMNtvKe1zXbjEVhbcEovQxvZrPvtiYW630GQDoMMarCnjfyfHA+A==
+
 tiny-warning@^1.0.0:
   version "1.0.3"
   resolved "https://registry.yarnpkg.com/tiny-warning/-/tiny-warning-1.0.3.tgz#94a30db453df4c643d0fd566060d60a875d84754"
   integrity sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==
 
 tippy.js@^6.1.1:
-  version "6.1.1"
-  resolved "https://registry.yarnpkg.com/tippy.js/-/tippy.js-6.1.1.tgz#9ed09aa4f9c47fb06a0e280e03055f898f5ddfff"
-  integrity sha512-Sk+FPihack9XFbPOc2jRbn6iRLA9my2a8qhaGY6wwD3EeW57/xY5PAPkZOutKVYDWLyNZ/laCkJqg7QJG/gqQw==
+  version "6.2.5"
+  resolved "https://registry.yarnpkg.com/tippy.js/-/tippy.js-6.2.5.tgz#5335e28228af5e22c524fe5e8a94654514f34b92"
+  integrity sha512-UIf8G99PMXGmdWPrr36s/DjQBdfxMPwzvPUXsxs3tDFDTZ1SgvKG+Jvt6RJ+aBqYL0oe/STxh3MNkCV3IWAKmw==
   dependencies:
-    "@popperjs/core" "^2.2.0"
+    "@popperjs/core" "^2.4.4"
 
 tmp@^0.0.33:
   version "0.0.33"
@@ -6228,9 +8101,9 @@ to-regex@^3.0.1, to-regex@^3.0.2:
     safe-regex "^1.1.0"
 
 toastify-js@^1.7.0:
-  version "1.7.0"
-  resolved "https://registry.yarnpkg.com/toastify-js/-/toastify-js-1.7.0.tgz#d6b44937ae2844a19c25fcc69ee5933165dbf666"
-  integrity sha512-GmPy4zJ/ulCfmCHlfCtgcB+K2xhx2AXW3T/ZZOSjyjaIGevhz+uvR8HSCTay/wBq4tt2mUnBqlObP1sSWGlsnQ==
+  version "1.9.0"
+  resolved "https://registry.yarnpkg.com/toastify-js/-/toastify-js-1.9.0.tgz#726963ce850d914ee836951b5e79bdf641fe979e"
+  integrity sha512-v+Y/EUtRNdwtORfIF4oJIZ2BJWTT27Y/83ccVwJLI+wjz+dsyrjdWzC6awhfLWu8KOnfky/ac5tB1sz60fy6sQ==
 
 toidentifier@1.0.0:
   version "1.0.0"
@@ -6254,52 +8127,43 @@ tough-cookie@^3.0.1:
     psl "^1.1.28"
     punycode "^2.1.1"
 
-tough-cookie@~2.4.3:
-  version "2.4.3"
-  resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.4.3.tgz#53f36da3f47783b0925afa06ff9f3b165280f781"
-  integrity sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==
-  dependencies:
-    psl "^1.1.24"
-    punycode "^1.4.1"
-
-tr46@^1.0.1:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/tr46/-/tr46-1.0.1.tgz#a8b13fd6bfd2489519674ccde55ba3693b706d09"
-  integrity sha1-qLE/1r/SSJUZZ0zN5VujaTtwbQk=
+tr46@^2.0.2:
+  version "2.0.2"
+  resolved "https://registry.yarnpkg.com/tr46/-/tr46-2.0.2.tgz#03273586def1595ae08fedb38d7733cee91d2479"
+  integrity sha512-3n1qG+/5kg+jrbTzwAykB5yRYtQCTqOGKq5U5PE3b0a1/mzo6snDhjGS0zJVJunO0NrT3Dg1MLy5TjWP/UJppg==
   dependencies:
-    punycode "^2.1.0"
+    punycode "^2.1.1"
 
 tributejs@^5.1.3:
   version "5.1.3"
   resolved "https://registry.yarnpkg.com/tributejs/-/tributejs-5.1.3.tgz#980600fc72865be5868893078b4bfde721129eae"
   integrity sha512-B5CXihaVzXw+1UHhNFyAwUTMDk1EfoLP5Tj1VhD9yybZ1I8DZJEv8tZ1l0RJo0t0tk9ZhR8eG5tEsaCvRigmdQ==
 
-ts-jest@^25.4.0:
-  version "25.4.0"
-  resolved "https://registry.yarnpkg.com/ts-jest/-/ts-jest-25.4.0.tgz#5ad504299f8541d463a52e93e5e9d76876be0ba4"
-  integrity sha512-+0ZrksdaquxGUBwSdTIcdX7VXdwLIlSRsyjivVA9gcO+Cvr6ByqDhu/mi5+HCcb6cMkiQp5xZ8qRO7/eCqLeyw==
+ts-jest@^26.1.3:
+  version "26.1.3"
+  resolved "https://registry.yarnpkg.com/ts-jest/-/ts-jest-26.1.3.tgz#aac928a05fdf13e3e6dfbc8caec3847442667894"
+  integrity sha512-beUTSvuqR9SmKQEylewqJdnXWMVGJRFqSz2M8wKJe7GBMmLZ5zw6XXKSJckbHNMxn+zdB3guN2eOucSw2gBMnw==
   dependencies:
     bs-logger "0.x"
     buffer-from "1.x"
     fast-json-stable-stringify "2.x"
+    jest-util "26.x"
     json5 "2.x"
     lodash.memoize "4.x"
     make-error "1.x"
-    micromatch "4.x"
     mkdirp "1.x"
-    resolve "1.x"
-    semver "6.x"
+    semver "7.x"
     yargs-parser "18.x"
 
 ts-node@^8.8.2:
-  version "8.8.2"
-  resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-8.8.2.tgz#0b39e690bee39ea5111513a9d2bcdc0bc121755f"
-  integrity sha512-duVj6BpSpUpD/oM4MfhO98ozgkp3Gt9qIp3jGxwU2DFvl/3IRaEAvbLa8G60uS7C77457e/m5TMowjedeRxI1Q==
+  version "8.10.2"
+  resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-8.10.2.tgz#eee03764633b1234ddd37f8db9ec10b75ec7fb8d"
+  integrity sha512-ISJJGgkIpDdBhWVu3jufsWpK3Rzo7bdiIXJjQc0ynKxVOVcg2oIrf2H2cejminGrptVc6q6/uynAHNCuWGbpVA==
   dependencies:
     arg "^4.1.0"
     diff "^4.0.1"
     make-error "^1.1.1"
-    source-map-support "^0.5.6"
+    source-map-support "^0.5.17"
     yn "3.1.1"
 
 ts-transform-classcat@^1.0.0:
@@ -6312,10 +8176,20 @@ ts-transform-inferno@^4.0.3:
   resolved "https://registry.yarnpkg.com/ts-transform-inferno/-/ts-transform-inferno-4.0.3.tgz#2cc0eb125abdaff24b8298106a618ab7c6319edc"
   integrity sha512-Pcg0PVQwJ7Fpv4+3R9obFNsrNKQyLbmUqsjeG7T7r4/4UTgIl0MSwurexjtuGpCp2iv2X/i9ffKPAfAOyYJ9og==
 
+tsconfig-paths@^3.9.0:
+  version "3.9.0"
+  resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.9.0.tgz#098547a6c4448807e8fcb8eae081064ee9a3c90b"
+  integrity sha512-dRcuzokWhajtZWkQsDVKbWyY+jgcLC5sqJhg2PSgf4ZkH2aHPvaOY8YWGhmjb68b5qqTfasSsDO9k7RUiEmZAw==
+  dependencies:
+    "@types/json5" "^0.0.29"
+    json5 "^1.0.1"
+    minimist "^1.2.0"
+    strip-bom "^3.0.0"
+
 tslib@^1.8.0, tslib@^1.8.1, tslib@^1.9.0:
-  version "1.10.0"
-  resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.10.0.tgz#c3c19f95973fb0a62973fb09d90d961ee43e5c8a"
-  integrity sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ==
+  version "1.13.0"
+  resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.13.0.tgz#c881e13cc7015894ed914862d276436fa9a47043"
+  integrity sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q==
 
 tsutils@^3.17.1:
   version "3.17.1"
@@ -6336,6 +8210,13 @@ tweetnacl@^0.14.3, tweetnacl@~0.14.0:
   resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64"
   integrity sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=
 
+type-check@^0.4.0, type-check@~0.4.0:
+  version "0.4.0"
+  resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1"
+  integrity sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==
+  dependencies:
+    prelude-ls "^1.2.1"
+
 type-check@~0.3.2:
   version "0.3.2"
   resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.3.2.tgz#5884cab512cf1d355e3fb784f30804b2b520db72"
@@ -6348,6 +8229,11 @@ type-detect@4.0.8:
   resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c"
   integrity sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==
 
+type-fest@^0.11.0:
+  version "0.11.0"
+  resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.11.0.tgz#97abf0872310fed88a5c466b25681576145e33f1"
+  integrity sha512-OdjXJxnCN1AvyLSzeKIgXTXxV+99ZuXl3Hpo9XpJAv9MBcHrrJOQ5kV7ypXOuQie+AmWG25hLbiKdwYTifzcfQ==
+
 type-fest@^0.6.0:
   version "0.6.0"
   resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.6.0.tgz#8d2a2370d3df886eb5c90ada1c5bf6188acf838b"
@@ -6373,21 +8259,36 @@ typedarray-to-buffer@^3.1.5:
   dependencies:
     is-typedarray "^1.0.0"
 
+typedarray@^0.0.6:
+  version "0.0.6"
+  resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
+  integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=
+
 typescript@^3.8.3:
-  version "3.8.3"
-  resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.8.3.tgz#409eb8544ea0335711205869ec458ab109ee1061"
-  integrity sha512-MYlEfn5VrLNsgudQTVJeNaQFUAI7DkhnOjdpAp4T+ku1TfQClewlbSuTVHiA+8skNBgaf02TL/kLOvig4y3G8w==
+  version "3.9.7"
+  resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.9.7.tgz#98d600a5ebdc38f40cb277522f12dc800e9e25fa"
+  integrity sha512-BLbiRkiBzAwsjut4x/dsibSTB6yWpwT5qWmC2OfuCg3GgVQCSgMs4vEctYPhsaGtd0AeuuHMkjZ2h2WG8MSzRw==
 
 uc.micro@^1.0.1, uc.micro@^1.0.5:
   version "1.0.6"
   resolved "https://registry.yarnpkg.com/uc.micro/-/uc.micro-1.0.6.tgz#9c411a802a409a91fc6cf74081baba34b24499ac"
   integrity sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==
 
+uid-number@0.0.6:
+  version "0.0.6"
+  resolved "https://registry.yarnpkg.com/uid-number/-/uid-number-0.0.6.tgz#0ea10e8035e8eb5b8e4449f06da1c730663baa81"
+  integrity sha1-DqEOgDXo61uOREnwbaHHMGY7qoE=
+
 ultron@1.0.x:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/ultron/-/ultron-1.0.2.tgz#ace116ab557cd197386a4e88f4685378c8b2e4fa"
   integrity sha1-rOEWq1V80Zc4ak6I9GhTeMiy5Po=
 
+umask@^1.1.0, umask@~1.1.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/umask/-/umask-1.1.0.tgz#f29cebf01df517912bb58ff9c4e50fde8e33320d"
+  integrity sha1-8pzr8B31F5ErtY/5xOUP3o4zMg0=
+
 unicode-canonical-property-names-ecmascript@^1.0.4:
   version "1.0.4"
   resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-1.0.4.tgz#2619800c4c825800efdd8343af7dd9933cbe2818"
@@ -6401,15 +8302,15 @@ unicode-match-property-ecmascript@^1.0.4:
     unicode-canonical-property-names-ecmascript "^1.0.4"
     unicode-property-aliases-ecmascript "^1.0.4"
 
-unicode-match-property-value-ecmascript@^1.1.0:
-  version "1.1.0"
-  resolved "https://registry.yarnpkg.com/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-1.1.0.tgz#5b4b426e08d13a80365e0d657ac7a6c1ec46a277"
-  integrity sha512-hDTHvaBk3RmFzvSl0UVrUmC3PuW9wKVnpoUDYH0JDkSIovzw+J5viQmeYHxVSBptubnr7PbH2e0fnpDRQnQl5g==
+unicode-match-property-value-ecmascript@^1.2.0:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-1.2.0.tgz#0d91f600eeeb3096aa962b1d6fc88876e64ea531"
+  integrity sha512-wjuQHGQVofmSJv1uVISKLE5zO2rNGzM/KCYZch/QQvez7C1hUhBIuZ701fYXExuufJFMPhv2SyL8CyoIfMLbIQ==
 
 unicode-property-aliases-ecmascript@^1.0.4:
-  version "1.0.5"
-  resolved "https://registry.yarnpkg.com/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-1.0.5.tgz#a9cc6cc7ce63a0a3023fc99e341b94431d405a57"
-  integrity sha512-L5RAqCfXqAwR3RriF8pM0lU0w4Ryf/GgzONwi6KnL1taJQa7x1TCxdJnILX59WIGOwR57IVxn7Nej0fz1Ny6fw==
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-1.1.0.tgz#dd57a99f6207bedff4628abefb94c50db941c8f4"
+  integrity sha512-PqSoPh/pWetQ2phoj5RLiaqIk4kCNwoV3CI+LfGmWLKI3rE3kl1h59XpX2BjgDrmbxD9ARtQobPGU1SguCYuQg==
 
 union-value@^1.0.0:
   version "1.0.1"
@@ -6421,6 +8322,27 @@ union-value@^1.0.0:
     is-extendable "^0.1.1"
     set-value "^2.0.1"
 
+unique-filename@^1.1.0, unique-filename@^1.1.1, unique-filename@~1.1.0:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/unique-filename/-/unique-filename-1.1.1.tgz#1d69769369ada0583103a1e6ae87681b56573230"
+  integrity sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ==
+  dependencies:
+    unique-slug "^2.0.0"
+
+unique-slug@^2.0.0:
+  version "2.0.2"
+  resolved "https://registry.yarnpkg.com/unique-slug/-/unique-slug-2.0.2.tgz#baabce91083fc64e945b0f3ad613e264f7cd4e6c"
+  integrity sha512-zoWr9ObaxALD3DOPfjPSqxt4fnZiWblxHIgeWqW8x7UqDzEtHEQLzji2cuJYQFCU6KmoJikOYAZlrTHHebjx2w==
+  dependencies:
+    imurmurhash "^0.1.4"
+
+unique-string@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/unique-string/-/unique-string-1.0.0.tgz#9e1057cca851abb93398f8b33ae187b99caec11a"
+  integrity sha1-nhBXzKhRq7kzmPizOuGHuZyuwRo=
+  dependencies:
+    crypto-random-string "^1.0.0"
+
 universalify@^0.1.0:
   version "0.1.2"
   resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66"
@@ -6439,6 +8361,27 @@ unset-value@^1.0.0:
     has-value "^0.3.1"
     isobject "^3.0.0"
 
+unzip-response@^2.0.1:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/unzip-response/-/unzip-response-2.0.1.tgz#d2f0f737d16b0615e72a6935ed04214572d56f97"
+  integrity sha1-0vD3N9FrBhXnKmk17QQhRXLVb5c=
+
+update-notifier@^2.2.0, update-notifier@^2.3.0, update-notifier@^2.5.0:
+  version "2.5.0"
+  resolved "https://registry.yarnpkg.com/update-notifier/-/update-notifier-2.5.0.tgz#d0744593e13f161e406acb1d9408b72cad08aff6"
+  integrity sha512-gwMdhgJHGuj/+wHJJs9e6PcCszpxR1b236igrOkUofGhqJuG+amlIKwApH1IW1WWl7ovZxsX49lMBWLxSdm5Dw==
+  dependencies:
+    boxen "^1.2.1"
+    chalk "^2.0.1"
+    configstore "^3.0.0"
+    import-lazy "^2.1.0"
+    is-ci "^1.0.10"
+    is-installed-globally "^0.1.0"
+    is-npm "^1.0.0"
+    latest-version "^3.0.0"
+    semver-diff "^2.0.0"
+    xdg-basedir "^3.0.0"
+
 uri-js@^4.2.2:
   version "4.2.2"
   resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.2.2.tgz#94c540e1ff772956e2299507c010aea6c8838eb0"
@@ -6451,6 +8394,13 @@ urix@^0.1.0:
   resolved "https://registry.yarnpkg.com/urix/-/urix-0.1.0.tgz#da937f7a62e21fec1fd18d49b35c2935067a6c72"
   integrity sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=
 
+url-parse-lax@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/url-parse-lax/-/url-parse-lax-1.0.0.tgz#7af8f303645e9bd79a272e7a14ac68bc0609da73"
+  integrity sha1-evjzA2Rem9eaJy56FKxovAYJ2nM=
+  dependencies:
+    prepend-http "^1.0.1"
+
 use@^3.1.0:
   version "3.1.1"
   resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f"
@@ -6461,6 +8411,18 @@ util-deprecate@~1.0.1:
   resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
   integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=
 
+util-extend@^1.0.1:
+  version "1.0.3"
+  resolved "https://registry.yarnpkg.com/util-extend/-/util-extend-1.0.3.tgz#a7c216d267545169637b3b6edc6ca9119e2ff93f"
+  integrity sha1-p8IW0mdUUWljeztu3GypEZ4v+T8=
+
+util-promisify@^2.1.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/util-promisify/-/util-promisify-2.1.0.tgz#3c2236476c4d32c5ff3c47002add7c13b9a82a53"
+  integrity sha1-PCI2R2xNMsX/PEcAKt18E7moKlM=
+  dependencies:
+    object.getownpropertydescriptors "^2.0.3"
+
 utils-extend@^1.0.4, utils-extend@^1.0.6, utils-extend@^1.0.7:
   version "1.0.8"
   resolved "https://registry.yarnpkg.com/utils-extend/-/utils-extend-1.0.8.tgz#ccfd7b64540f8e90ee21eec57769d0651cab8a5f"
@@ -6471,26 +8433,31 @@ utils-merge@1.0.1:
   resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713"
   integrity sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=
 
-uuid@^3.3.2:
+uuid@^3.2.1, uuid@^3.3.2:
   version "3.4.0"
   resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee"
   integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==
 
+uuid@^8.2.0:
+  version "8.2.0"
+  resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.2.0.tgz#cb10dd6b118e2dada7d0cd9730ba7417c93d920e"
+  integrity sha512-CYpGiFTUrmI6OBMkAdjSDM0k5h8SkkiTP4WAjQgDgNB1S3Ou9VBEvr6q0Kv2H1mMk7IWfxYGpMH5sd5AvcIV2Q==
+
 v8-compile-cache@^2.0.3:
-  version "2.1.0"
-  resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.1.0.tgz#e14de37b31a6d194f5690d67efc4e7f6fc6ab30e"
-  integrity sha512-usZBT3PW+LOjM25wbqIlZwPeJV+3OSz3M1k1Ws8snlW39dZyYL9lOGC5FgPVHfk0jKmjiDV8Z0mIbVQPiwFs7g==
+  version "2.1.1"
+  resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.1.1.tgz#54bc3cdd43317bca91e35dcaf305b1a7237de745"
+  integrity sha512-8OQ9CL+VWyt3JStj7HX7/ciTL2V3Rl1Wf5OL+SNTm0yK1KvtReVulksyeRnCANHHuUxHlQig+JJDlUhBt1NQDQ==
 
 v8-to-istanbul@^4.1.3:
-  version "4.1.3"
-  resolved "https://registry.yarnpkg.com/v8-to-istanbul/-/v8-to-istanbul-4.1.3.tgz#22fe35709a64955f49a08a7c7c959f6520ad6f20"
-  integrity sha512-sAjOC+Kki6aJVbUOXJbcR0MnbfjvBzwKZazEJymA2IX49uoOdEdk+4fBq5cXgYgiyKtAyrrJNtBZdOeDIF+Fng==
+  version "4.1.4"
+  resolved "https://registry.yarnpkg.com/v8-to-istanbul/-/v8-to-istanbul-4.1.4.tgz#b97936f21c0e2d9996d4985e5c5156e9d4e49cd6"
+  integrity sha512-Rw6vJHj1mbdK8edjR7+zuJrpDtKIgNdAvTSAcpYfgMIw+u2dPDntD3dgN4XQFLU2/fvFQdzj+EeSGfd/jnY5fQ==
   dependencies:
     "@types/istanbul-lib-coverage" "^2.0.1"
     convert-source-map "^1.6.0"
     source-map "^0.7.3"
 
-validate-npm-package-license@^3.0.1:
+validate-npm-package-license@^3.0.1, validate-npm-package-license@^3.0.3:
   version "3.0.4"
   resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz#fc91f6b9c7ba15c857f4cb2c5defeec39d4f410a"
   integrity sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==
@@ -6498,6 +8465,13 @@ validate-npm-package-license@^3.0.1:
     spdx-correct "^3.0.0"
     spdx-expression-parse "^3.0.0"
 
+validate-npm-package-name@^3.0.0, validate-npm-package-name@~3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/validate-npm-package-name/-/validate-npm-package-name-3.0.0.tgz#5fa912d81eb7d0c74afc140de7317f0ca7df437e"
+  integrity sha1-X6kS2B630MdK/BQN5zF/DKffQ34=
+  dependencies:
+    builtins "^1.0.3"
+
 value-equal@^1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/value-equal/-/value-equal-1.0.1.tgz#1e0b794c734c5c0cade179c437d356d931a34d6c"
@@ -6522,20 +8496,18 @@ void-elements@^2.0.1:
   resolved "https://registry.yarnpkg.com/void-elements/-/void-elements-2.0.1.tgz#c066afb582bb1cb4128d60ea92392e94d5e9dbec"
   integrity sha1-wGavtYK7HLQSjWDqkjkulNXp2+w=
 
-w3c-hr-time@^1.0.1:
+w3c-hr-time@^1.0.2:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz#0a89cdf5cc15822df9c360543676963e0cc308cd"
   integrity sha512-z8P5DvDNjKDoFIHK7q8r8lackT6l+jo/Ye3HOle7l9nICP9lf1Ci25fy9vHd0JOWewkIFzXIEig3TdKT7JQ5fQ==
   dependencies:
     browser-process-hrtime "^1.0.0"
 
-w3c-xmlserializer@^1.1.2:
-  version "1.1.2"
-  resolved "https://registry.yarnpkg.com/w3c-xmlserializer/-/w3c-xmlserializer-1.1.2.tgz#30485ca7d70a6fd052420a3d12fd90e6339ce794"
-  integrity sha512-p10l/ayESzrBMYWRID6xbuCKh2Fp77+sA0doRuGn4tTIMrrZVeqfpKjXHY+oDh3K4nLdPgNwMTVP6Vp4pvqbNg==
+w3c-xmlserializer@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/w3c-xmlserializer/-/w3c-xmlserializer-2.0.0.tgz#3e7104a05b75146cc60f564380b7f683acf1020a"
+  integrity sha512-4tzD0mF8iSiMiNs30BiLO3EpfGLZUT2MSX/G+o7ZywDzliWQ3OPtTZ0PTC3B3ca1UAf4cJMHB+2Bf56EriJuRA==
   dependencies:
-    domexception "^1.0.1"
-    webidl-conversions "^4.0.2"
     xml-name-validator "^3.0.0"
 
 walker@^1.0.7, walker@~1.0.5:
@@ -6553,31 +8525,43 @@ watch@^1.0.1:
     exec-sh "^0.2.0"
     minimist "^1.2.0"
 
-webidl-conversions@^4.0.2:
-  version "4.0.2"
-  resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-4.0.2.tgz#a855980b1f0b6b359ba1d5d9fb39ae941faa63ad"
-  integrity sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==
+wcwidth@^1.0.0:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/wcwidth/-/wcwidth-1.0.1.tgz#f0b0dcf915bc5ff1528afadb2c0e17b532da2fe8"
+  integrity sha1-8LDc+RW8X/FSivrbLA4XtTLaL+g=
+  dependencies:
+    defaults "^1.0.3"
+
+webidl-conversions@^5.0.0:
+  version "5.0.0"
+  resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-5.0.0.tgz#ae59c8a00b121543a2acc65c0434f57b0fc11aff"
+  integrity sha512-VlZwKPCkYKxQgeSbH5EyngOmRp7Ww7I9rQLERETtf5ofd9pGeswWiOtogpEO850jziPRarreGxn5QIiTqpb2wA==
 
-whatwg-encoding@^1.0.1, whatwg-encoding@^1.0.5:
+webidl-conversions@^6.1.0:
+  version "6.1.0"
+  resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-6.1.0.tgz#9111b4d7ea80acd40f5270d666621afa78b69514"
+  integrity sha512-qBIvFLGiBpLjfwmYAaHPXsn+ho5xZnGvyGvsarywGNc8VyQJUMHJ8OBKGGrPER0okBeMDaan4mNBlgBROxuI8w==
+
+whatwg-encoding@^1.0.5:
   version "1.0.5"
   resolved "https://registry.yarnpkg.com/whatwg-encoding/-/whatwg-encoding-1.0.5.tgz#5abacf777c32166a51d085d6b4f3e7d27113ddb0"
   integrity sha512-b5lim54JOPN9HtzvK9HFXvBma/rnfFeqsic0hSpjtDbVxR3dJKLc+KB4V6GgiGOvl7CY/KNh8rxSo9DKQrnUEw==
   dependencies:
     iconv-lite "0.4.24"
 
-whatwg-mimetype@^2.2.0, whatwg-mimetype@^2.3.0:
+whatwg-mimetype@^2.3.0:
   version "2.3.0"
   resolved "https://registry.yarnpkg.com/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz#3d4b1e0312d2079879f826aff18dbeeca5960fbf"
   integrity sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g==
 
-whatwg-url@^7.0.0:
-  version "7.1.0"
-  resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-7.1.0.tgz#c2c492f1eca612988efd3d2266be1b9fc6170d06"
-  integrity sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==
+whatwg-url@^8.0.0:
+  version "8.1.0"
+  resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-8.1.0.tgz#c628acdcf45b82274ce7281ee31dd3c839791771"
+  integrity sha512-vEIkwNi9Hqt4TV9RdnaBPNt+E2Sgmo3gePebCRgZ1R7g6d23+53zCTnuB0amKI4AXq6VM8jj2DUAa0S1vjJxkw==
   dependencies:
     lodash.sortby "^4.7.0"
-    tr46 "^1.0.1"
-    webidl-conversions "^4.0.2"
+    tr46 "^2.0.2"
+    webidl-conversions "^5.0.0"
 
 which-module@^2.0.0:
   version "2.0.0"
@@ -6589,7 +8573,7 @@ which-pm-runs@^1.0.0:
   resolved "https://registry.yarnpkg.com/which-pm-runs/-/which-pm-runs-1.0.0.tgz#670b3afbc552e0b55df6b7780ca74615f23ad1cb"
   integrity sha1-Zws6+8VS4LVd9rd4DKdGFfI60cs=
 
-which@^1.2.9, which@^1.3.1:
+which@1, which@^1.2.9, which@^1.3.0, which@^1.3.1, which@~1.3.0:
   version "1.3.1"
   resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a"
   integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==
@@ -6603,18 +8587,48 @@ which@^2.0.1, which@^2.0.2:
   dependencies:
     isexe "^2.0.0"
 
-word-wrap@~1.2.3:
+wide-align@^1.1.0:
+  version "1.1.3"
+  resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.3.tgz#ae074e6bdc0c14a431e804e624549c633b000457"
+  integrity sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==
+  dependencies:
+    string-width "^1.0.2 || 2"
+
+widest-line@^2.0.0:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/widest-line/-/widest-line-2.0.1.tgz#7438764730ec7ef4381ce4df82fb98a53142a3fc"
+  integrity sha512-Ba5m9/Fa4Xt9eb2ELXt77JxVDV8w7qQrH0zS/TWSJdLyAwQjWoOzpzj5lwVftDz6n/EOu3tNACS84v509qwnJA==
+  dependencies:
+    string-width "^2.1.1"
+
+word-wrap@^1.2.3, word-wrap@~1.2.3:
   version "1.2.3"
   resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c"
   integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==
 
-wrap-ansi@^3.0.1:
-  version "3.0.1"
-  resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-3.0.1.tgz#288a04d87eda5c286e060dfe8f135ce8d007f8ba"
-  integrity sha1-KIoE2H7aXChuBg3+jxNc6NAH+Lo=
+worker-farm@^1.6.0:
+  version "1.7.0"
+  resolved "https://registry.yarnpkg.com/worker-farm/-/worker-farm-1.7.0.tgz#26a94c5391bbca926152002f69b84a4bf772e5a8"
+  integrity sha512-rvw3QTZc8lAxyVrqcSGVm5yP/IJ2UcB3U0graE3LCFoZ0Yn2x4EoVSqJKdB/T5M+FLcRPjz4TDacRf3OCfNUzw==
   dependencies:
-    string-width "^2.1.1"
-    strip-ansi "^4.0.0"
+    errno "~0.1.7"
+
+wrap-ansi@^2.0.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-2.1.0.tgz#d8fc3d284dd05794fe84973caecdd1cf824fdd85"
+  integrity sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=
+  dependencies:
+    string-width "^1.0.1"
+    strip-ansi "^3.0.1"
+
+wrap-ansi@^5.1.0:
+  version "5.1.0"
+  resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-5.1.0.tgz#1fd1f67235d5b6d0fee781056001bfb694c03b09"
+  integrity sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==
+  dependencies:
+    ansi-styles "^3.2.0"
+    string-width "^3.0.0"
+    strip-ansi "^5.0.0"
 
 wrap-ansi@^6.2.0:
   version "6.2.0"
@@ -6625,11 +8639,20 @@ wrap-ansi@^6.2.0:
     string-width "^4.1.0"
     strip-ansi "^6.0.0"
 
-wrappy@1:
+wrappy@1, wrappy@~1.0.2:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
   integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=
 
+write-file-atomic@^2.0.0, write-file-atomic@^2.3.0:
+  version "2.4.3"
+  resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-2.4.3.tgz#1fd2e9ae1df3e75b8d8c367443c692d4ca81f481"
+  integrity sha512-GaETH5wwsX+GcnzhPgKcKjJ6M2Cq3/iZp1WyY/X1CSqrW+jVNM9Y7D8EC2sM4ZG/V8wZlSniJnCKWPmBYAucRQ==
+  dependencies:
+    graceful-fs "^4.1.11"
+    imurmurhash "^0.1.4"
+    signal-exit "^3.0.2"
+
 write-file-atomic@^3.0.0:
   version "3.0.3"
   resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-3.0.3.tgz#56bd5c5a5c70481cd19c571bd39ab965a5de56e8"
@@ -6655,17 +8678,22 @@ ws@^1.1.1:
     options ">=0.0.5"
     ultron "1.0.x"
 
-ws@^7.0.0, ws@^7.2.3:
-  version "7.2.3"
-  resolved "https://registry.yarnpkg.com/ws/-/ws-7.2.3.tgz#a5411e1fb04d5ed0efee76d26d5c46d830c39b46"
-  integrity sha512-HTDl9G9hbkNDk98naoR/cHDws7+EyYMOdL1BmjsZXRUjf7d+MficC4B7HLUPlSiho0vg+CWKrGIt/VJBd1xunQ==
+ws@^7.2.3:
+  version "7.3.1"
+  resolved "https://registry.yarnpkg.com/ws/-/ws-7.3.1.tgz#d0547bf67f7ce4f12a72dfe31262c68d7dc551c8"
+  integrity sha512-D3RuNkynyHmEJIpD2qrgVkc9DQ23OrN/moAwZX4L8DfvszsJxpjQuUq3LMx6HoYji9fbIOBY18XWBsAux1ZZUA==
+
+xdg-basedir@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/xdg-basedir/-/xdg-basedir-3.0.0.tgz#496b2cc109eca8dbacfe2dc72b603c17c5870ad4"
+  integrity sha1-SWsswQnsqNus/i3HK2A8F8WHCtQ=
 
 xml-name-validator@^3.0.0:
   version "3.0.0"
   resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-3.0.0.tgz#6ae73e06de4d8c6e47f9fb181f78d648ad457c6a"
   integrity sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw==
 
-xmlchars@^2.1.1:
+xmlchars@^2.2.0:
   version "2.2.0"
   resolved "https://registry.yarnpkg.com/xmlchars/-/xmlchars-2.2.0.tgz#060fe1bcb7f9c76fe2a17db86a9bc3ab894210cb"
   integrity sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==
@@ -6677,19 +8705,37 @@ xregexp@^4.3.0:
   dependencies:
     "@babel/runtime-corejs3" "^7.8.3"
 
+xtend@~4.0.1:
+  version "4.0.2"
+  resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54"
+  integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==
+
+y18n@^3.2.1:
+  version "3.2.1"
+  resolved "https://registry.yarnpkg.com/y18n/-/y18n-3.2.1.tgz#6d15fba884c08679c0d77e88e7759e811e07fa41"
+  integrity sha1-bRX7qITAhnnA136I53WegR4H+kE=
+
 y18n@^4.0.0:
   version "4.0.0"
   resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.0.tgz#95ef94f85ecc81d007c264e190a120f0a3c8566b"
   integrity sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==
 
+yallist@^2.1.2:
+  version "2.1.2"
+  resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52"
+  integrity sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=
+
+yallist@^3.0.0, yallist@^3.0.2, yallist@^3.0.3:
+  version "3.1.1"
+  resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd"
+  integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==
+
 yaml@^1.7.2:
-  version "1.7.2"
-  resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.7.2.tgz#f26aabf738590ab61efaca502358e48dc9f348b2"
-  integrity sha512-qXROVp90sb83XtAoqE8bP9RwAkTTZbugRUTm5YeFCBfNRPEp2YzTeqWiz7m5OORHzEvrA/qcGS8hp/E+MMROYw==
-  dependencies:
-    "@babel/runtime" "^7.6.3"
+  version "1.10.0"
+  resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.0.tgz#3b593add944876077d4d683fee01081bd9fff31e"
+  integrity sha512-yr2icI4glYaNG+KWONODapy2/jDdMSDnrONSjblABjD9B4Z5LgiircSt8m8sRZFNi08kG9Sm0uSHtEmP3zaEGg==
 
-yargs-parser@18.x, yargs-parser@^18.1.1:
+yargs-parser@18.x, yargs-parser@^18.1.2:
   version "18.1.3"
   resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-18.1.3.tgz#be68c4975c6b2abf469236b0c870362fab09a7b0"
   integrity sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==
@@ -6697,10 +8743,42 @@ yargs-parser@18.x, yargs-parser@^18.1.1:
     camelcase "^5.0.0"
     decamelize "^1.2.0"
 
+yargs-parser@^15.0.1:
+  version "15.0.1"
+  resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-15.0.1.tgz#54786af40b820dcb2fb8025b11b4d659d76323b3"
+  integrity sha512-0OAMV2mAZQrs3FkNpDQcBk1x5HXb8X4twADss4S0Iuk+2dGnLOE/fRHrsYm542GduMveyA77OF4wrNJuanRCWw==
+  dependencies:
+    camelcase "^5.0.0"
+    decamelize "^1.2.0"
+
+yargs-parser@^7.0.0:
+  version "7.0.0"
+  resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-7.0.0.tgz#8d0ac42f16ea55debd332caf4c4038b3e3f5dfd9"
+  integrity sha1-jQrELxbqVd69MyyvTEA4s+P139k=
+  dependencies:
+    camelcase "^4.1.0"
+
+yargs@^14.2.3:
+  version "14.2.3"
+  resolved "https://registry.yarnpkg.com/yargs/-/yargs-14.2.3.tgz#1a1c3edced1afb2a2fea33604bc6d1d8d688a414"
+  integrity sha512-ZbotRWhF+lkjijC/VhmOT9wSgyBQ7+zr13+YLkhfsSiTriYsMzkTUFP18pFhWwBeMa5gUc1MzbhrO6/VB7c9Xg==
+  dependencies:
+    cliui "^5.0.0"
+    decamelize "^1.2.0"
+    find-up "^3.0.0"
+    get-caller-file "^2.0.1"
+    require-directory "^2.1.1"
+    require-main-filename "^2.0.0"
+    set-blocking "^2.0.0"
+    string-width "^3.0.0"
+    which-module "^2.0.0"
+    y18n "^4.0.0"
+    yargs-parser "^15.0.1"
+
 yargs@^15.3.1:
-  version "15.3.1"
-  resolved "https://registry.yarnpkg.com/yargs/-/yargs-15.3.1.tgz#9505b472763963e54afe60148ad27a330818e98b"
-  integrity sha512-92O1HWEjw27sBfgmXiixJWT5hRBp2eobqXicLtPBIDBhYB+1HpwZlXmbW2luivBJHBzki+7VyCLRtAkScbTBQA==
+  version "15.4.1"
+  resolved "https://registry.yarnpkg.com/yargs/-/yargs-15.4.1.tgz#0d87a16de01aee9d8bec2bfbf74f67851730f4f8"
+  integrity sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==
   dependencies:
     cliui "^6.0.0"
     decamelize "^1.2.0"
@@ -6712,7 +8790,26 @@ yargs@^15.3.1:
     string-width "^4.2.0"
     which-module "^2.0.0"
     y18n "^4.0.0"
-    yargs-parser "^18.1.1"
+    yargs-parser "^18.1.2"
+
+yargs@^8.0.2:
+  version "8.0.2"
+  resolved "https://registry.yarnpkg.com/yargs/-/yargs-8.0.2.tgz#6299a9055b1cefc969ff7e79c1d918dceb22c360"
+  integrity sha1-YpmpBVsc78lp/355wdkY3Osiw2A=
+  dependencies:
+    camelcase "^4.1.0"
+    cliui "^3.2.0"
+    decamelize "^1.1.1"
+    get-caller-file "^1.0.1"
+    os-locale "^2.0.0"
+    read-pkg-up "^2.0.0"
+    require-directory "^2.1.1"
+    require-main-filename "^1.0.1"
+    set-blocking "^2.0.0"
+    string-width "^2.0.0"
+    which-module "^2.0.0"
+    y18n "^3.2.1"
+    yargs-parser "^7.0.0"
 
 yn@3.1.1:
   version "3.1.1"