]> Untitled Git - lemmy.git/commitdiff
Merge branch 'master' into federation_merge_from_master_2
authorDessalines <tyhou13@gmx.com>
Wed, 24 Jun 2020 01:11:38 +0000 (21:11 -0400)
committerDessalines <tyhou13@gmx.com>
Wed, 24 Jun 2020 01:11:38 +0000 (21:11 -0400)
108 files changed:
.dockerignore
.gitignore
README.md
docker/dev/test_deploy.sh
docker/federation-test/run-tests.sh [new file with mode: 0755]
docker/federation/Dockerfile [new file with mode: 0644]
docker/federation/docker-compose.yml [new file with mode: 0644]
docker/federation/nginx.conf [new file with mode: 0644]
docker/federation/run-federation-test.bash [new file with mode: 0755]
docs/src/about_guide.md
docs/src/contributing_federation_development.md
server/.rustfmt.toml
server/Cargo.lock
server/Cargo.toml
server/config/config.hjson [new file with mode: 0644]
server/config/defaults.hjson
server/migrations/2020-03-26-192410_add_activitypub_tables/down.sql [new file with mode: 0644]
server/migrations/2020-03-26-192410_add_activitypub_tables/up.sql [new file with mode: 0644]
server/migrations/2020-04-03-194936_add_activitypub_for_posts_and_comments/down.sql [new file with mode: 0644]
server/migrations/2020-04-03-194936_add_activitypub_for_posts_and_comments/up.sql [new file with mode: 0644]
server/migrations/2020-04-07-135912_add_user_community_apub_constraints/down.sql [new file with mode: 0644]
server/migrations/2020-04-07-135912_add_user_community_apub_constraints/up.sql [new file with mode: 0644]
server/migrations/2020-04-14-163701_update_views_for_activitypub/down.sql [new file with mode: 0644]
server/migrations/2020-04-14-163701_update_views_for_activitypub/up.sql [new file with mode: 0644]
server/migrations/2020-04-21-123957_remove_unique_user_constraints/down.sql [new file with mode: 0644]
server/migrations/2020-04-21-123957_remove_unique_user_constraints/up.sql [new file with mode: 0644]
server/migrations/2020-05-05-210233_add_activitypub_for_private_messages/down.sql [new file with mode: 0644]
server/migrations/2020-05-05-210233_add_activitypub_for_private_messages/up.sql [new file with mode: 0644]
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 [new file with mode: 0644]
server/src/apub/comment.rs [new file with mode: 0644]
server/src/apub/community.rs
server/src/apub/community_inbox.rs [new file with mode: 0644]
server/src/apub/extensions/group_extensions.rs [new file with mode: 0644]
server/src/apub/extensions/mod.rs [new file with mode: 0644]
server/src/apub/extensions/page_extension.rs [new file with mode: 0644]
server/src/apub/extensions/signatures.rs [new file with mode: 0644]
server/src/apub/fetcher.rs [new file with mode: 0644]
server/src/apub/mod.rs
server/src/apub/post.rs
server/src/apub/private_message.rs [new file with mode: 0644]
server/src/apub/shared_inbox.rs [new file with mode: 0644]
server/src/apub/user.rs
server/src/apub/user_inbox.rs [new file with mode: 0644]
server/src/db/activity.rs [new file with mode: 0644]
server/src/db/category.rs
server/src/db/code_migrations.rs [new file with mode: 0644]
server/src/db/comment.rs
server/src/db/comment_view.rs
server/src/db/community.rs
server/src/db/community_view.rs
server/src/db/mod.rs
server/src/db/moderator.rs
server/src/db/moderator_views.rs
server/src/db/password_reset_request.rs
server/src/db/post.rs
server/src/db/post_view.rs
server/src/db/private_message.rs
server/src/db/private_message_view.rs
server/src/db/site.rs
server/src/db/site_view.rs
server/src/db/user.rs
server/src/db/user_mention.rs
server/src/db/user_mention_view.rs
server/src/db/user_view.rs
server/src/lib.rs
server/src/main.rs
server/src/rate_limit/mod.rs
server/src/rate_limit/rate_limiter.rs
server/src/routes/api.rs
server/src/routes/federation.rs
server/src/routes/feeds.rs
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/schema.rs
server/src/settings.rs
server/src/websocket/mod.rs
server/src/websocket/server.rs
ui/.eslintignore
ui/.gitignore
ui/jest.config.js [new file with mode: 0644]
ui/package.json
ui/src/api_tests/api.spec.ts [new file with mode: 0644]
ui/src/components/admin-settings.tsx
ui/src/components/comment-node.tsx
ui/src/components/communities.tsx
ui/src/components/community-link.tsx [new file with mode: 0644]
ui/src/components/community.tsx
ui/src/components/main.tsx
ui/src/components/post-form.tsx
ui/src/components/post-listing.tsx
ui/src/components/private-message-form.tsx
ui/src/components/search.tsx
ui/src/components/sidebar.tsx
ui/src/components/user-listing.tsx
ui/src/components/user.tsx
ui/src/env.ts
ui/src/interfaces.ts
ui/src/utils.ts
ui/yarn.lock

index a29cd2ec2f54f23b14ae40c0e9d6318e18a2c44a..255caf67fed0ec280ea4e62e8dba272871a1d61d 100644 (file)
@@ -1,6 +1,12 @@
+# build folders and similar which are not needed for the docker build
 ui/node_modules
-ui/dist
 server/target
 docker/dev/volumes
 docker/federation/volumes
+docker/federation-test/volumes
 .git
+ansible
+
+# exceptions, needed for federation-test build
+
+!server/target/debug/lemmy_server
index 9f7fa1e3c6930299976d9bab212b53ec509d4d9c..236a729eb05f97bf7ade4a0f8a904c491905039f 100644 (file)
@@ -1,10 +1,18 @@
+# local ansible configuration
 ansible/inventory
 ansible/inventory_dev
 ansible/passwords/
+
+# docker build files
 docker/lemmy_mine.hjson
 docker/dev/env_deploy.sh
+docker/federation/volumes
+docker/federation-test/volumes
+docker/dev/volumes
+
+# local build files
 build/
-.idea/
 ui/src/translations
-docker/dev/volumes
-docker/federation-test/volumes
+
+# ide config
+.idea/
index 09b2540f0a8e9eba4488b31b9db377db076dfb85..41ba480991f46cf0188277734b11cea510eb58b3 100644 (file)
--- a/README.md
+++ b/README.md
@@ -73,7 +73,7 @@ Each lemmy server can set its own moderation policy; appointing site-wide admins
   - Full vote scores `(+/-)` like old reddit.
   - Themes, including light, dark, and solarized.
   - Emojis with autocomplete support. Start typing `:`
-  - User tagging using `@`, Community tagging using `#`.
+  - User tagging using `@`, Community tagging using `!`.
   - Integrated image uploading in both posts and comments.
   - A post can consist of a title and any combination of self text, a URL, or nothing else.
   - Notifications, on comment replies and when you're tagged.
index ce85f60c6f23f9e03beafd6361993ddc99a6a5ec..c2ecc0c897578f4837d9830fd57350e1265a5a62 100755 (executable)
@@ -1,15 +1,18 @@
-#!/bin/sh
+#!/bin/bash
 set -e
 
+BRANCH=$1
+
+git checkout $BRANCH
+
 export COMPOSE_DOCKER_CLI_BUILD=1
 export DOCKER_BUILDKIT=1
 
 # Rebuilding dev docker
-docker-compose build
-docker tag dev_lemmy:latest dessalines/lemmy:test
-docker push dessalines/lemmy:test
+sudo docker build . -f "docker/dev/Dockerfile" -t "dessalines/lemmy:$BRANCH"
+sudo docker push "dessalines/lemmy:$BRANCH"
 
 # Run the playbook
-pushd ../../../lemmy-ansible
-ansible-playbook -i test playbooks/site.yml --vault-password-file vault_pass
+pushd ../lemmy-ansible
+ansible-playbook -i test playbooks/site.yml
 popd
diff --git a/docker/federation-test/run-tests.sh b/docker/federation-test/run-tests.sh
new file mode 100755 (executable)
index 0000000..b2d319d
--- /dev/null
@@ -0,0 +1,23 @@
+#!/bin/bash
+set -e
+
+pushd ../../server/
+cargo build
+popd
+
+sudo docker build ../../ --file ../federation/Dockerfile --tag lemmy-federation:latest
+
+sudo docker-compose --file ../federation/docker-compose.yml --project-directory . up -d
+
+pushd ../../ui
+yarn
+echo "Waiting for Lemmy to start..."
+while [[ "$(curl -s -o /dev/null -w '%{http_code}' 'localhost:8540/api/v1/site')" != "200" ]]; do sleep 1; done
+while [[ "$(curl -s -o /dev/null -w '%{http_code}' 'localhost:8550/api/v1/site')" != "200" ]]; do sleep 1; done
+while [[ "$(curl -s -o /dev/null -w '%{http_code}' 'localhost:8560/api/v1/site')" != "200" ]]; do sleep 1; done
+yarn api-test || true
+popd
+
+sudo docker-compose --file ../federation/docker-compose.yml --project-directory . down
+
+sudo rm -r volumes/
diff --git a/docker/federation/Dockerfile b/docker/federation/Dockerfile
new file mode 100644 (file)
index 0000000..ec7bf2d
--- /dev/null
@@ -0,0 +1,17 @@
+FROM ekidd/rust-musl-builder:1.42.0-openssl11
+
+USER root
+RUN mkdir /app/dist/documentation/ -p \
+ && addgroup --gid 1001 lemmy \
+ && adduser --disabled-password --shell /bin/sh -u 1001 --ingroup lemmy lemmy
+
+# Copy resources
+COPY server/config/defaults.hjson /app/config/defaults.hjson
+COPY ui/dist /app/dist
+COPY server/target/debug/lemmy_server /app/lemmy
+
+RUN chown lemmy:lemmy /app/ -R
+USER lemmy
+EXPOSE 8536
+WORKDIR /app
+CMD ["/app/lemmy"]
diff --git a/docker/federation/docker-compose.yml b/docker/federation/docker-compose.yml
new file mode 100644 (file)
index 0000000..585f2b4
--- /dev/null
@@ -0,0 +1,132 @@
+version: '3.3'
+
+services:
+  nginx:
+    image: nginx:1.17-alpine
+    ports:
+      - "8540:8540"
+      - "8550:8550"
+      - "8560:8560"
+    volumes:
+      # Hack to make this work from both docker/federation/ and docker/federation-test/
+      - ../federation/nginx.conf:/etc/nginx/nginx.conf
+    depends_on:
+      - lemmy_alpha
+      - pictrs_alpha
+      - lemmy_beta
+      - pictrs_beta
+      - lemmy_gamma
+      - pictrs_gamma
+      - iframely
+    restart: "always"
+
+  lemmy_alpha:
+    image: lemmy-federation:latest
+    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_PORT=8540
+      - LEMMY_SETUP__ADMIN_USERNAME=lemmy_alpha
+      - LEMMY_SETUP__ADMIN_PASSWORD=lemmy
+      - LEMMY_SETUP__SITE_NAME=lemmy_alpha
+      - RUST_BACKTRACE=1
+      - RUST_LOG=debug
+    restart: always
+    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
+    restart: always
+  pictrs_alpha:
+    image: asonix/pictrs:v0.1.13-r0
+    user: 991:991
+    volumes:
+      - ./volumes/pictrs_alpha:/mnt
+    restart: always
+
+  lemmy_beta:
+    image: lemmy-federation:latest
+    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_PORT=8550
+      - LEMMY_SETUP__ADMIN_USERNAME=lemmy_beta
+      - LEMMY_SETUP__ADMIN_PASSWORD=lemmy
+      - LEMMY_SETUP__SITE_NAME=lemmy_beta
+      - RUST_BACKTRACE=1
+      - RUST_LOG=debug
+    restart: always
+    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
+    restart: always
+  pictrs_beta:
+    image: asonix/pictrs:v0.1.13-r0
+    user: 991:991
+    volumes:
+      - ./volumes/pictrs_beta:/mnt
+    restart: always
+
+  lemmy_gamma:
+    image: lemmy-federation:latest
+    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_PORT=8560
+      - LEMMY_SETUP__ADMIN_USERNAME=lemmy_gamma
+      - LEMMY_SETUP__ADMIN_PASSWORD=lemmy
+      - LEMMY_SETUP__SITE_NAME=lemmy_gamma
+      - RUST_BACKTRACE=1
+      - RUST_LOG=debug
+    restart: always
+    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
+    restart: always
+  pictrs_gamma:
+    image: asonix/pictrs:v0.1.13-r0
+    user: 991:991
+    volumes:
+      - ./volumes/pictrs_gamma:/mnt
+    restart: always
+
+  iframely:
+    image: dogbin/iframely:latest
+    volumes:
+      - ../iframely.config.local.js:/iframely/config.local.js:ro
+    restart: always
diff --git a/docker/federation/nginx.conf b/docker/federation/nginx.conf
new file mode 100644 (file)
index 0000000..25160eb
--- /dev/null
@@ -0,0 +1,125 @@
+events {
+    worker_connections 1024;
+}
+
+http {
+    server {
+        listen 8540;
+        server_name 127.0.0.1;
+        access_log  off;
+
+        # Upload limit for pictshare
+        client_max_body_size 50M;
+
+        location / {
+            proxy_pass http://lemmy_alpha:8540;
+            proxy_set_header X-Real-IP $remote_addr;
+            proxy_set_header Host $host;
+            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+
+            # 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_alpha: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;
+            proxy_set_header Host $host;
+            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+        }
+    }
+
+    server {
+        listen 8550;
+        server_name 127.0.0.1;
+        access_log off;
+
+        # Upload limit for pictshare
+        client_max_body_size 50M;
+
+        location / {
+            proxy_pass http://lemmy_beta:8550;
+            proxy_set_header X-Real-IP $remote_addr;
+            proxy_set_header Host $host;
+            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+
+            # 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_beta: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;
+            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;
+
+            # 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_gamma: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;
+            proxy_set_header Host $host;
+            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+        }
+    }
+}
diff --git a/docker/federation/run-federation-test.bash b/docker/federation/run-federation-test.bash
new file mode 100755 (executable)
index 0000000..bc73fff
--- /dev/null
@@ -0,0 +1,28 @@
+#!/bin/bash
+set -e
+
+# already start rust build in the background
+pushd ../../server/ || exit
+cargo build &
+popd || exit
+
+if [ "$1" = "-yarn" ]; then
+  pushd ../../ui/ || exit
+  yarn
+  yarn build
+  popd || exit
+fi
+
+# wait for rust build to finish
+pushd ../../server/ || exit
+cargo build
+popd || exit
+
+sudo docker build ../../ --file Dockerfile -t lemmy-federation:latest
+
+for Item in alpha beta gamma ; do
+  sudo mkdir -p volumes/pictrs_$Item
+  sudo chown -R 991:991 volumes/pictrs_$Item
+done
+
+sudo docker-compose up
index 16788ab93733ce4062e74f89886b14db64092a41..6f709b580d11e8189814020cc460b48a5455ad27 100644 (file)
@@ -3,7 +3,7 @@
 Start typing...
 
 - `@a_user_name` to get a list of usernames.
-- `#a_community` to get a list of communities.
+- `!a_community` to get a list of communities.
 - `:emoji` to get a list of emojis.
 
 ## Sorting
index 80c9ec7eac70e2917d792dfa2eecb8e984c5de49..520a61275ffb29972f80965f93bb7c15151f43ed 100644 (file)
@@ -15,7 +15,7 @@ git checkout federation
 git pull federation federation
 ```
 
-## Running
+## Running locally
 
 You need to have the following packages installed, the Docker service needs to be running.
 
@@ -31,7 +31,30 @@ cd dev/federation-test
 ```
 
 After the build is finished and the docker-compose setup is running, open [127.0.0.1:8540](http://127.0.0.1:8540) and
-[127.0.0.1:8541](http://127.0.0.1:8541) in your browser to use the test instances. You can login as admin with
-username `lemmy` and password `lemmy`, or create new accounts.
+[127.0.0.1:8550](http://127.0.0.1:8550) in your browser to use the test instances. You can login as admin with
+username `lemmy_alpha` and `lemmy_beta` respectively, with password `lemmy`.
 
-Please get in touch if you want to contribute to this, so we can coordinate things and avoid duplicate work.
+## Running on a server
+
+Note that federation is currently in alpha. Only use it for testing, not on any production server, and be aware
+that you might have to wipe the instance data at one point or another.
+
+Follow the normal installation instructions, either with [Ansible](administration_install_ansible.md) or
+[manually](administration_install_docker.md). Then replace the line `image: dessalines/lemmy:v0.x.x` in 
+`/lemmy/docker-compose.yml` with `image: dessalines/lemmy:federation`. Also add the following in
+`/lemmy/lemmy.hjson`:
+
+```
+    federation: {
+        enabled: true
+        allowed_instances: example.com
+    }
+```
+
+Afterwards, and whenver you want to update to the latest version, run these commands on the server:
+
+```
+cd /lemmy/
+sudo docker-compose pull
+sudo docker-compose up -d
+```
index 684a7f8a27ae738ba679bcbdc8a3fc4ac1b9c305..f3efdc308472dcc95439ed2ecb08c6194e22d315 100644 (file)
@@ -1,2 +1,5 @@
 tab_spaces = 2
-edition="2018"
\ No newline at end of file
+edition="2018"
+imports_layout="HorizontalVertical"
+merge_imports=true
+reorder_imports=true
index f91b665b89a2f28f6c42ff56e1f9960e67c38446..f124396b7e3200ab361bfbe9b4976cc4da9fa38b 100644 (file)
 # This file is automatically @generated by Cargo.
 # It is not intended for manual editing.
 [[package]]
-name = "activitypub"
-version = "0.2.0"
+name = "activitystreams"
+version = "0.6.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "464cb473bfb402b857cc15b1153974c203a43f1485da4dda15cd17a738548958"
 dependencies = [
- "activitystreams-derive 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
- "activitystreams-traits 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
- "activitystreams-types 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
- "serde 1.0.106 (registry+https://github.com/rust-lang/crates.io-index)",
- "serde_derive 1.0.106 (registry+https://github.com/rust-lang/crates.io-index)",
- "serde_json 1.0.52 (registry+https://github.com/rust-lang/crates.io-index)",
+ "activitystreams-derive",
+ "chrono",
+ "mime",
+ "serde 1.0.114",
+ "serde_json",
+ "thiserror",
+ "url",
 ]
 
 [[package]]
 name = "activitystreams-derive"
-version = "0.2.0"
+version = "0.6.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c39ba5929399e9f921055bac76dd8f47419fa5b6b6da1ac4c1e82b94ed0ac7b4"
 dependencies = [
- "proc-macro2 1.0.10 (registry+https://github.com/rust-lang/crates.io-index)",
- "quote 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)",
- "syn 1.0.17 (registry+https://github.com/rust-lang/crates.io-index)",
+ "proc-macro2",
+ "quote",
+ "syn",
 ]
 
 [[package]]
-name = "activitystreams-traits"
-version = "0.2.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
+name = "activitystreams-ext"
+version = "0.1.0"
+source = "git+https://git.asonix.dog/asonix/activitystreams-ext#e5c97f4ea9f60e49bc7ff27fb0fb515d3190fd25"
 dependencies = [
- "serde 1.0.106 (registry+https://github.com/rust-lang/crates.io-index)",
- "serde_json 1.0.52 (registry+https://github.com/rust-lang/crates.io-index)",
- "thiserror 1.0.19 (registry+https://github.com/rust-lang/crates.io-index)",
+ "activitystreams-new",
+ "serde 1.0.114",
+ "serde_json",
 ]
 
 [[package]]
-name = "activitystreams-types"
-version = "0.3.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
+name = "activitystreams-new"
+version = "0.1.0"
+source = "git+https://git.asonix.dog/asonix/activitystreams-sketch#99c7e9aa5596eda846a1ebd5978ca72d11d4c08a"
 dependencies = [
- "activitystreams-derive 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
- "activitystreams-traits 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
- "chrono 0.4.11 (registry+https://github.com/rust-lang/crates.io-index)",
- "mime 0.3.16 (registry+https://github.com/rust-lang/crates.io-index)",
- "serde 1.0.106 (registry+https://github.com/rust-lang/crates.io-index)",
- "serde_derive 1.0.106 (registry+https://github.com/rust-lang/crates.io-index)",
- "serde_json 1.0.52 (registry+https://github.com/rust-lang/crates.io-index)",
+ "activitystreams",
+ "serde 1.0.114",
+ "serde_json",
+ "typed-builder",
 ]
 
 [[package]]
 name = "actix"
 version = "0.9.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a4af87564ff659dee8f9981540cac9418c45e910c8072fdedd643a262a38fcaf"
 dependencies = [
- "actix-http 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)",
- "actix-rt 1.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
- "actix_derive 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)",
- "bitflags 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
- "bytes 0.5.4 (registry+https://github.com/rust-lang/crates.io-index)",
- "crossbeam-channel 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)",
- "derive_more 0.99.5 (registry+https://github.com/rust-lang/crates.io-index)",
- "futures 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)",
- "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
- "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)",
- "parking_lot 0.10.2 (registry+https://github.com/rust-lang/crates.io-index)",
- "pin-project 0.4.9 (registry+https://github.com/rust-lang/crates.io-index)",
- "smallvec 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
- "tokio 0.2.20 (registry+https://github.com/rust-lang/crates.io-index)",
- "tokio-util 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
- "trust-dns-proto 0.18.0-alpha.2 (registry+https://github.com/rust-lang/crates.io-index)",
- "trust-dns-resolver 0.18.0-alpha.2 (registry+https://github.com/rust-lang/crates.io-index)",
+ "actix-http",
+ "actix-rt",
+ "actix_derive",
+ "bitflags",
+ "bytes",
+ "crossbeam-channel",
+ "derive_more",
+ "futures",
+ "lazy_static",
+ "log",
+ "parking_lot",
+ "pin-project",
+ "smallvec",
+ "tokio",
+ "tokio-util 0.2.0",
+ "trust-dns-proto",
+ "trust-dns-resolver",
 ]
 
 [[package]]
 name = "actix-codec"
 version = "0.2.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09e55f0a5c2ca15795035d90c46bd0e73a5123b72f68f12596d6ba5282051380"
 dependencies = [
- "bitflags 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
- "bytes 0.5.4 (registry+https://github.com/rust-lang/crates.io-index)",
- "futures-core 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)",
- "futures-sink 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)",
- "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)",
- "tokio 0.2.20 (registry+https://github.com/rust-lang/crates.io-index)",
- "tokio-util 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "bitflags",
+ "bytes",
+ "futures-core",
+ "futures-sink",
+ "log",
+ "tokio",
+ "tokio-util 0.2.0",
 ]
 
 [[package]]
 name = "actix-connect"
 version = "1.0.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c95cc9569221e9802bf4c377f6c18b90ef10227d787611decf79fd47d2a8e76c"
 dependencies = [
- "actix-codec 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
- "actix-rt 1.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
- "actix-service 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)",
- "actix-utils 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)",
- "derive_more 0.99.5 (registry+https://github.com/rust-lang/crates.io-index)",
- "either 1.5.3 (registry+https://github.com/rust-lang/crates.io-index)",
- "futures 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)",
- "http 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
- "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)",
- "trust-dns-proto 0.18.0-alpha.2 (registry+https://github.com/rust-lang/crates.io-index)",
- "trust-dns-resolver 0.18.0-alpha.2 (registry+https://github.com/rust-lang/crates.io-index)",
+ "actix-codec",
+ "actix-rt",
+ "actix-service",
+ "actix-utils",
+ "derive_more",
+ "either",
+ "futures",
+ "http",
+ "log",
+ "trust-dns-proto",
+ "trust-dns-resolver",
 ]
 
 [[package]]
 name = "actix-files"
-version = "0.2.1"
+version = "0.2.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "193b22cb1f7b4ff12a4eb2415d6d19e47e44ea93e05930b30d05375ea29d3529"
 dependencies = [
- "actix-http 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)",
- "actix-service 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)",
- "actix-web 2.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
- "bitflags 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
- "bytes 0.5.4 (registry+https://github.com/rust-lang/crates.io-index)",
- "derive_more 0.99.5 (registry+https://github.com/rust-lang/crates.io-index)",
- "futures 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)",
- "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)",
- "mime 0.3.16 (registry+https://github.com/rust-lang/crates.io-index)",
- "mime_guess 2.0.3 (registry+https://github.com/rust-lang/crates.io-index)",
- "percent-encoding 2.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
- "v_htmlescape 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)",
+ "actix-http",
+ "actix-service",
+ "actix-web",
+ "bitflags",
+ "bytes",
+ "derive_more",
+ "futures-core",
+ "futures-util",
+ "log",
+ "mime",
+ "mime_guess",
+ "percent-encoding",
+ "v_htmlescape",
 ]
 
 [[package]]
 name = "actix-http"
 version = "1.0.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-dependencies = [
- "actix-codec 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
- "actix-connect 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)",
- "actix-rt 1.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
- "actix-service 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)",
- "actix-threadpool 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
- "actix-utils 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)",
- "base64 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)",
- "bitflags 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
- "brotli2 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)",
- "bytes 0.5.4 (registry+https://github.com/rust-lang/crates.io-index)",
- "chrono 0.4.11 (registry+https://github.com/rust-lang/crates.io-index)",
- "copyless 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)",
- "derive_more 0.99.5 (registry+https://github.com/rust-lang/crates.io-index)",
- "either 1.5.3 (registry+https://github.com/rust-lang/crates.io-index)",
- "encoding_rs 0.8.22 (registry+https://github.com/rust-lang/crates.io-index)",
- "failure 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)",
- "flate2 1.0.14 (registry+https://github.com/rust-lang/crates.io-index)",
- "futures-channel 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)",
- "futures-core 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)",
- "futures-util 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)",
- "fxhash 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
- "h2 0.2.4 (registry+https://github.com/rust-lang/crates.io-index)",
- "http 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
- "httparse 1.3.4 (registry+https://github.com/rust-lang/crates.io-index)",
- "indexmap 1.3.2 (registry+https://github.com/rust-lang/crates.io-index)",
- "language-tags 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
- "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
- "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)",
- "mime 0.3.16 (registry+https://github.com/rust-lang/crates.io-index)",
- "percent-encoding 2.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
- "pin-project 0.4.9 (registry+https://github.com/rust-lang/crates.io-index)",
- "rand 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)",
- "regex 1.3.7 (registry+https://github.com/rust-lang/crates.io-index)",
- "serde 1.0.106 (registry+https://github.com/rust-lang/crates.io-index)",
- "serde_json 1.0.52 (registry+https://github.com/rust-lang/crates.io-index)",
- "serde_urlencoded 0.6.1 (registry+https://github.com/rust-lang/crates.io-index)",
- "sha1 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)",
- "slab 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)",
- "time 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)",
+checksum = "c16664cc4fdea8030837ad5a845eb231fb93fc3c5c171edfefb52fad92ce9019"
+dependencies = [
+ "actix-codec",
+ "actix-connect",
+ "actix-rt",
+ "actix-service",
+ "actix-threadpool",
+ "actix-utils",
+ "base64 0.11.0",
+ "bitflags",
+ "brotli2",
+ "bytes",
+ "chrono",
+ "copyless",
+ "derive_more",
+ "either",
+ "encoding_rs",
+ "failure",
+ "flate2",
+ "futures-channel",
+ "futures-core",
+ "futures-util",
+ "fxhash",
+ "h2",
+ "http",
+ "httparse",
+ "indexmap",
+ "language-tags",
+ "lazy_static",
+ "log",
+ "mime",
+ "percent-encoding",
+ "pin-project",
+ "rand 0.7.3",
+ "regex",
+ "serde 1.0.114",
+ "serde_json",
+ "serde_urlencoded",
+ "sha1",
+ "slab",
+ "time",
 ]
 
 [[package]]
 name = "actix-macros"
-version = "0.1.1"
+version = "0.1.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a60f9ba7c4e6df97f3aacb14bb5c0cd7d98a49dcbaed0d7f292912ad9a6a3ed2"
 dependencies = [
- "quote 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)",
- "syn 1.0.17 (registry+https://github.com/rust-lang/crates.io-index)",
+ "quote",
+ "syn",
 ]
 
 [[package]]
 name = "actix-router"
 version = "0.2.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9d7a10ca4d94e8c8e7a87c5173aba1b97ba9a6563ca02b0e1cd23531093d3ec8"
 dependencies = [
- "bytestring 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
- "http 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
- "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)",
- "regex 1.3.7 (registry+https://github.com/rust-lang/crates.io-index)",
- "serde 1.0.106 (registry+https://github.com/rust-lang/crates.io-index)",
+ "bytestring",
+ "http",
+ "log",
+ "regex",
+ "serde 1.0.114",
 ]
 
 [[package]]
 name = "actix-rt"
 version = "1.1.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "143fcc2912e0d1de2bcf4e2f720d2a60c28652ab4179685a1ee159e0fb3db227"
 dependencies = [
- "actix-macros 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
- "actix-threadpool 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
- "copyless 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)",
- "futures-channel 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)",
- "futures-util 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)",
- "smallvec 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
- "tokio 0.2.20 (registry+https://github.com/rust-lang/crates.io-index)",
+ "actix-macros",
+ "actix-threadpool",
+ "copyless",
+ "futures-channel",
+ "futures-util",
+ "smallvec",
+ "tokio",
 ]
 
 [[package]]
 name = "actix-server"
-version = "1.0.2"
+version = "1.0.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6d74b464215a473c973a2d7d03a69cc10f4ce1f4b38a7659c5193dc5c675630"
 dependencies = [
- "actix-codec 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
- "actix-rt 1.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
- "actix-service 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)",
- "actix-utils 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)",
- "futures 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)",
- "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)",
- "mio 0.6.21 (registry+https://github.com/rust-lang/crates.io-index)",
- "mio-uds 0.6.7 (registry+https://github.com/rust-lang/crates.io-index)",
- "net2 0.2.33 (registry+https://github.com/rust-lang/crates.io-index)",
- "num_cpus 1.13.0 (registry+https://github.com/rust-lang/crates.io-index)",
- "slab 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)",
+ "actix-codec",
+ "actix-rt",
+ "actix-service",
+ "actix-utils",
+ "futures-channel",
+ "futures-util",
+ "log",
+ "mio",
+ "mio-uds",
+ "num_cpus",
+ "slab",
+ "socket2",
 ]
 
 [[package]]
 name = "actix-service"
 version = "1.0.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d3e4fc95dfa7e24171b2d0bb46b85f8ab0e8499e4e3caec691fc4ea65c287564"
 dependencies = [
- "futures-util 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)",
- "pin-project 0.4.9 (registry+https://github.com/rust-lang/crates.io-index)",
+ "futures-util",
+ "pin-project",
 ]
 
 [[package]]
 name = "actix-testing"
-version = "1.0.0"
+version = "1.0.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "47239ca38799ab74ee6a8a94d1ce857014b2ac36f242f70f3f75a66f691e791c"
 dependencies = [
- "actix-macros 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
- "actix-rt 1.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
- "actix-server 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)",
- "actix-service 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)",
- "futures 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)",
- "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)",
- "net2 0.2.33 (registry+https://github.com/rust-lang/crates.io-index)",
+ "actix-macros",
+ "actix-rt",
+ "actix-server",
+ "actix-service",
+ "log",
+ "socket2",
 ]
 
 [[package]]
 name = "actix-threadpool"
-version = "0.3.1"
+version = "0.3.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "91164716d956745c79dcea5e66d2aa04506549958accefcede5368c70f2fd4ff"
 dependencies = [
- "derive_more 0.99.5 (registry+https://github.com/rust-lang/crates.io-index)",
- "futures-channel 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)",
- "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
- "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)",
- "num_cpus 1.13.0 (registry+https://github.com/rust-lang/crates.io-index)",
- "parking_lot 0.10.2 (registry+https://github.com/rust-lang/crates.io-index)",
- "threadpool 1.7.1 (registry+https://github.com/rust-lang/crates.io-index)",
+ "derive_more",
+ "futures-channel",
+ "lazy_static",
+ "log",
+ "num_cpus",
+ "parking_lot",
+ "threadpool",
 ]
 
 [[package]]
 name = "actix-tls"
 version = "1.0.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a4e5b4faaf105e9a6d389c606c298dcdb033061b00d532af9df56ff3a54995a8"
 dependencies = [
- "actix-codec 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
- "actix-rt 1.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
- "actix-service 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)",
- "actix-utils 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)",
- "derive_more 0.99.5 (registry+https://github.com/rust-lang/crates.io-index)",
- "either 1.5.3 (registry+https://github.com/rust-lang/crates.io-index)",
- "futures 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)",
- "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)",
+ "actix-codec",
+ "actix-rt",
+ "actix-service",
+ "actix-utils",
+ "derive_more",
+ "either",
+ "futures",
+ "log",
 ]
 
 [[package]]
 name = "actix-utils"
 version = "1.0.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fcf8f5631bf01adec2267808f00e228b761c60c0584cc9fa0b5364f41d147f4e"
 dependencies = [
- "actix-codec 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
- "actix-rt 1.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
- "actix-service 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)",
- "bitflags 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
- "bytes 0.5.4 (registry+https://github.com/rust-lang/crates.io-index)",
- "either 1.5.3 (registry+https://github.com/rust-lang/crates.io-index)",
- "futures 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)",
- "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)",
- "pin-project 0.4.9 (registry+https://github.com/rust-lang/crates.io-index)",
- "slab 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)",
+ "actix-codec",
+ "actix-rt",
+ "actix-service",
+ "bitflags",
+ "bytes",
+ "either",
+ "futures",
+ "log",
+ "pin-project",
+ "slab",
 ]
 
 [[package]]
 name = "actix-web"
 version = "2.0.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-dependencies = [
- "actix-codec 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
- "actix-http 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)",
- "actix-macros 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
- "actix-router 0.2.4 (registry+https://github.com/rust-lang/crates.io-index)",
- "actix-rt 1.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
- "actix-server 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)",
- "actix-service 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)",
- "actix-testing 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
- "actix-threadpool 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
- "actix-tls 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
- "actix-utils 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)",
- "actix-web-codegen 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
- "awc 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)",
- "bytes 0.5.4 (registry+https://github.com/rust-lang/crates.io-index)",
- "derive_more 0.99.5 (registry+https://github.com/rust-lang/crates.io-index)",
- "encoding_rs 0.8.22 (registry+https://github.com/rust-lang/crates.io-index)",
- "futures 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)",
- "fxhash 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
- "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)",
- "mime 0.3.16 (registry+https://github.com/rust-lang/crates.io-index)",
- "net2 0.2.33 (registry+https://github.com/rust-lang/crates.io-index)",
- "pin-project 0.4.9 (registry+https://github.com/rust-lang/crates.io-index)",
- "regex 1.3.7 (registry+https://github.com/rust-lang/crates.io-index)",
- "serde 1.0.106 (registry+https://github.com/rust-lang/crates.io-index)",
- "serde_json 1.0.52 (registry+https://github.com/rust-lang/crates.io-index)",
- "serde_urlencoded 0.6.1 (registry+https://github.com/rust-lang/crates.io-index)",
- "time 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)",
- "url 2.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
+checksum = "3158e822461040822f0dbf1735b9c2ce1f95f93b651d7a7aded00b1efbb1f635"
+dependencies = [
+ "actix-codec",
+ "actix-http",
+ "actix-macros",
+ "actix-router",
+ "actix-rt",
+ "actix-server",
+ "actix-service",
+ "actix-testing",
+ "actix-threadpool",
+ "actix-tls",
+ "actix-utils",
+ "actix-web-codegen",
+ "awc",
+ "bytes",
+ "derive_more",
+ "encoding_rs",
+ "futures",
+ "fxhash",
+ "log",
+ "mime",
+ "net2",
+ "pin-project",
+ "regex",
+ "serde 1.0.114",
+ "serde_json",
+ "serde_urlencoded",
+ "time",
+ "url",
 ]
 
 [[package]]
 name = "actix-web-actors"
 version = "2.0.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc1bd41bd66c4e9b5274cec87aac30168e63d64e96fd19db38edef6b46ba2982"
 dependencies = [
- "actix 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)",
- "actix-codec 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
- "actix-http 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)",
- "actix-web 2.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
- "bytes 0.5.4 (registry+https://github.com/rust-lang/crates.io-index)",
- "futures 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)",
- "pin-project 0.4.9 (registry+https://github.com/rust-lang/crates.io-index)",
+ "actix",
+ "actix-codec",
+ "actix-http",
+ "actix-web",
+ "bytes",
+ "futures",
+ "pin-project",
 ]
 
 [[package]]
 name = "actix-web-codegen"
-version = "0.2.1"
+version = "0.2.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a71bf475cbe07281d0b3696abb48212db118e7e23219f13596ce865235ff5766"
 dependencies = [
- "proc-macro2 1.0.10 (registry+https://github.com/rust-lang/crates.io-index)",
- "quote 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)",
- "syn 1.0.17 (registry+https://github.com/rust-lang/crates.io-index)",
+ "proc-macro2",
+ "quote",
+ "syn",
 ]
 
 [[package]]
 name = "actix_derive"
 version = "0.5.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b95aceadaf327f18f0df5962fedc1bde2f870566a0b9f65c89508a3b1f79334c"
 dependencies = [
- "proc-macro2 1.0.10 (registry+https://github.com/rust-lang/crates.io-index)",
- "quote 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)",
- "syn 1.0.17 (registry+https://github.com/rust-lang/crates.io-index)",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "addr2line"
+version = "0.12.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "602d785912f476e480434627e8732e6766b760c045bbf897d9dfaa9f4fbd399c"
+dependencies = [
+ "gimli",
 ]
 
 [[package]]
 name = "adler32"
-version = "1.0.4"
+version = "1.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "567b077b825e468cc974f0020d4082ee6e03132512f207ef1a02fd5d00d1f32d"
 
 [[package]]
 name = "aho-corasick"
-version = "0.7.10"
+version = "0.7.13"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "043164d8ba5c4c3035fec9bbee8647c0261d788f3474306f93bb65901cae0e86"
 dependencies = [
- "memchr 2.3.3 (registry+https://github.com/rust-lang/crates.io-index)",
+ "memchr",
 ]
 
 [[package]]
 name = "ansi_term"
 version = "0.11.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b"
 dependencies = [
- "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)",
+ "winapi 0.3.8",
 ]
 
 [[package]]
 name = "arc-swap"
-version = "0.4.5"
+version = "0.4.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4d25d88fd6b8041580a654f9d0c581a047baee2b3efee13275f2fc392fc75034"
 
 [[package]]
 name = "arrayvec"
-version = "0.4.12"
+version = "0.5.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-dependencies = [
- "nodrop 0.1.14 (registry+https://github.com/rust-lang/crates.io-index)",
-]
+checksum = "cff77d8686867eceff3105329d4698d96c2391c176d5d03adc90c7389162b5b8"
 
 [[package]]
 name = "ascii_utils"
 version = "0.9.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "71938f30533e4d95a6d17aa530939da3842c2ab6f4f84b9dae68447e4129f74a"
 
 [[package]]
 name = "async-trait"
-version = "0.1.30"
+version = "0.1.36"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a265e3abeffdce30b2e26b7a11b222fe37c6067404001b434101457d0385eb92"
 dependencies = [
- "proc-macro2 1.0.10 (registry+https://github.com/rust-lang/crates.io-index)",
- "quote 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)",
- "syn 1.0.17 (registry+https://github.com/rust-lang/crates.io-index)",
+ "proc-macro2",
+ "quote",
+ "syn",
 ]
 
 [[package]]
 name = "attohttpc"
 version = "0.14.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "93610ce1c035e8a273fe56a19852e42798f3c019ca2726e52d2971197f116525"
 dependencies = [
- "http 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
- "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)",
- "rustls 0.17.0 (registry+https://github.com/rust-lang/crates.io-index)",
- "url 2.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
- "webpki 0.21.2 (registry+https://github.com/rust-lang/crates.io-index)",
- "webpki-roots 0.19.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "http",
+ "log",
+ "rustls",
+ "url",
+ "webpki",
+ "webpki-roots",
 ]
 
 [[package]]
 name = "atty"
 version = "0.2.14"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8"
 dependencies = [
- "hermit-abi 0.1.11 (registry+https://github.com/rust-lang/crates.io-index)",
- "libc 0.2.69 (registry+https://github.com/rust-lang/crates.io-index)",
- "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)",
+ "hermit-abi",
+ "libc",
+ "winapi 0.3.8",
 ]
 
 [[package]]
 name = "autocfg"
 version = "0.1.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1d49d90015b3c36167a20fe2810c5cd875ad504b39cff3d4eae7977e6b7c1cb2"
 
 [[package]]
 name = "autocfg"
 version = "1.0.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f8aac770f1885fd7e387acedd76065302551364496e46b3dd00860b2f8359b9d"
 
 [[package]]
 name = "awc"
 version = "1.0.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d7601d4d1d7ef2335d6597a41b5fe069f6ab799b85f53565ab390e7b7065aac5"
 dependencies = [
- "actix-codec 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
- "actix-http 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)",
- "actix-rt 1.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
- "actix-service 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)",
- "base64 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)",
- "bytes 0.5.4 (registry+https://github.com/rust-lang/crates.io-index)",
- "derive_more 0.99.5 (registry+https://github.com/rust-lang/crates.io-index)",
- "futures-core 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)",
- "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)",
- "mime 0.3.16 (registry+https://github.com/rust-lang/crates.io-index)",
- "percent-encoding 2.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
- "rand 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)",
- "serde 1.0.106 (registry+https://github.com/rust-lang/crates.io-index)",
- "serde_json 1.0.52 (registry+https://github.com/rust-lang/crates.io-index)",
- "serde_urlencoded 0.6.1 (registry+https://github.com/rust-lang/crates.io-index)",
+ "actix-codec",
+ "actix-http",
+ "actix-rt",
+ "actix-service",
+ "base64 0.11.0",
+ "bytes",
+ "derive_more",
+ "futures-core",
+ "log",
+ "mime",
+ "percent-encoding",
+ "rand 0.7.3",
+ "serde 1.0.114",
+ "serde_json",
+ "serde_urlencoded",
 ]
 
 [[package]]
 name = "backtrace"
-version = "0.3.46"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-dependencies = [
- "backtrace-sys 0.1.35 (registry+https://github.com/rust-lang/crates.io-index)",
- "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)",
- "libc 0.2.69 (registry+https://github.com/rust-lang/crates.io-index)",
- "rustc-demangle 0.1.16 (registry+https://github.com/rust-lang/crates.io-index)",
-]
-
-[[package]]
-name = "backtrace-sys"
-version = "0.1.35"
+version = "0.3.49"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "05100821de9e028f12ae3d189176b41ee198341eb8f369956407fea2f5cc666c"
 dependencies = [
- "cc 1.0.50 (registry+https://github.com/rust-lang/crates.io-index)",
- "libc 0.2.69 (registry+https://github.com/rust-lang/crates.io-index)",
+ "addr2line",
+ "cfg-if",
+ "libc",
+ "miniz_oxide",
+ "object",
+ "rustc-demangle",
 ]
 
 [[package]]
 name = "base64"
 version = "0.9.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "489d6c0ed21b11d038c31b6ceccca973e65d73ba3bd8ecb9a2babf5546164643"
 dependencies = [
- "byteorder 1.3.4 (registry+https://github.com/rust-lang/crates.io-index)",
- "safemem 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)",
+ "byteorder",
+ "safemem",
 ]
 
 [[package]]
 name = "base64"
 version = "0.10.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b25d992356d2eb0ed82172f5248873db5560c4721f564b13cb5193bda5e668e"
 dependencies = [
- "byteorder 1.3.4 (registry+https://github.com/rust-lang/crates.io-index)",
+ "byteorder",
 ]
 
 [[package]]
 name = "base64"
 version = "0.11.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b41b7ea54a0c9d92199de89e20e58d49f02f8e699814ef3fdf266f6f748d15c7"
 
 [[package]]
 name = "base64"
-version = "0.12.0"
+version = "0.12.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e223af0dc48c96d4f8342ec01a4974f139df863896b316681efd36742f22cc67"
 
 [[package]]
 name = "bcrypt"
-version = "0.7.0"
+version = "0.8.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41b70db86f3c560199b0dada79a22b9a924622384abb2a756a9707ffcce077f2"
 dependencies = [
- "base64 0.12.0 (registry+https://github.com/rust-lang/crates.io-index)",
- "blowfish 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
- "byteorder 1.3.4 (registry+https://github.com/rust-lang/crates.io-index)",
- "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
- "rand 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)",
+ "base64 0.12.2",
+ "blowfish",
+ "byteorder",
+ "getrandom",
 ]
 
 [[package]]
 name = "bitflags"
 version = "1.2.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693"
 
 [[package]]
 name = "block-buffer"
 version = "0.7.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c0940dc441f31689269e10ac70eb1002a3a1d3ad1390e030043662eb7fe4688b"
 dependencies = [
- "block-padding 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
- "byte-tools 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
- "byteorder 1.3.4 (registry+https://github.com/rust-lang/crates.io-index)",
- "generic-array 0.12.3 (registry+https://github.com/rust-lang/crates.io-index)",
+ "block-padding",
+ "byte-tools",
+ "byteorder",
+ "generic-array",
 ]
 
 [[package]]
 name = "block-cipher-trait"
 version = "0.6.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1c924d49bd09e7c06003acda26cd9742e796e34282ec6c1189404dee0c1f4774"
 dependencies = [
- "generic-array 0.12.3 (registry+https://github.com/rust-lang/crates.io-index)",
+ "generic-array",
 ]
 
 [[package]]
 name = "block-padding"
 version = "0.1.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fa79dedbb091f449f1f39e53edf88d5dbe95f895dae6135a8d7b881fb5af73f5"
 dependencies = [
- "byte-tools 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
+ "byte-tools",
 ]
 
 [[package]]
 name = "blowfish"
 version = "0.4.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6aeb80d00f2688459b8542068abd974cfb101e7a82182414a99b5026c0d85cc3"
 dependencies = [
- "block-cipher-trait 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)",
- "byteorder 1.3.4 (registry+https://github.com/rust-lang/crates.io-index)",
- "opaque-debug 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)",
+ "block-cipher-trait",
+ "byteorder",
+ "opaque-debug",
 ]
 
 [[package]]
 name = "brotli-sys"
 version = "0.3.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4445dea95f4c2b41cde57cc9fee236ae4dbae88d8fcbdb4750fc1bb5d86aaecd"
 dependencies = [
- "cc 1.0.50 (registry+https://github.com/rust-lang/crates.io-index)",
- "libc 0.2.69 (registry+https://github.com/rust-lang/crates.io-index)",
+ "cc",
+ "libc",
 ]
 
 [[package]]
 name = "brotli2"
 version = "0.3.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0cb036c3eade309815c15ddbacec5b22c4d1f3983a774ab2eac2e3e9ea85568e"
 dependencies = [
- "brotli-sys 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)",
- "libc 0.2.69 (registry+https://github.com/rust-lang/crates.io-index)",
+ "brotli-sys",
+ "libc",
 ]
 
 [[package]]
 name = "bufstream"
 version = "0.1.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "40e38929add23cdf8a366df9b0e088953150724bcbe5fc330b0d8eb3b328eec8"
 
 [[package]]
 name = "bumpalo"
-version = "3.2.1"
+version = "3.4.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2e8c087f005730276d1096a652e92a8bacee2e2472bcc9715a74d2bec38b5820"
 
 [[package]]
 name = "byte-tools"
 version = "0.3.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e3b5ca7a04898ad4bcd41c90c5285445ff5b791899bb1b0abdd2a2aa791211d7"
 
 [[package]]
 name = "byteorder"
 version = "1.3.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "08c48aae112d48ed9f069b33538ea9e3e90aa263cfa3d1c24309612b1f7472de"
 
 [[package]]
 name = "bytes"
-version = "0.5.4"
+version = "0.5.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "118cf036fbb97d0816e3c34b2d7a1e8cfc60f68fcf63d550ddbe9bd5f59c213b"
+dependencies = [
+ "loom",
+]
 
 [[package]]
 name = "bytestring"
 version = "0.1.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fc7c05fa5172da78a62d9949d662d2ac89d4cc7355d7b49adee5163f1fb3f363"
 dependencies = [
- "bytes 0.5.4 (registry+https://github.com/rust-lang/crates.io-index)",
+ "bytes",
 ]
 
 [[package]]
 name = "cc"
-version = "1.0.50"
+version = "1.0.54"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7bbb73db36c1246e9034e307d0fba23f9a2e251faa47ade70c1bd252220c8311"
 
 [[package]]
 name = "cfg-if"
 version = "0.1.10"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822"
 
 [[package]]
 name = "chrono"
 version = "0.4.11"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "80094f509cf8b5ae86a4966a39b3ff66cd7e2a3e594accec3743ff3fabeab5b2"
 dependencies = [
- "num-integer 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)",
- "num-traits 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)",
- "serde 1.0.106 (registry+https://github.com/rust-lang/crates.io-index)",
- "time 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)",
+ "num-integer",
+ "num-traits 0.2.12",
+ "serde 1.0.114",
+ "time",
 ]
 
 [[package]]
 name = "clap"
-version = "2.33.0"
+version = "2.33.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bdfa80d47f954d53a35a64987ca1422f495b8d6483c0fe9f7117b36c2a792129"
 dependencies = [
- "ansi_term 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)",
- "atty 0.2.14 (registry+https://github.com/rust-lang/crates.io-index)",
- "bitflags 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
- "strsim 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)",
- "textwrap 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)",
- "unicode-width 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)",
- "vec_map 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)",
+ "ansi_term",
+ "atty",
+ "bitflags",
+ "strsim 0.8.0",
+ "textwrap",
+ "unicode-width",
+ "vec_map",
 ]
 
 [[package]]
 name = "cloudabi"
 version = "0.0.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ddfc5b9aa5d4507acaf872de71051dfd0e309860e88966e1051e462a077aac4f"
 dependencies = [
- "bitflags 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
+ "bitflags",
 ]
 
 [[package]]
 name = "comrak"
 version = "0.7.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e17058cc536cf290563e88787d7b9e6030ce4742943017cc2ffb71f88034021c"
 dependencies = [
- "clap 2.33.0 (registry+https://github.com/rust-lang/crates.io-index)",
- "entities 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)",
- "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
- "pest 2.1.3 (registry+https://github.com/rust-lang/crates.io-index)",
- "pest_derive 2.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
- "regex 1.3.7 (registry+https://github.com/rust-lang/crates.io-index)",
- "twoway 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
- "typed-arena 1.7.0 (registry+https://github.com/rust-lang/crates.io-index)",
- "unicode_categories 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
+ "clap",
+ "entities",
+ "lazy_static",
+ "pest",
+ "pest_derive",
+ "regex",
+ "twoway",
+ "typed-arena",
+ "unicode_categories",
 ]
 
 [[package]]
 name = "config"
 version = "0.10.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "19b076e143e1d9538dde65da30f8481c2a6c44040edb8e02b9bf1351edb92ce3"
 dependencies = [
- "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
- "nom 5.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
- "serde 1.0.106 (registry+https://github.com/rust-lang/crates.io-index)",
- "serde-hjson 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)",
+ "lazy_static",
+ "nom 5.1.2",
+ "serde 1.0.114",
+ "serde-hjson",
 ]
 
 [[package]]
 name = "copyless"
-version = "0.1.4"
+version = "0.1.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a2df960f5d869b2dd8532793fde43eb5427cceb126c929747a26823ab0eeb536"
 
 [[package]]
 name = "core-foundation"
 version = "0.7.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "57d24c7a13c43e870e37c1556b74555437870a04514f7685f5b354e090567171"
 dependencies = [
- "core-foundation-sys 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)",
- "libc 0.2.69 (registry+https://github.com/rust-lang/crates.io-index)",
+ "core-foundation-sys",
+ "libc",
 ]
 
 [[package]]
 name = "core-foundation-sys"
 version = "0.7.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b3a71ab494c0b5b860bdc8407ae08978052417070c2ced38573a9157ad75b8ac"
 
 [[package]]
 name = "crc32fast"
 version = "1.2.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ba125de2af0df55319f41944744ad91c71113bf74a4646efff39afe1f6842db1"
 dependencies = [
- "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)",
+ "cfg-if",
 ]
 
 [[package]]
 name = "crossbeam-channel"
 version = "0.4.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cced8691919c02aac3cb0a1bc2e9b73d89e832bf9a06fc579d4e71b68a2da061"
 dependencies = [
- "crossbeam-utils 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)",
- "maybe-uninit 2.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "crossbeam-utils",
+ "maybe-uninit",
 ]
 
 [[package]]
 name = "crossbeam-utils"
 version = "0.7.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c3c7c73a2d1e9fc0886a08b93e98eb643461230d5f1925e4036204d5f2e261a8"
 dependencies = [
- "autocfg 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
- "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)",
- "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "autocfg 1.0.0",
+ "cfg-if",
+ "lazy_static",
 ]
 
 [[package]]
 name = "darling"
 version = "0.10.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0d706e75d87e35569db781a9b5e2416cff1236a47ed380831f959382ccd5f858"
 dependencies = [
- "darling_core 0.10.2 (registry+https://github.com/rust-lang/crates.io-index)",
- "darling_macro 0.10.2 (registry+https://github.com/rust-lang/crates.io-index)",
+ "darling_core",
+ "darling_macro",
 ]
 
 [[package]]
 name = "darling_core"
 version = "0.10.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f0c960ae2da4de88a91b2d920c2a7233b400bc33cb28453a2987822d8392519b"
 dependencies = [
- "fnv 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)",
- "ident_case 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)",
- "proc-macro2 1.0.10 (registry+https://github.com/rust-lang/crates.io-index)",
- "quote 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)",
- "strsim 0.9.3 (registry+https://github.com/rust-lang/crates.io-index)",
- "syn 1.0.17 (registry+https://github.com/rust-lang/crates.io-index)",
+ "fnv",
+ "ident_case",
+ "proc-macro2",
+ "quote",
+ "strsim 0.9.3",
+ "syn",
 ]
 
 [[package]]
 name = "darling_macro"
 version = "0.10.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d9b5a2f4ac4969822c62224815d069952656cadc7084fdca9751e6d959189b72"
 dependencies = [
- "darling_core 0.10.2 (registry+https://github.com/rust-lang/crates.io-index)",
- "quote 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)",
- "syn 1.0.17 (registry+https://github.com/rust-lang/crates.io-index)",
+ "darling_core",
+ "quote",
+ "syn",
 ]
 
 [[package]]
 name = "derive_builder"
 version = "0.9.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a2658621297f2cf68762a6f7dc0bb7e1ff2cfd6583daef8ee0fed6f7ec468ec0"
 dependencies = [
- "darling 0.10.2 (registry+https://github.com/rust-lang/crates.io-index)",
- "derive_builder_core 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)",
- "proc-macro2 1.0.10 (registry+https://github.com/rust-lang/crates.io-index)",
- "quote 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)",
- "syn 1.0.17 (registry+https://github.com/rust-lang/crates.io-index)",
+ "darling",
+ "derive_builder_core",
+ "proc-macro2",
+ "quote",
+ "syn",
 ]
 
 [[package]]
 name = "derive_builder_core"
 version = "0.9.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2791ea3e372c8495c0bc2033991d76b512cd799d07491fbd6890124db9458bef"
 dependencies = [
- "darling 0.10.2 (registry+https://github.com/rust-lang/crates.io-index)",
- "proc-macro2 1.0.10 (registry+https://github.com/rust-lang/crates.io-index)",
- "quote 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)",
- "syn 1.0.17 (registry+https://github.com/rust-lang/crates.io-index)",
+ "darling",
+ "proc-macro2",
+ "quote",
+ "syn",
 ]
 
 [[package]]
 name = "derive_more"
-version = "0.99.5"
+version = "0.99.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bc655351f820d774679da6cdc23355a93de496867d8203496675162e17b1d671"
 dependencies = [
- "proc-macro2 1.0.10 (registry+https://github.com/rust-lang/crates.io-index)",
- "quote 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)",
- "syn 1.0.17 (registry+https://github.com/rust-lang/crates.io-index)",
+ "proc-macro2",
+ "quote",
+ "syn",
 ]
 
 [[package]]
 name = "diesel"
-version = "1.4.4"
+version = "1.4.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3e2de9deab977a153492a1468d1b1c0662c1cf39e5ea87d0c060ecd59ef18d8c"
 dependencies = [
- "bitflags 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
- "byteorder 1.3.4 (registry+https://github.com/rust-lang/crates.io-index)",
- "chrono 0.4.11 (registry+https://github.com/rust-lang/crates.io-index)",
- "diesel_derives 1.4.1 (registry+https://github.com/rust-lang/crates.io-index)",
- "pq-sys 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)",
- "r2d2 0.8.8 (registry+https://github.com/rust-lang/crates.io-index)",
+ "bitflags",
+ "byteorder",
+ "chrono",
+ "diesel_derives",
+ "pq-sys",
+ "r2d2",
+ "serde_json",
 ]
 
 [[package]]
 name = "diesel_derives"
 version = "1.4.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "45f5098f628d02a7a0f68ddba586fb61e80edec3bdc1be3b921f4ceec60858d3"
 dependencies = [
- "proc-macro2 1.0.10 (registry+https://github.com/rust-lang/crates.io-index)",
- "quote 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)",
- "syn 1.0.17 (registry+https://github.com/rust-lang/crates.io-index)",
+ "proc-macro2",
+ "quote",
+ "syn",
 ]
 
 [[package]]
 name = "diesel_migrations"
 version = "1.4.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bf3cde8413353dc7f5d72fa8ce0b99a560a359d2c5ef1e5817ca731cd9008f4c"
 dependencies = [
- "migrations_internals 1.4.1 (registry+https://github.com/rust-lang/crates.io-index)",
- "migrations_macros 1.4.2 (registry+https://github.com/rust-lang/crates.io-index)",
+ "migrations_internals",
+ "migrations_macros",
 ]
 
 [[package]]
 name = "digest"
 version = "0.8.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f3d0c8c8752312f9713efd397ff63acb9f85585afbf179282e720e7704954dd5"
 dependencies = [
- "generic-array 0.12.3 (registry+https://github.com/rust-lang/crates.io-index)",
+ "generic-array",
 ]
 
 [[package]]
 name = "dotenv"
 version = "0.15.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f"
 
 [[package]]
 name = "dtoa"
-version = "0.4.5"
+version = "0.4.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "134951f4028bdadb9b84baf4232681efbf277da25144b9b0ad65df75946c422b"
 
 [[package]]
 name = "either"
 version = "1.5.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bb1f6b1ce1c140482ea30ddd3335fc0024ac7ee112895426e0a629a6c20adfe3"
 
 [[package]]
 name = "email"
 version = "0.0.20"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "91549a51bb0241165f13d57fc4c72cef063b4088fb078b019ecbf464a45f22e4"
 dependencies = [
- "base64 0.9.3 (registry+https://github.com/rust-lang/crates.io-index)",
- "chrono 0.4.11 (registry+https://github.com/rust-lang/crates.io-index)",
- "encoding 0.2.33 (registry+https://github.com/rust-lang/crates.io-index)",
- "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
- "rand 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)",
- "time 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)",
- "version_check 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
+ "base64 0.9.3",
+ "chrono",
+ "encoding",
+ "lazy_static",
+ "rand 0.4.6",
+ "time",
+ "version_check 0.1.5",
 ]
 
 [[package]]
 name = "encoding"
 version = "0.2.33"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6b0d943856b990d12d3b55b359144ff341533e516d94098b1d3fc1ac666d36ec"
 dependencies = [
- "encoding-index-japanese 1.20141219.5 (registry+https://github.com/rust-lang/crates.io-index)",
- "encoding-index-korean 1.20141219.5 (registry+https://github.com/rust-lang/crates.io-index)",
- "encoding-index-simpchinese 1.20141219.5 (registry+https://github.com/rust-lang/crates.io-index)",
- "encoding-index-singlebyte 1.20141219.5 (registry+https://github.com/rust-lang/crates.io-index)",
- "encoding-index-tradchinese 1.20141219.5 (registry+https://github.com/rust-lang/crates.io-index)",
+ "encoding-index-japanese",
+ "encoding-index-korean",
+ "encoding-index-simpchinese",
+ "encoding-index-singlebyte",
+ "encoding-index-tradchinese",
 ]
 
 [[package]]
 name = "encoding-index-japanese"
 version = "1.20141219.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "04e8b2ff42e9a05335dbf8b5c6f7567e5591d0d916ccef4e0b1710d32a0d0c91"
 dependencies = [
- "encoding_index_tests 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)",
+ "encoding_index_tests",
 ]
 
 [[package]]
 name = "encoding-index-korean"
 version = "1.20141219.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4dc33fb8e6bcba213fe2f14275f0963fd16f0a02c878e3095ecfdf5bee529d81"
 dependencies = [
- "encoding_index_tests 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)",
+ "encoding_index_tests",
 ]
 
 [[package]]
 name = "encoding-index-simpchinese"
 version = "1.20141219.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d87a7194909b9118fc707194baa434a4e3b0fb6a5a757c73c3adb07aa25031f7"
 dependencies = [
- "encoding_index_tests 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)",
+ "encoding_index_tests",
 ]
 
 [[package]]
 name = "encoding-index-singlebyte"
 version = "1.20141219.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3351d5acffb224af9ca265f435b859c7c01537c0849754d3db3fdf2bfe2ae84a"
 dependencies = [
- "encoding_index_tests 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)",
+ "encoding_index_tests",
 ]
 
 [[package]]
 name = "encoding-index-tradchinese"
 version = "1.20141219.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fd0e20d5688ce3cab59eb3ef3a2083a5c77bf496cb798dc6fcdb75f323890c18"
 dependencies = [
- "encoding_index_tests 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)",
+ "encoding_index_tests",
 ]
 
 [[package]]
 name = "encoding_index_tests"
 version = "0.1.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a246d82be1c9d791c5dfde9a2bd045fc3cbba3fa2b11ad558f27d01712f00569"
 
 [[package]]
 name = "encoding_rs"
-version = "0.8.22"
+version = "0.8.23"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e8ac63f94732332f44fe654443c46f6375d1939684c17b0afb6cb56b0456e171"
 dependencies = [
- "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)",
+ "cfg-if",
 ]
 
 [[package]]
 name = "entities"
 version = "1.0.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b5320ae4c3782150d900b79807611a59a99fc9a1d61d686faafc24b93fc8d7ca"
 
 [[package]]
 name = "enum-as-inner"
 version = "0.3.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bc4bfcfacb61d231109d1d55202c1f33263319668b168843e02ad4652725ec9c"
 dependencies = [
- "heck 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
- "proc-macro2 1.0.10 (registry+https://github.com/rust-lang/crates.io-index)",
- "quote 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)",
- "syn 1.0.17 (registry+https://github.com/rust-lang/crates.io-index)",
+ "heck",
+ "proc-macro2",
+ "quote",
+ "syn",
 ]
 
 [[package]]
 name = "env_logger"
 version = "0.7.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "44533bbbb3bb3c1fa17d9f2e4e38bbbaf8396ba82193c4cb1b6445d711445d36"
 dependencies = [
- "atty 0.2.14 (registry+https://github.com/rust-lang/crates.io-index)",
- "humantime 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
- "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)",
- "regex 1.3.7 (registry+https://github.com/rust-lang/crates.io-index)",
- "termcolor 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "atty",
+ "humantime",
+ "log",
+ "regex",
+ "termcolor",
 ]
 
 [[package]]
 name = "failure"
 version = "0.1.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d32e9bd16cc02eae7db7ef620b392808b89f6a5e16bb3497d159c6b92a0f4f86"
 dependencies = [
- "backtrace 0.3.46 (registry+https://github.com/rust-lang/crates.io-index)",
- "failure_derive 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)",
+ "backtrace",
+ "failure_derive",
 ]
 
 [[package]]
 name = "failure_derive"
-version = "0.1.7"
+version = "0.1.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "aa4da3c766cd7a0db8242e326e9e4e081edd567072893ed320008189715366a4"
 dependencies = [
- "proc-macro2 1.0.10 (registry+https://github.com/rust-lang/crates.io-index)",
- "quote 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)",
- "syn 1.0.17 (registry+https://github.com/rust-lang/crates.io-index)",
- "synstructure 0.12.3 (registry+https://github.com/rust-lang/crates.io-index)",
+ "proc-macro2",
+ "quote",
+ "syn",
+ "synstructure",
 ]
 
 [[package]]
 name = "fake-simd"
 version = "0.1.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e88a8acf291dafb59c2d96e8f59828f3838bb1a70398823ade51a84de6a6deed"
 
 [[package]]
 name = "fast_chemail"
 version = "0.9.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "495a39d30d624c2caabe6312bfead73e7717692b44e0b32df168c275a2e8e9e4"
 dependencies = [
- "ascii_utils 0.9.3 (registry+https://github.com/rust-lang/crates.io-index)",
+ "ascii_utils",
 ]
 
 [[package]]
 name = "flate2"
 version = "1.0.14"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2cfff41391129e0a856d6d822600b8d71179d46879e310417eb9c762eb178b42"
 dependencies = [
- "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)",
- "crc32fast 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
- "libc 0.2.69 (registry+https://github.com/rust-lang/crates.io-index)",
- "miniz_oxide 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)",
+ "cfg-if",
+ "crc32fast",
+ "libc",
+ "miniz_oxide",
 ]
 
 [[package]]
 name = "fnv"
-version = "1.0.6"
+version = "1.0.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
 
 [[package]]
 name = "foreign-types"
 version = "0.3.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
 dependencies = [
- "foreign-types-shared 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
+ "foreign-types-shared",
 ]
 
 [[package]]
 name = "foreign-types-shared"
 version = "0.1.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
 
 [[package]]
 name = "fuchsia-cprng"
 version = "0.1.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba"
 
 [[package]]
 name = "fuchsia-zircon"
 version = "0.3.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2e9763c69ebaae630ba35f74888db465e49e259ba1bc0eda7d06f4a067615d82"
 dependencies = [
- "bitflags 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
- "fuchsia-zircon-sys 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)",
+ "bitflags",
+ "fuchsia-zircon-sys",
 ]
 
 [[package]]
 name = "fuchsia-zircon-sys"
 version = "0.3.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3dcaa9ae7725d12cdb85b3ad99a434db70b468c09ded17e012d86b5c1010f7a7"
 
 [[package]]
 name = "futures"
-version = "0.3.4"
+version = "0.3.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e05b85ec287aac0dc34db7d4a569323df697f9c55b99b15d6b4ef8cde49f613"
 dependencies = [
- "futures-channel 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)",
- "futures-core 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)",
- "futures-executor 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)",
- "futures-io 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)",
- "futures-sink 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)",
- "futures-task 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)",
- "futures-util 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)",
+ "futures-channel",
+ "futures-core",
+ "futures-executor",
+ "futures-io",
+ "futures-sink",
+ "futures-task",
+ "futures-util",
 ]
 
 [[package]]
 name = "futures-channel"
-version = "0.3.4"
+version = "0.3.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f366ad74c28cca6ba456d95e6422883cfb4b252a83bed929c83abfdbbf2967d5"
 dependencies = [
- "futures-core 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)",
- "futures-sink 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)",
+ "futures-core",
+ "futures-sink",
 ]
 
 [[package]]
 name = "futures-core"
-version = "0.3.4"
+version = "0.3.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "59f5fff90fd5d971f936ad674802482ba441b6f09ba5e15fd8b39145582ca399"
 
 [[package]]
 name = "futures-executor"
-version = "0.3.4"
+version = "0.3.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "10d6bb888be1153d3abeb9006b11b02cf5e9b209fda28693c31ae1e4e012e314"
 dependencies = [
- "futures-core 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)",
- "futures-task 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)",
- "futures-util 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)",
+ "futures-core",
+ "futures-task",
+ "futures-util",
 ]
 
 [[package]]
 name = "futures-io"
-version = "0.3.4"
+version = "0.3.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "de27142b013a8e869c14957e6d2edeef89e97c289e69d042ee3a49acd8b51789"
 
 [[package]]
 name = "futures-macro"
-version = "0.3.4"
+version = "0.3.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d0b5a30a4328ab5473878237c447333c093297bded83a4983d10f4deea240d39"
 dependencies = [
- "proc-macro-hack 0.5.15 (registry+https://github.com/rust-lang/crates.io-index)",
- "proc-macro2 1.0.10 (registry+https://github.com/rust-lang/crates.io-index)",
- "quote 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)",
- "syn 1.0.17 (registry+https://github.com/rust-lang/crates.io-index)",
+ "proc-macro-hack",
+ "proc-macro2",
+ "quote",
+ "syn",
 ]
 
 [[package]]
 name = "futures-sink"
-version = "0.3.4"
+version = "0.3.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f2032893cb734c7a05d85ce0cc8b8c4075278e93b24b66f9de99d6eb0fa8acc"
 
 [[package]]
 name = "futures-task"
-version = "0.3.4"
+version = "0.3.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bdb66b5f09e22019b1ab0830f7785bcea8e7a42148683f99214f73f8ec21a626"
+dependencies = [
+ "once_cell",
+]
 
 [[package]]
 name = "futures-util"
-version = "0.3.4"
+version = "0.3.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8764574ff08b701a084482c3c7031349104b07ac897393010494beaa18ce32c6"
 dependencies = [
- "futures-channel 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)",
- "futures-core 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)",
- "futures-io 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)",
- "futures-macro 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)",
- "futures-sink 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)",
- "futures-task 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)",
- "memchr 2.3.3 (registry+https://github.com/rust-lang/crates.io-index)",
- "pin-utils 0.1.0-alpha.4 (registry+https://github.com/rust-lang/crates.io-index)",
- "proc-macro-hack 0.5.15 (registry+https://github.com/rust-lang/crates.io-index)",
- "proc-macro-nested 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)",
- "slab 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)",
+ "futures-channel",
+ "futures-core",
+ "futures-io",
+ "futures-macro",
+ "futures-sink",
+ "futures-task",
+ "memchr",
+ "pin-project",
+ "pin-utils",
+ "proc-macro-hack",
+ "proc-macro-nested",
+ "slab",
 ]
 
 [[package]]
 name = "fxhash"
 version = "0.2.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c"
+dependencies = [
+ "byteorder",
+]
+
+[[package]]
+name = "generator"
+version = "0.6.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "add72f17bb81521258fcc8a7a3245b1e184e916bfbe34f0ea89558f440df5c68"
 dependencies = [
- "byteorder 1.3.4 (registry+https://github.com/rust-lang/crates.io-index)",
+ "cc",
+ "libc",
+ "log",
+ "rustc_version",
+ "winapi 0.3.8",
 ]
 
 [[package]]
 name = "generic-array"
 version = "0.12.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c68f0274ae0e023facc3c97b2e00f076be70e254bc851d972503b328db79b2ec"
 dependencies = [
- "typenum 1.12.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "typenum",
 ]
 
 [[package]]
 name = "getrandom"
 version = "0.1.14"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7abc8dd8451921606d809ba32e95b6111925cd2906060d2dcc29c070220503eb"
 dependencies = [
- "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)",
- "libc 0.2.69 (registry+https://github.com/rust-lang/crates.io-index)",
- "wasi 0.9.0+wasi-snapshot-preview1 (registry+https://github.com/rust-lang/crates.io-index)",
+ "cfg-if",
+ "libc",
+ "wasi",
 ]
 
+[[package]]
+name = "gimli"
+version = "0.21.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bcc8e0c9bce37868955864dbecd2b1ab2bdf967e6f28066d65aaac620444b65c"
+
 [[package]]
 name = "h2"
-version = "0.2.4"
+version = "0.2.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "79b7246d7e4b979c03fa093da39cfb3617a96bbeee6310af63991668d7e843ff"
 dependencies = [
- "bytes 0.5.4 (registry+https://github.com/rust-lang/crates.io-index)",
- "fnv 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)",
- "futures-core 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)",
- "futures-sink 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)",
- "futures-util 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)",
- "http 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
- "indexmap 1.3.2 (registry+https://github.com/rust-lang/crates.io-index)",
- "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)",
- "slab 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)",
- "tokio 0.2.20 (registry+https://github.com/rust-lang/crates.io-index)",
- "tokio-util 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
+ "bytes",
+ "fnv",
+ "futures-core",
+ "futures-sink",
+ "futures-util",
+ "http",
+ "indexmap",
+ "log",
+ "slab",
+ "tokio",
+ "tokio-util 0.3.1",
 ]
 
 [[package]]
 name = "heck"
 version = "0.3.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "20564e78d53d2bb135c343b3f47714a56af2061f1c928fdb541dc7b9fdd94205"
 dependencies = [
- "unicode-segmentation 1.6.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "unicode-segmentation",
 ]
 
 [[package]]
 name = "hermit-abi"
-version = "0.1.11"
+version = "0.1.14"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b9586eedd4ce6b3c498bc3b4dd92fc9f11166aa908a914071953768066c67909"
 dependencies = [
- "libc 0.2.69 (registry+https://github.com/rust-lang/crates.io-index)",
+ "libc",
 ]
 
 [[package]]
 name = "hostname"
 version = "0.1.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "21ceb46a83a85e824ef93669c8b390009623863b5c195d1ba747292c0c72f94e"
 dependencies = [
- "libc 0.2.69 (registry+https://github.com/rust-lang/crates.io-index)",
- "winutil 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
+ "libc",
+ "winutil",
 ]
 
 [[package]]
 name = "hostname"
 version = "0.3.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3c731c3e10504cc8ed35cfe2f1db4c9274c3d35fa486e3b31df46f068ef3e867"
 dependencies = [
- "libc 0.2.69 (registry+https://github.com/rust-lang/crates.io-index)",
- "match_cfg 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
- "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)",
+ "libc",
+ "match_cfg",
+ "winapi 0.3.8",
 ]
 
 [[package]]
 name = "htmlescape"
 version = "0.3.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e9025058dae765dee5070ec375f591e2ba14638c63feff74f13805a72e523163"
 
 [[package]]
 name = "http"
 version = "0.2.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "28d569972648b2c512421b5f2a405ad6ac9666547189d0c5477a3f200f3e02f9"
 dependencies = [
- "bytes 0.5.4 (registry+https://github.com/rust-lang/crates.io-index)",
- "fnv 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)",
- "itoa 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)",
+ "bytes",
+ "fnv",
+ "itoa",
+]
+
+[[package]]
+name = "http-signature-normalization"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "648233553603e7bb55bc1ea08a514661e212c09c10f6434507894273d8b5e773"
+dependencies = [
+ "chrono",
+ "thiserror",
 ]
 
 [[package]]
 name = "httparse"
 version = "1.3.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cd179ae861f0c2e53da70d892f5f3029f9594be0c41dc5269cd371691b1dc2f9"
 
 [[package]]
 name = "humantime"
 version = "1.3.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "df004cfca50ef23c36850aaaa59ad52cc70d0e90243c3c7737a4dd32dc7a3c4f"
 dependencies = [
- "quick-error 1.2.3 (registry+https://github.com/rust-lang/crates.io-index)",
+ "quick-error",
 ]
 
 [[package]]
 name = "ident_case"
 version = "1.0.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
 
 [[package]]
 name = "idna"
 version = "0.2.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "02e2673c30ee86b5b96a9cb52ad15718aa1f966f5ab9ad54a8b95d5ca33120a9"
 dependencies = [
- "matches 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)",
- "unicode-bidi 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)",
- "unicode-normalization 0.1.12 (registry+https://github.com/rust-lang/crates.io-index)",
+ "matches",
+ "unicode-bidi",
+ "unicode-normalization",
 ]
 
 [[package]]
 name = "indexmap"
-version = "1.3.2"
+version = "1.4.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c398b2b113b55809ceb9ee3e753fcbac793f1956663f3c36549c1346015c2afe"
 dependencies = [
- "autocfg 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "autocfg 1.0.0",
 ]
 
 [[package]]
 name = "iovec"
 version = "0.1.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b2b3ea6ff95e175473f8ffe6a7eb7c00d054240321b84c57051175fe3c1e075e"
 dependencies = [
- "libc 0.2.69 (registry+https://github.com/rust-lang/crates.io-index)",
+ "libc",
 ]
 
 [[package]]
 name = "ipconfig"
-version = "0.2.1"
+version = "0.2.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f7e2f18aece9709094573a9f24f483c4f65caa4298e2f7ae1b71cc65d853fad7"
 dependencies = [
- "socket2 0.3.12 (registry+https://github.com/rust-lang/crates.io-index)",
- "widestring 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
- "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)",
- "winreg 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)",
+ "socket2",
+ "widestring",
+ "winapi 0.3.8",
+ "winreg",
+]
+
+[[package]]
+name = "itertools"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "284f18f85651fe11e8a991b2adb42cb078325c996ed026d994719efcfca1d54b"
+dependencies = [
+ "either",
 ]
 
 [[package]]
 name = "itoa"
-version = "0.4.5"
+version = "0.4.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc6f3ad7b9d11a0c00842ff8de1b60ee58661048eb8049ed33c73594f359d7e6"
 
 [[package]]
 name = "js-sys"
-version = "0.3.37"
+version = "0.3.40"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ce10c23ad2ea25ceca0093bd3192229da4c5b3c0f2de499c1ecac0d98d452177"
 dependencies = [
- "wasm-bindgen 0.2.60 (registry+https://github.com/rust-lang/crates.io-index)",
+ "wasm-bindgen",
 ]
 
 [[package]]
 name = "jsonwebtoken"
-version = "7.1.0"
+version = "7.1.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b1f325ae57ddcf609f02d891486ce740f5bbd0cc3e93f9bffaacdf6594b21404"
 dependencies = [
- "base64 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)",
- "pem 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)",
- "ring 0.16.12 (registry+https://github.com/rust-lang/crates.io-index)",
- "serde 1.0.106 (registry+https://github.com/rust-lang/crates.io-index)",
- "serde_json 1.0.52 (registry+https://github.com/rust-lang/crates.io-index)",
- "simple_asn1 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "base64 0.12.2",
+ "pem",
+ "ring",
+ "serde 1.0.114",
+ "serde_json",
+ "simple_asn1",
 ]
 
 [[package]]
 name = "kernel32-sys"
 version = "0.2.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7507624b29483431c0ba2d82aece8ca6cdba9382bff4ddd0f7490560c056098d"
 dependencies = [
- "winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)",
- "winapi-build 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
+ "winapi 0.2.8",
+ "winapi-build",
 ]
 
 [[package]]
 name = "language-tags"
 version = "0.2.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a91d884b6667cd606bb5a69aa0c99ba811a115fc68915e7056ec08a46e93199a"
 
 [[package]]
 name = "lazy_static"
 version = "1.4.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
 
 [[package]]
 name = "lemmy_server"
 version = "0.0.1"
 dependencies = [
- "activitypub 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
- "actix 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)",
- "actix-files 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
- "actix-rt 1.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
- "actix-web 2.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
- "actix-web-actors 2.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
- "attohttpc 0.14.0 (registry+https://github.com/rust-lang/crates.io-index)",
- "bcrypt 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)",
- "chrono 0.4.11 (registry+https://github.com/rust-lang/crates.io-index)",
- "comrak 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)",
- "config 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)",
- "diesel 1.4.4 (registry+https://github.com/rust-lang/crates.io-index)",
- "diesel_migrations 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
- "dotenv 0.15.0 (registry+https://github.com/rust-lang/crates.io-index)",
- "env_logger 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)",
- "failure 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)",
- "futures 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)",
- "htmlescape 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
- "jsonwebtoken 7.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
- "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
- "lettre 0.9.3 (registry+https://github.com/rust-lang/crates.io-index)",
- "lettre_email 0.9.4 (registry+https://github.com/rust-lang/crates.io-index)",
- "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)",
- "percent-encoding 2.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
- "rand 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)",
- "regex 1.3.7 (registry+https://github.com/rust-lang/crates.io-index)",
- "rss 1.9.0 (registry+https://github.com/rust-lang/crates.io-index)",
- "serde 1.0.106 (registry+https://github.com/rust-lang/crates.io-index)",
- "serde_json 1.0.52 (registry+https://github.com/rust-lang/crates.io-index)",
- "sha2 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)",
- "strum 0.18.0 (registry+https://github.com/rust-lang/crates.io-index)",
- "strum_macros 0.18.0 (registry+https://github.com/rust-lang/crates.io-index)",
- "tokio 0.2.20 (registry+https://github.com/rust-lang/crates.io-index)",
+ "activitystreams",
+ "activitystreams-ext",
+ "activitystreams-new",
+ "actix",
+ "actix-files",
+ "actix-rt",
+ "actix-web",
+ "actix-web-actors",
+ "attohttpc",
+ "base64 0.12.2",
+ "bcrypt",
+ "chrono",
+ "comrak",
+ "config",
+ "diesel",
+ "diesel_migrations",
+ "dotenv",
+ "env_logger",
+ "failure",
+ "futures",
+ "htmlescape",
+ "http",
+ "http-signature-normalization",
+ "itertools",
+ "jsonwebtoken",
+ "lazy_static",
+ "lettre",
+ "lettre_email",
+ "log",
+ "openssl",
+ "percent-encoding",
+ "rand 0.7.3",
+ "regex",
+ "rss",
+ "serde 1.0.114",
+ "serde_json",
+ "sha2",
+ "strum",
+ "strum_macros",
+ "tokio",
+ "url",
+ "uuid 0.8.1",
 ]
 
 [[package]]
 name = "lettre"
 version = "0.9.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bf43f3202a879fbdab4ecafec3349b0139f81d31c626246d53bcbb546253ffaa"
 dependencies = [
- "base64 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)",
- "bufstream 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)",
- "fast_chemail 0.9.6 (registry+https://github.com/rust-lang/crates.io-index)",
- "hostname 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
- "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)",
- "native-tls 0.2.4 (registry+https://github.com/rust-lang/crates.io-index)",
- "nom 4.2.3 (registry+https://github.com/rust-lang/crates.io-index)",
- "serde 1.0.106 (registry+https://github.com/rust-lang/crates.io-index)",
- "serde_derive 1.0.106 (registry+https://github.com/rust-lang/crates.io-index)",
- "serde_json 1.0.52 (registry+https://github.com/rust-lang/crates.io-index)",
+ "base64 0.10.1",
+ "bufstream",
+ "fast_chemail",
+ "hostname 0.1.5",
+ "log",
+ "native-tls",
+ "nom 4.2.3",
+ "serde 1.0.114",
+ "serde_derive",
+ "serde_json",
 ]
 
 [[package]]
 name = "lettre_email"
 version = "0.9.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fd02480f8dcf48798e62113974d6ccca2129a51d241fa20f1ea349c8a42559d5"
 dependencies = [
- "base64 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)",
- "email 0.0.20 (registry+https://github.com/rust-lang/crates.io-index)",
- "lettre 0.9.3 (registry+https://github.com/rust-lang/crates.io-index)",
- "mime 0.3.16 (registry+https://github.com/rust-lang/crates.io-index)",
- "time 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)",
- "uuid 0.7.4 (registry+https://github.com/rust-lang/crates.io-index)",
+ "base64 0.10.1",
+ "email",
+ "lettre",
+ "mime",
+ "time",
+ "uuid 0.7.4",
 ]
 
 [[package]]
 name = "lexical-core"
-version = "0.6.2"
+version = "0.7.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "db65c6da02e61f55dae90a0ae427b2a5f6b3e8db09f58d10efab23af92592616"
 dependencies = [
- "arrayvec 0.4.12 (registry+https://github.com/rust-lang/crates.io-index)",
- "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)",
- "rustc_version 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)",
- "ryu 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)",
- "static_assertions 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)",
+ "arrayvec",
+ "bitflags",
+ "cfg-if",
+ "ryu",
+ "static_assertions",
 ]
 
 [[package]]
 name = "libc"
-version = "0.2.69"
+version = "0.2.71"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9457b06509d27052635f90d6466700c65095fdf75409b3fbdd903e988b886f49"
 
 [[package]]
 name = "linked-hash-map"
 version = "0.3.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6d262045c5b87c0861b3f004610afd0e2c851e2908d08b6c870cbb9d5f494ecd"
 dependencies = [
- "serde 0.8.23 (registry+https://github.com/rust-lang/crates.io-index)",
- "serde_test 0.8.23 (registry+https://github.com/rust-lang/crates.io-index)",
+ "serde 0.8.23",
+ "serde_test",
 ]
 
 [[package]]
 name = "linked-hash-map"
-version = "0.5.2"
+version = "0.5.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8dd5a6d5999d9907cda8ed67bbd137d3af8085216c2ac62de5be860bd41f304a"
 
 [[package]]
 name = "lock_api"
 version = "0.3.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c4da24a77a3d8a6d4862d95f72e6fdb9c09a643ecdb402d754004a557f2bec75"
 dependencies = [
- "scopeguard 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "scopeguard",
 ]
 
 [[package]]
 name = "log"
 version = "0.4.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "14b6052be84e6b71ab17edffc2eeabf5c2c3ae1fdb464aae35ac50c67a44e1f7"
 dependencies = [
- "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)",
+ "cfg-if",
+]
+
+[[package]]
+name = "loom"
+version = "0.3.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4ecc775857611e1df29abba5c41355cdf540e7e9d4acfdf0f355eefee82330b7"
+dependencies = [
+ "cfg-if",
+ "generator",
+ "scoped-tls",
 ]
 
 [[package]]
 name = "lru-cache"
 version = "0.1.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "31e24f1ad8321ca0e8a1e0ac13f23cb668e6f5466c2c57319f6a5cf1cc8e3b1c"
 dependencies = [
- "linked-hash-map 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)",
+ "linked-hash-map 0.5.3",
 ]
 
 [[package]]
 name = "maplit"
 version = "1.0.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d"
 
 [[package]]
 name = "match_cfg"
 version = "0.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ffbee8634e0d45d258acb448e7eaab3fce7a0a467395d4d9f228e3c1f01fb2e4"
 
 [[package]]
 name = "matches"
 version = "0.1.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7ffc5c5338469d4d3ea17d269fa8ea3512ad247247c30bd2df69e68309ed0a08"
 
 [[package]]
 name = "maybe-uninit"
 version = "2.0.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "60302e4db3a61da70c0cb7991976248362f30319e88850c487b9b95bbf059e00"
 
 [[package]]
 name = "memchr"
 version = "2.3.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3728d817d99e5ac407411fa471ff9800a778d88a24685968b36824eaf4bee400"
 
 [[package]]
 name = "migrations_internals"
 version = "1.4.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2b4fc84e4af020b837029e017966f86a1c2d5e83e64b589963d5047525995860"
 dependencies = [
- "diesel 1.4.4 (registry+https://github.com/rust-lang/crates.io-index)",
+ "diesel",
 ]
 
 [[package]]
 name = "migrations_macros"
 version = "1.4.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9753f12909fd8d923f75ae5c3258cae1ed3c8ec052e1b38c93c21a6d157f789c"
 dependencies = [
- "migrations_internals 1.4.1 (registry+https://github.com/rust-lang/crates.io-index)",
- "proc-macro2 1.0.10 (registry+https://github.com/rust-lang/crates.io-index)",
- "quote 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)",
- "syn 1.0.17 (registry+https://github.com/rust-lang/crates.io-index)",
+ "migrations_internals",
+ "proc-macro2",
+ "quote",
+ "syn",
 ]
 
 [[package]]
 name = "mime"
 version = "0.3.16"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d"
 
 [[package]]
 name = "mime_guess"
 version = "2.0.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2684d4c2e97d99848d30b324b00c8fcc7e5c897b7cbb5819b09e7c90e8baf212"
 dependencies = [
- "mime 0.3.16 (registry+https://github.com/rust-lang/crates.io-index)",
- "unicase 2.6.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "mime",
+ "unicase",
 ]
 
 [[package]]
 name = "miniz_oxide"
-version = "0.3.6"
+version = "0.3.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "791daaae1ed6889560f8c4359194f56648355540573244a5448a83ba1ecc7435"
 dependencies = [
- "adler32 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)",
+ "adler32",
 ]
 
 [[package]]
 name = "mio"
-version = "0.6.21"
+version = "0.6.22"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fce347092656428bc8eaf6201042cb551b8d67855af7374542a92a0fbfcac430"
 dependencies = [
- "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)",
- "fuchsia-zircon 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)",
- "fuchsia-zircon-sys 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)",
- "iovec 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)",
- "kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
- "libc 0.2.69 (registry+https://github.com/rust-lang/crates.io-index)",
- "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)",
- "miow 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
- "net2 0.2.33 (registry+https://github.com/rust-lang/crates.io-index)",
- "slab 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)",
- "winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)",
+ "cfg-if",
+ "fuchsia-zircon",
+ "fuchsia-zircon-sys",
+ "iovec",
+ "kernel32-sys",
+ "libc",
+ "log",
+ "miow",
+ "net2",
+ "slab",
+ "winapi 0.2.8",
 ]
 
 [[package]]
 name = "mio-uds"
-version = "0.6.7"
+version = "0.6.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "afcb699eb26d4332647cc848492bbc15eafb26f08d0304550d5aa1f612e066f0"
 dependencies = [
- "iovec 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)",
- "libc 0.2.69 (registry+https://github.com/rust-lang/crates.io-index)",
- "mio 0.6.21 (registry+https://github.com/rust-lang/crates.io-index)",
+ "iovec",
+ "libc",
+ "mio",
 ]
 
 [[package]]
 name = "miow"
 version = "0.2.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8c1f2f3b1cf331de6896aabf6e9d55dca90356cc9960cca7eaaf408a355ae919"
 dependencies = [
- "kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
- "net2 0.2.33 (registry+https://github.com/rust-lang/crates.io-index)",
- "winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)",
- "ws2_32-sys 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
+ "kernel32-sys",
+ "net2",
+ "winapi 0.2.8",
+ "ws2_32-sys",
 ]
 
 [[package]]
 name = "native-tls"
 version = "0.2.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2b0d88c06fe90d5ee94048ba40409ef1d9315d86f6f38c2efdaad4fb50c58b2d"
 dependencies = [
- "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
- "libc 0.2.69 (registry+https://github.com/rust-lang/crates.io-index)",
- "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)",
- "openssl 0.10.29 (registry+https://github.com/rust-lang/crates.io-index)",
- "openssl-probe 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
- "openssl-sys 0.9.58 (registry+https://github.com/rust-lang/crates.io-index)",
- "schannel 0.1.19 (registry+https://github.com/rust-lang/crates.io-index)",
- "security-framework 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)",
- "security-framework-sys 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)",
- "tempfile 3.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "lazy_static",
+ "libc",
+ "log",
+ "openssl",
+ "openssl-probe",
+ "openssl-sys",
+ "schannel",
+ "security-framework",
+ "security-framework-sys",
+ "tempfile",
 ]
 
 [[package]]
 name = "net2"
-version = "0.2.33"
+version = "0.2.34"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2ba7c918ac76704fb42afcbbb43891e72731f3dcca3bef2a19786297baf14af7"
 dependencies = [
- "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)",
- "libc 0.2.69 (registry+https://github.com/rust-lang/crates.io-index)",
- "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)",
+ "cfg-if",
+ "libc",
+ "winapi 0.3.8",
 ]
 
-[[package]]
-name = "nodrop"
-version = "0.1.14"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-
 [[package]]
 name = "nom"
 version = "4.2.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2ad2a91a8e869eeb30b9cb3119ae87773a8f4ae617f41b1eb9c154b2905f7bd6"
 dependencies = [
- "memchr 2.3.3 (registry+https://github.com/rust-lang/crates.io-index)",
- "version_check 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
+ "memchr",
+ "version_check 0.1.5",
 ]
 
 [[package]]
 name = "nom"
-version = "5.1.1"
+version = "5.1.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ffb4262d26ed83a1c0a33a38fe2bb15797329c85770da05e6b828ddb782627af"
 dependencies = [
- "lexical-core 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)",
- "memchr 2.3.3 (registry+https://github.com/rust-lang/crates.io-index)",
- "version_check 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)",
+ "lexical-core",
+ "memchr",
+ "version_check 0.9.2",
 ]
 
 [[package]]
 name = "num-bigint"
 version = "0.2.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "090c7f9998ee0ff65aa5b723e4009f7b217707f1fb5ea551329cc4d6231fb304"
 dependencies = [
- "autocfg 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
- "num-integer 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)",
- "num-traits 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)",
+ "autocfg 1.0.0",
+ "num-integer",
+ "num-traits 0.2.12",
 ]
 
 [[package]]
 name = "num-integer"
-version = "0.1.42"
+version = "0.1.43"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8d59457e662d541ba17869cf51cf177c0b5f0cbf476c66bdc90bf1edac4f875b"
 dependencies = [
- "autocfg 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
- "num-traits 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)",
+ "autocfg 1.0.0",
+ "num-traits 0.2.12",
 ]
 
 [[package]]
 name = "num-traits"
 version = "0.1.43"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "92e5113e9fd4cc14ded8e499429f396a20f98c772a47cc8622a736e1ec843c31"
 dependencies = [
- "num-traits 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)",
+ "num-traits 0.2.12",
 ]
 
 [[package]]
 name = "num-traits"
-version = "0.2.11"
+version = "0.2.12"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac267bcc07f48ee5f8935ab0d24f316fb722d7a1292e2913f0cc196b29ffd611"
 dependencies = [
- "autocfg 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "autocfg 1.0.0",
 ]
 
 [[package]]
 name = "num_cpus"
 version = "1.13.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "05499f3756671c15885fee9034446956fff3f243d6077b91e5767df161f766b3"
 dependencies = [
- "hermit-abi 0.1.11 (registry+https://github.com/rust-lang/crates.io-index)",
- "libc 0.2.69 (registry+https://github.com/rust-lang/crates.io-index)",
+ "hermit-abi",
+ "libc",
 ]
 
+[[package]]
+name = "object"
+version = "0.20.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1ab52be62400ca80aa00285d25253d7f7c437b7375c4de678f5405d3afe82ca5"
+
+[[package]]
+name = "once_cell"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b631f7e854af39a1739f401cf34a8a013dfe09eac4fa4dba91e9768bd28168d"
+
 [[package]]
 name = "opaque-debug"
 version = "0.2.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2839e79665f131bdb5782e51f2c6c9599c133c6098982a54c794358bf432529c"
 
 [[package]]
 name = "openssl"
 version = "0.10.29"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cee6d85f4cb4c4f59a6a85d5b68a233d280c82e29e822913b9c8b129fbf20bdd"
 dependencies = [
- "bitflags 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
- "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)",
- "foreign-types 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)",
- "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
- "libc 0.2.69 (registry+https://github.com/rust-lang/crates.io-index)",
- "openssl-sys 0.9.58 (registry+https://github.com/rust-lang/crates.io-index)",
+ "bitflags",
+ "cfg-if",
+ "foreign-types",
+ "lazy_static",
+ "libc",
+ "openssl-sys",
 ]
 
 [[package]]
 name = "openssl-probe"
 version = "0.1.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "77af24da69f9d9341038eba93a073b1fdaaa1b788221b00a69bce9e762cb32de"
 
 [[package]]
 name = "openssl-sys"
 version = "0.9.58"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a842db4709b604f0fe5d1170ae3565899be2ad3d9cbc72dedc789ac0511f78de"
 dependencies = [
- "autocfg 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
- "cc 1.0.50 (registry+https://github.com/rust-lang/crates.io-index)",
- "libc 0.2.69 (registry+https://github.com/rust-lang/crates.io-index)",
- "pkg-config 0.3.17 (registry+https://github.com/rust-lang/crates.io-index)",
- "vcpkg 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)",
+ "autocfg 1.0.0",
+ "cc",
+ "libc",
+ "pkg-config",
+ "vcpkg",
 ]
 
 [[package]]
 name = "parking_lot"
 version = "0.10.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d3a704eb390aafdc107b0e392f56a82b668e3a71366993b5340f5833fd62505e"
 dependencies = [
- "lock_api 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)",
- "parking_lot_core 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)",
+ "lock_api",
+ "parking_lot_core",
 ]
 
 [[package]]
 name = "parking_lot_core"
-version = "0.7.1"
+version = "0.7.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d58c7c768d4ba344e3e8d72518ac13e259d7c7ade24167003b8488e10b6740a3"
 dependencies = [
- "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)",
- "cloudabi 0.0.3 (registry+https://github.com/rust-lang/crates.io-index)",
- "libc 0.2.69 (registry+https://github.com/rust-lang/crates.io-index)",
- "redox_syscall 0.1.56 (registry+https://github.com/rust-lang/crates.io-index)",
- "smallvec 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
- "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)",
+ "cfg-if",
+ "cloudabi",
+ "libc",
+ "redox_syscall",
+ "smallvec",
+ "winapi 0.3.8",
 ]
 
 [[package]]
 name = "pem"
-version = "0.7.0"
+version = "0.8.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "59698ea79df9bf77104aefd39cc3ec990cb9693fb59c3b0a70ddf2646fdffb4b"
 dependencies = [
- "base64 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)",
- "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
- "regex 1.3.7 (registry+https://github.com/rust-lang/crates.io-index)",
+ "base64 0.12.2",
+ "once_cell",
+ "regex",
 ]
 
 [[package]]
 name = "percent-encoding"
 version = "2.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e"
 
 [[package]]
 name = "pest"
 version = "2.1.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "10f4872ae94d7b90ae48754df22fd42ad52ce740b8f370b03da4835417403e53"
 dependencies = [
- "ucd-trie 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)",
+ "ucd-trie",
 ]
 
 [[package]]
 name = "pest_derive"
 version = "2.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "833d1ae558dc601e9a60366421196a8d94bc0ac980476d0b67e1d0988d72b2d0"
 dependencies = [
- "pest 2.1.3 (registry+https://github.com/rust-lang/crates.io-index)",
- "pest_generator 2.1.3 (registry+https://github.com/rust-lang/crates.io-index)",
+ "pest",
+ "pest_generator",
 ]
 
 [[package]]
 name = "pest_generator"
 version = "2.1.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "99b8db626e31e5b81787b9783425769681b347011cc59471e33ea46d2ea0cf55"
 dependencies = [
- "pest 2.1.3 (registry+https://github.com/rust-lang/crates.io-index)",
- "pest_meta 2.1.3 (registry+https://github.com/rust-lang/crates.io-index)",
- "proc-macro2 1.0.10 (registry+https://github.com/rust-lang/crates.io-index)",
- "quote 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)",
- "syn 1.0.17 (registry+https://github.com/rust-lang/crates.io-index)",
+ "pest",
+ "pest_meta",
+ "proc-macro2",
+ "quote",
+ "syn",
 ]
 
 [[package]]
 name = "pest_meta"
 version = "2.1.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "54be6e404f5317079812fc8f9f5279de376d8856929e21c184ecf6bbd692a11d"
 dependencies = [
- "maplit 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)",
- "pest 2.1.3 (registry+https://github.com/rust-lang/crates.io-index)",
- "sha-1 0.8.2 (registry+https://github.com/rust-lang/crates.io-index)",
+ "maplit",
+ "pest",
+ "sha-1",
 ]
 
 [[package]]
 name = "pin-project"
-version = "0.4.9"
+version = "0.4.22"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "12e3a6cdbfe94a5e4572812a0201f8c0ed98c1c452c7b8563ce2276988ef9c17"
 dependencies = [
- "pin-project-internal 0.4.9 (registry+https://github.com/rust-lang/crates.io-index)",
+ "pin-project-internal",
 ]
 
 [[package]]
 name = "pin-project-internal"
-version = "0.4.9"
+version = "0.4.22"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6a0ffd45cf79d88737d7cc85bfd5d2894bee1139b356e616fe85dc389c61aaf7"
 dependencies = [
- "proc-macro2 1.0.10 (registry+https://github.com/rust-lang/crates.io-index)",
- "quote 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)",
- "syn 1.0.17 (registry+https://github.com/rust-lang/crates.io-index)",
+ "proc-macro2",
+ "quote",
+ "syn",
 ]
 
 [[package]]
 name = "pin-project-lite"
-version = "0.1.4"
+version = "0.1.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "282adbf10f2698a7a77f8e983a74b2d18176c19a7fd32a45446139ae7b02b715"
 
 [[package]]
 name = "pin-utils"
-version = "0.1.0-alpha.4"
+version = "0.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
 
 [[package]]
 name = "pkg-config"
 version = "0.3.17"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "05da548ad6865900e60eaba7f589cc0783590a92e940c26953ff81ddbab2d677"
 
 [[package]]
 name = "ppv-lite86"
-version = "0.2.6"
+version = "0.2.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "237a5ed80e274dbc66f86bd59c1e25edc039660be53194b5fe0a482e0f2612ea"
 
 [[package]]
 name = "pq-sys"
 version = "0.4.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6ac25eee5a0582f45a67e837e350d784e7003bd29a5f460796772061ca49ffda"
 dependencies = [
- "vcpkg 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)",
+ "vcpkg",
 ]
 
 [[package]]
 name = "proc-macro-hack"
-version = "0.5.15"
+version = "0.5.16"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7e0456befd48169b9f13ef0f0ad46d492cf9d2dbb918bcf38e01eed4ce3ec5e4"
 
 [[package]]
 name = "proc-macro-nested"
-version = "0.1.4"
+version = "0.1.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eba180dafb9038b050a4c280019bbedf9f2467b61e5d892dcad585bb57aadc5a"
 
 [[package]]
 name = "proc-macro2"
-version = "1.0.10"
+version = "1.0.18"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "beae6331a816b1f65d04c45b078fd8e6c93e8071771f41b8163255bbd8d7c8fa"
 dependencies = [
- "unicode-xid 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "unicode-xid",
 ]
 
 [[package]]
 name = "quick-error"
 version = "1.2.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0"
 
 [[package]]
 name = "quick-xml"
 version = "0.17.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fe1e430bdcf30c9fdc25053b9c459bb1a4672af4617b6c783d7d91dc17c6bbb0"
 dependencies = [
- "encoding_rs 0.8.22 (registry+https://github.com/rust-lang/crates.io-index)",
- "memchr 2.3.3 (registry+https://github.com/rust-lang/crates.io-index)",
+ "encoding_rs",
+ "memchr",
 ]
 
 [[package]]
 name = "quote"
-version = "1.0.3"
+version = "1.0.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "aa563d17ecb180e500da1cfd2b028310ac758de548efdd203e18f283af693f37"
 dependencies = [
- "proc-macro2 1.0.10 (registry+https://github.com/rust-lang/crates.io-index)",
+ "proc-macro2",
 ]
 
 [[package]]
 name = "r2d2"
 version = "0.8.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1497e40855348e4a8a40767d8e55174bce1e445a3ac9254ad44ad468ee0485af"
 dependencies = [
- "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)",
- "parking_lot 0.10.2 (registry+https://github.com/rust-lang/crates.io-index)",
- "scheduled-thread-pool 0.2.4 (registry+https://github.com/rust-lang/crates.io-index)",
+ "log",
+ "parking_lot",
+ "scheduled-thread-pool",
 ]
 
 [[package]]
 name = "rand"
 version = "0.4.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "552840b97013b1a26992c11eac34bdd778e464601a4c2054b5f0bff7c6761293"
 dependencies = [
- "fuchsia-cprng 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
- "libc 0.2.69 (registry+https://github.com/rust-lang/crates.io-index)",
- "rand_core 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
- "rdrand 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
- "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)",
+ "fuchsia-cprng",
+ "libc",
+ "rand_core 0.3.1",
+ "rdrand",
+ "winapi 0.3.8",
 ]
 
 [[package]]
 name = "rand"
 version = "0.6.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6d71dacdc3c88c1fde3885a3be3fbab9f35724e6ce99467f7d9c5026132184ca"
 dependencies = [
- "autocfg 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)",
- "libc 0.2.69 (registry+https://github.com/rust-lang/crates.io-index)",
- "rand_chacha 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
- "rand_core 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)",
- "rand_hc 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
- "rand_isaac 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
- "rand_jitter 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)",
- "rand_os 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)",
- "rand_pcg 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
- "rand_xorshift 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
- "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)",
+ "autocfg 0.1.7",
+ "libc",
+ "rand_chacha 0.1.1",
+ "rand_core 0.4.2",
+ "rand_hc 0.1.0",
+ "rand_isaac",
+ "rand_jitter",
+ "rand_os",
+ "rand_pcg",
+ "rand_xorshift",
+ "winapi 0.3.8",
 ]
 
 [[package]]
 name = "rand"
 version = "0.7.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03"
 dependencies = [
- "getrandom 0.1.14 (registry+https://github.com/rust-lang/crates.io-index)",
- "libc 0.2.69 (registry+https://github.com/rust-lang/crates.io-index)",
- "rand_chacha 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
- "rand_core 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)",
- "rand_hc 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "getrandom",
+ "libc",
+ "rand_chacha 0.2.2",
+ "rand_core 0.5.1",
+ "rand_hc 0.2.0",
 ]
 
 [[package]]
 name = "rand_chacha"
 version = "0.1.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "556d3a1ca6600bfcbab7c7c91ccb085ac7fbbcd70e008a98742e7847f4f7bcef"
 dependencies = [
- "autocfg 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)",
- "rand_core 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
+ "autocfg 0.1.7",
+ "rand_core 0.3.1",
 ]
 
 [[package]]
 name = "rand_chacha"
 version = "0.2.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402"
 dependencies = [
- "ppv-lite86 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)",
- "rand_core 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)",
+ "ppv-lite86",
+ "rand_core 0.5.1",
 ]
 
 [[package]]
 name = "rand_core"
 version = "0.3.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7a6fdeb83b075e8266dcc8762c22776f6877a63111121f5f8c7411e5be7eed4b"
 dependencies = [
- "rand_core 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)",
+ "rand_core 0.4.2",
 ]
 
 [[package]]
 name = "rand_core"
 version = "0.4.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9c33a3c44ca05fa6f1807d8e6743f3824e8509beca625669633be0acbdf509dc"
 
 [[package]]
 name = "rand_core"
 version = "0.5.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19"
 dependencies = [
- "getrandom 0.1.14 (registry+https://github.com/rust-lang/crates.io-index)",
+ "getrandom",
 ]
 
 [[package]]
 name = "rand_hc"
 version = "0.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7b40677c7be09ae76218dc623efbf7b18e34bced3f38883af07bb75630a21bc4"
 dependencies = [
- "rand_core 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
+ "rand_core 0.3.1",
 ]
 
 [[package]]
 name = "rand_hc"
 version = "0.2.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c"
 dependencies = [
- "rand_core 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)",
+ "rand_core 0.5.1",
 ]
 
 [[package]]
 name = "rand_isaac"
 version = "0.1.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ded997c9d5f13925be2a6fd7e66bf1872597f759fd9dd93513dd7e92e5a5ee08"
 dependencies = [
- "rand_core 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
+ "rand_core 0.3.1",
 ]
 
 [[package]]
 name = "rand_jitter"
 version = "0.1.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1166d5c91dc97b88d1decc3285bb0a99ed84b05cfd0bc2341bdf2d43fc41e39b"
 dependencies = [
- "libc 0.2.69 (registry+https://github.com/rust-lang/crates.io-index)",
- "rand_core 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)",
- "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)",
+ "libc",
+ "rand_core 0.4.2",
+ "winapi 0.3.8",
 ]
 
 [[package]]
 name = "rand_os"
 version = "0.1.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7b75f676a1e053fc562eafbb47838d67c84801e38fc1ba459e8f180deabd5071"
 dependencies = [
- "cloudabi 0.0.3 (registry+https://github.com/rust-lang/crates.io-index)",
- "fuchsia-cprng 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
- "libc 0.2.69 (registry+https://github.com/rust-lang/crates.io-index)",
- "rand_core 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)",
- "rdrand 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
- "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)",
+ "cloudabi",
+ "fuchsia-cprng",
+ "libc",
+ "rand_core 0.4.2",
+ "rdrand",
+ "winapi 0.3.8",
 ]
 
 [[package]]
 name = "rand_pcg"
 version = "0.1.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "abf9b09b01790cfe0364f52bf32995ea3c39f4d2dd011eac241d2914146d0b44"
 dependencies = [
- "autocfg 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)",
- "rand_core 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)",
+ "autocfg 0.1.7",
+ "rand_core 0.4.2",
 ]
 
 [[package]]
 name = "rand_xorshift"
 version = "0.1.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cbf7e9e623549b0e21f6e97cf8ecf247c1a8fd2e8a992ae265314300b2455d5c"
 dependencies = [
- "rand_core 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
+ "rand_core 0.3.1",
 ]
 
 [[package]]
 name = "rdrand"
 version = "0.4.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "678054eb77286b51581ba43620cc911abf02758c91f93f479767aed0f90458b2"
 dependencies = [
- "rand_core 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
+ "rand_core 0.3.1",
 ]
 
 [[package]]
 name = "redox_syscall"
 version = "0.1.56"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2439c63f3f6139d1b57529d16bc3b8bb855230c8efcc5d3a896c8bea7c3b1e84"
 
 [[package]]
 name = "regex"
-version = "1.3.7"
+version = "1.3.9"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9c3780fcf44b193bc4d09f36d2a3c87b251da4a046c87795a0d35f4f927ad8e6"
 dependencies = [
- "aho-corasick 0.7.10 (registry+https://github.com/rust-lang/crates.io-index)",
- "memchr 2.3.3 (registry+https://github.com/rust-lang/crates.io-index)",
- "regex-syntax 0.6.17 (registry+https://github.com/rust-lang/crates.io-index)",
- "thread_local 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)",
+ "aho-corasick",
+ "memchr",
+ "regex-syntax",
+ "thread_local",
 ]
 
 [[package]]
 name = "regex-syntax"
-version = "0.6.17"
+version = "0.6.18"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "26412eb97c6b088a6997e05f69403a802a92d520de2f8e63c2b65f9e0f47c4e8"
 
 [[package]]
 name = "remove_dir_all"
-version = "0.5.2"
+version = "0.5.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7"
 dependencies = [
- "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)",
+ "winapi 0.3.8",
 ]
 
 [[package]]
 name = "resolv-conf"
 version = "0.6.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "11834e137f3b14e309437a8276714eed3a80d1ef894869e510f2c0c0b98b9f4a"
 dependencies = [
- "hostname 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
- "quick-error 1.2.3 (registry+https://github.com/rust-lang/crates.io-index)",
+ "hostname 0.3.1",
+ "quick-error",
 ]
 
 [[package]]
 name = "ring"
-version = "0.16.12"
+version = "0.16.15"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "952cd6b98c85bbc30efa1ba5783b8abf12fec8b3287ffa52605b9432313e34e4"
 dependencies = [
- "cc 1.0.50 (registry+https://github.com/rust-lang/crates.io-index)",
- "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
- "libc 0.2.69 (registry+https://github.com/rust-lang/crates.io-index)",
- "spin 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)",
- "untrusted 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)",
- "web-sys 0.3.37 (registry+https://github.com/rust-lang/crates.io-index)",
- "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)",
+ "cc",
+ "libc",
+ "once_cell",
+ "spin",
+ "untrusted",
+ "web-sys",
+ "winapi 0.3.8",
 ]
 
 [[package]]
 name = "rss"
 version = "1.9.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "99979205510c60f80a119dedbabd0b8426517384edf205322f8bcd51796bcef9"
 dependencies = [
- "derive_builder 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)",
- "quick-xml 0.17.2 (registry+https://github.com/rust-lang/crates.io-index)",
+ "derive_builder",
+ "quick-xml",
 ]
 
 [[package]]
 name = "rustc-demangle"
 version = "0.1.16"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4c691c0e608126e00913e33f0ccf3727d5fc84573623b8d65b2df340b5201783"
 
 [[package]]
 name = "rustc_version"
 version = "0.2.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "138e3e0acb6c9fb258b19b67cb8abd63c00679d2851805ea151465464fe9030a"
 dependencies = [
- "semver 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "semver",
 ]
 
 [[package]]
 name = "rustls"
 version = "0.17.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c0d4a31f5d68413404705d6982529b0e11a9aacd4839d1d6222ee3b8cb4015e1"
 dependencies = [
- "base64 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)",
- "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)",
- "ring 0.16.12 (registry+https://github.com/rust-lang/crates.io-index)",
- "sct 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)",
- "webpki 0.21.2 (registry+https://github.com/rust-lang/crates.io-index)",
+ "base64 0.11.0",
+ "log",
+ "ring",
+ "sct",
+ "webpki",
 ]
 
 [[package]]
 name = "ryu"
-version = "1.0.3"
+version = "1.0.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e"
 
 [[package]]
 name = "safemem"
 version = "0.3.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ef703b7cb59335eae2eb93ceb664c0eb7ea6bf567079d843e09420219668e072"
 
 [[package]]
 name = "schannel"
 version = "0.1.19"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f05ba609c234e60bee0d547fe94a4c7e9da733d1c962cf6e59efa4cd9c8bc75"
 dependencies = [
- "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
- "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)",
+ "lazy_static",
+ "winapi 0.3.8",
 ]
 
 [[package]]
 name = "scheduled-thread-pool"
 version = "0.2.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0988d7fdf88d5e5fcf5923a0f1e8ab345f3e98ab4bc6bc45a2d5ff7f7458fbf6"
 dependencies = [
- "parking_lot 0.10.2 (registry+https://github.com/rust-lang/crates.io-index)",
+ "parking_lot",
 ]
 
+[[package]]
+name = "scoped-tls"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "332ffa32bf586782a3efaeb58f127980944bbc8c4d6913a86107ac2a5ab24b28"
+
 [[package]]
 name = "scopeguard"
 version = "1.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
 
 [[package]]
 name = "sct"
 version = "0.6.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e3042af939fca8c3453b7af0f1c66e533a15a86169e39de2657310ade8f98d3c"
 dependencies = [
- "ring 0.16.12 (registry+https://github.com/rust-lang/crates.io-index)",
- "untrusted 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "ring",
+ "untrusted",
 ]
 
 [[package]]
 name = "security-framework"
 version = "0.4.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "64808902d7d99f78eaddd2b4e2509713babc3dc3c85ad6f4c447680f3c01e535"
 dependencies = [
- "bitflags 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
- "core-foundation 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)",
- "core-foundation-sys 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)",
- "libc 0.2.69 (registry+https://github.com/rust-lang/crates.io-index)",
- "security-framework-sys 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)",
+ "bitflags",
+ "core-foundation",
+ "core-foundation-sys",
+ "libc",
+ "security-framework-sys",
 ]
 
 [[package]]
 name = "security-framework-sys"
 version = "0.4.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "17bf11d99252f512695eb468de5516e5cf75455521e69dfe343f3b74e4748405"
 dependencies = [
- "core-foundation-sys 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)",
- "libc 0.2.69 (registry+https://github.com/rust-lang/crates.io-index)",
+ "core-foundation-sys",
+ "libc",
 ]
 
 [[package]]
 name = "semver"
 version = "0.9.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403"
 dependencies = [
- "semver-parser 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "semver-parser",
 ]
 
 [[package]]
 name = "semver-parser"
 version = "0.7.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3"
 
 [[package]]
 name = "serde"
 version = "0.8.23"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9dad3f759919b92c3068c696c15c3d17238234498bbdcc80f2c469606f948ac8"
 
 [[package]]
 name = "serde"
-version = "1.0.106"
+version = "1.0.114"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5317f7588f0a5078ee60ef675ef96735a1442132dc645eb1d12c018620ed8cd3"
 dependencies = [
- "serde_derive 1.0.106 (registry+https://github.com/rust-lang/crates.io-index)",
+ "serde_derive",
 ]
 
 [[package]]
 name = "serde-hjson"
 version = "0.9.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6a3a4e0ea8a88553209f6cc6cfe8724ecad22e1acf372793c27d995290fe74f8"
 dependencies = [
- "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
- "linked-hash-map 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
- "num-traits 0.1.43 (registry+https://github.com/rust-lang/crates.io-index)",
- "regex 1.3.7 (registry+https://github.com/rust-lang/crates.io-index)",
- "serde 0.8.23 (registry+https://github.com/rust-lang/crates.io-index)",
+ "lazy_static",
+ "linked-hash-map 0.3.0",
+ "num-traits 0.1.43",
+ "regex",
+ "serde 0.8.23",
 ]
 
 [[package]]
 name = "serde_derive"
-version = "1.0.106"
+version = "1.0.114"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2a0be94b04690fbaed37cddffc5c134bf537c8e3329d53e982fe04c374978f8e"
 dependencies = [
- "proc-macro2 1.0.10 (registry+https://github.com/rust-lang/crates.io-index)",
- "quote 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)",
- "syn 1.0.17 (registry+https://github.com/rust-lang/crates.io-index)",
+ "proc-macro2",
+ "quote",
+ "syn",
 ]
 
 [[package]]
 name = "serde_json"
-version = "1.0.52"
+version = "1.0.55"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ec2c5d7e739bc07a3e73381a39d61fdb5f671c60c1df26a130690665803d8226"
 dependencies = [
- "itoa 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)",
- "ryu 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)",
- "serde 1.0.106 (registry+https://github.com/rust-lang/crates.io-index)",
+ "indexmap",
+ "itoa",
+ "ryu",
+ "serde 1.0.114",
 ]
 
 [[package]]
 name = "serde_test"
 version = "0.8.23"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "110b3dbdf8607ec493c22d5d947753282f3bae73c0f56d322af1e8c78e4c23d5"
 dependencies = [
- "serde 0.8.23 (registry+https://github.com/rust-lang/crates.io-index)",
+ "serde 0.8.23",
 ]
 
 [[package]]
 name = "serde_urlencoded"
 version = "0.6.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9ec5d77e2d4c73717816afac02670d5c4f534ea95ed430442cad02e7a6e32c97"
 dependencies = [
- "dtoa 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)",
- "itoa 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)",
- "serde 1.0.106 (registry+https://github.com/rust-lang/crates.io-index)",
- "url 2.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
+ "dtoa",
+ "itoa",
+ "serde 1.0.114",
+ "url",
 ]
 
 [[package]]
 name = "sha-1"
 version = "0.8.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f7d94d0bede923b3cea61f3f1ff57ff8cdfd77b400fb8f9998949e0cf04163df"
 dependencies = [
- "block-buffer 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)",
- "digest 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)",
- "fake-simd 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
- "opaque-debug 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)",
+ "block-buffer",
+ "digest",
+ "fake-simd",
+ "opaque-debug",
 ]
 
 [[package]]
 name = "sha1"
 version = "0.6.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2579985fda508104f7587689507983eadd6a6e84dd35d6d115361f530916fa0d"
 
 [[package]]
 name = "sha2"
-version = "0.8.1"
+version = "0.8.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a256f46ea78a0c0d9ff00077504903ac881a1dafdc20da66545699e7776b3e69"
 dependencies = [
- "block-buffer 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)",
- "digest 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)",
- "fake-simd 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
- "opaque-debug 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)",
+ "block-buffer",
+ "digest",
+ "fake-simd",
+ "opaque-debug",
 ]
 
 [[package]]
 name = "signal-hook-registry"
 version = "1.2.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "94f478ede9f64724c5d173d7bb56099ec3e2d9fc2774aac65d34b8b890405f41"
 dependencies = [
- "arc-swap 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)",
- "libc 0.2.69 (registry+https://github.com/rust-lang/crates.io-index)",
+ "arc-swap",
+ "libc",
 ]
 
 [[package]]
 name = "simple_asn1"
 version = "0.4.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2b25ecba7165254f0c97d6c22a64b1122a03634b18d20a34daf21e18f892e618"
 dependencies = [
- "chrono 0.4.11 (registry+https://github.com/rust-lang/crates.io-index)",
- "num-bigint 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)",
- "num-traits 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)",
+ "chrono",
+ "num-bigint",
+ "num-traits 0.2.12",
 ]
 
 [[package]]
 name = "slab"
 version = "0.4.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c111b5bd5695e56cffe5129854aa230b39c93a305372fdbb2668ca2394eea9f8"
 
 [[package]]
 name = "smallvec"
-version = "1.3.0"
+version = "1.4.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c7cb5678e1615754284ec264d9bb5b4c27d2018577fd90ac0ceb578591ed5ee4"
 
 [[package]]
 name = "socket2"
 version = "0.3.12"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "03088793f677dce356f3ccc2edb1b314ad191ab702a5de3faf49304f7e104918"
 dependencies = [
- "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)",
- "libc 0.2.69 (registry+https://github.com/rust-lang/crates.io-index)",
- "redox_syscall 0.1.56 (registry+https://github.com/rust-lang/crates.io-index)",
- "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)",
+ "cfg-if",
+ "libc",
+ "redox_syscall",
+ "winapi 0.3.8",
 ]
 
 [[package]]
 name = "spin"
 version = "0.5.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d"
 
 [[package]]
 name = "static_assertions"
-version = "0.3.4"
+version = "1.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
 
 [[package]]
 name = "strsim"
 version = "0.8.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a"
 
 [[package]]
 name = "strsim"
 version = "0.9.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6446ced80d6c486436db5c078dde11a9f73d42b57fb273121e160b84f63d894c"
 
 [[package]]
 name = "strum"
 version = "0.18.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "57bd81eb48f4c437cadc685403cad539345bf703d78e63707418431cecd4522b"
 
 [[package]]
 name = "strum_macros"
 version = "0.18.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "87c85aa3f8ea653bfd3ddf25f7ee357ee4d204731f6aa9ad04002306f6e2774c"
 dependencies = [
- "heck 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
- "proc-macro2 1.0.10 (registry+https://github.com/rust-lang/crates.io-index)",
- "quote 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)",
- "syn 1.0.17 (registry+https://github.com/rust-lang/crates.io-index)",
+ "heck",
+ "proc-macro2",
+ "quote",
+ "syn",
 ]
 
 [[package]]
 name = "syn"
-version = "1.0.17"
+version = "1.0.33"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e8d5d96e8cbb005d6959f119f773bfaebb5684296108fb32600c00cde305b2cd"
 dependencies = [
- "proc-macro2 1.0.10 (registry+https://github.com/rust-lang/crates.io-index)",
- "quote 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)",
- "unicode-xid 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "proc-macro2",
+ "quote",
+ "unicode-xid",
 ]
 
 [[package]]
 name = "synstructure"
-version = "0.12.3"
+version = "0.12.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b834f2d66f734cb897113e34aaff2f1ab4719ca946f9a7358dba8f8064148701"
 dependencies = [
- "proc-macro2 1.0.10 (registry+https://github.com/rust-lang/crates.io-index)",
- "quote 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)",
- "syn 1.0.17 (registry+https://github.com/rust-lang/crates.io-index)",
- "unicode-xid 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "proc-macro2",
+ "quote",
+ "syn",
+ "unicode-xid",
 ]
 
 [[package]]
 name = "tempfile"
 version = "3.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7a6e24d9338a0a5be79593e2fa15a648add6138caa803e2d5bc782c371732ca9"
 dependencies = [
- "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)",
- "libc 0.2.69 (registry+https://github.com/rust-lang/crates.io-index)",
- "rand 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)",
- "redox_syscall 0.1.56 (registry+https://github.com/rust-lang/crates.io-index)",
- "remove_dir_all 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)",
- "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)",
+ "cfg-if",
+ "libc",
+ "rand 0.7.3",
+ "redox_syscall",
+ "remove_dir_all",
+ "winapi 0.3.8",
 ]
 
 [[package]]
 name = "termcolor"
 version = "1.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bb6bfa289a4d7c5766392812c0a1f4c1ba45afa1ad47803c11e1f407d846d75f"
 dependencies = [
- "winapi-util 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)",
+ "winapi-util",
 ]
 
 [[package]]
 name = "textwrap"
 version = "0.11.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060"
 dependencies = [
- "unicode-width 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)",
+ "unicode-width",
 ]
 
 [[package]]
 name = "thiserror"
-version = "1.0.19"
+version = "1.0.20"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7dfdd070ccd8ccb78f4ad66bf1982dc37f620ef696c6b5028fe2ed83dd3d0d08"
 dependencies = [
- "thiserror-impl 1.0.19 (registry+https://github.com/rust-lang/crates.io-index)",
+ "thiserror-impl",
 ]
 
 [[package]]
 name = "thiserror-impl"
-version = "1.0.19"
+version = "1.0.20"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bd80fc12f73063ac132ac92aceea36734f04a1d93c1240c6944e23a3b8841793"
 dependencies = [
- "proc-macro2 1.0.10 (registry+https://github.com/rust-lang/crates.io-index)",
- "quote 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)",
- "syn 1.0.17 (registry+https://github.com/rust-lang/crates.io-index)",
+ "proc-macro2",
+ "quote",
+ "syn",
 ]
 
 [[package]]
 name = "thread_local"
 version = "1.0.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d40c6d1b69745a6ec6fb1ca717914848da4b44ae29d9b3080cbee91d72a69b14"
 dependencies = [
- "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "lazy_static",
 ]
 
 [[package]]
 name = "threadpool"
-version = "1.7.1"
+version = "1.8.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d050e60b33d41c19108b32cea32164033a9013fe3b46cbd4457559bfbf77afaa"
 dependencies = [
- "num_cpus 1.13.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "num_cpus",
 ]
 
 [[package]]
 name = "time"
-version = "0.1.42"
+version = "0.1.43"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ca8a50ef2360fbd1eeb0ecd46795a87a19024eb4b53c5dc916ca1fd95fe62438"
 dependencies = [
- "libc 0.2.69 (registry+https://github.com/rust-lang/crates.io-index)",
- "redox_syscall 0.1.56 (registry+https://github.com/rust-lang/crates.io-index)",
- "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)",
+ "libc",
+ "winapi 0.3.8",
 ]
 
+[[package]]
+name = "tinyvec"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "53953d2d3a5ad81d9f844a32f14ebb121f50b650cd59d0ee2a07cf13c617efed"
+
 [[package]]
 name = "tokio"
-version = "0.2.20"
+version = "0.2.21"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d099fa27b9702bed751524694adbe393e18b36b204da91eb1cbbbbb4a5ee2d58"
 dependencies = [
- "bytes 0.5.4 (registry+https://github.com/rust-lang/crates.io-index)",
- "fnv 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)",
- "futures-core 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)",
- "iovec 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)",
- "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
- "libc 0.2.69 (registry+https://github.com/rust-lang/crates.io-index)",
- "memchr 2.3.3 (registry+https://github.com/rust-lang/crates.io-index)",
- "mio 0.6.21 (registry+https://github.com/rust-lang/crates.io-index)",
- "mio-uds 0.6.7 (registry+https://github.com/rust-lang/crates.io-index)",
- "pin-project-lite 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)",
- "signal-hook-registry 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
- "slab 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)",
- "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)",
+ "bytes",
+ "fnv",
+ "futures-core",
+ "iovec",
+ "lazy_static",
+ "libc",
+ "memchr",
+ "mio",
+ "mio-uds",
+ "pin-project-lite",
+ "signal-hook-registry",
+ "slab",
+ "winapi 0.3.8",
 ]
 
 [[package]]
 name = "tokio-util"
 version = "0.2.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "571da51182ec208780505a32528fc5512a8fe1443ab960b3f2f3ef093cd16930"
 dependencies = [
- "bytes 0.5.4 (registry+https://github.com/rust-lang/crates.io-index)",
- "futures-core 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)",
- "futures-sink 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)",
- "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)",
- "pin-project-lite 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)",
- "tokio 0.2.20 (registry+https://github.com/rust-lang/crates.io-index)",
+ "bytes",
+ "futures-core",
+ "futures-sink",
+ "log",
+ "pin-project-lite",
+ "tokio",
 ]
 
 [[package]]
 name = "tokio-util"
 version = "0.3.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "be8242891f2b6cbef26a2d7e8605133c2c554cd35b3e4948ea892d6d68436499"
 dependencies = [
- "bytes 0.5.4 (registry+https://github.com/rust-lang/crates.io-index)",
- "futures-core 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)",
- "futures-sink 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)",
- "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)",
- "pin-project-lite 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)",
- "tokio 0.2.20 (registry+https://github.com/rust-lang/crates.io-index)",
+ "bytes",
+ "futures-core",
+ "futures-sink",
+ "log",
+ "pin-project-lite",
+ "tokio",
 ]
 
 [[package]]
 name = "trust-dns-proto"
 version = "0.18.0-alpha.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2a7f3a2ab8a919f5eca52a468866a67ed7d3efa265d48a652a9a3452272b413f"
 dependencies = [
- "async-trait 0.1.30 (registry+https://github.com/rust-lang/crates.io-index)",
- "enum-as-inner 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)",
- "failure 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)",
- "futures 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)",
- "idna 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
- "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
- "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)",
- "rand 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)",
- "smallvec 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
- "socket2 0.3.12 (registry+https://github.com/rust-lang/crates.io-index)",
- "tokio 0.2.20 (registry+https://github.com/rust-lang/crates.io-index)",
- "url 2.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
+ "async-trait",
+ "enum-as-inner",
+ "failure",
+ "futures",
+ "idna",
+ "lazy_static",
+ "log",
+ "rand 0.7.3",
+ "smallvec",
+ "socket2",
+ "tokio",
+ "url",
 ]
 
 [[package]]
 name = "trust-dns-resolver"
 version = "0.18.0-alpha.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6f90b1502b226f8b2514c6d5b37bafa8c200d7ca4102d57dc36ee0f3b7a04a2f"
 dependencies = [
- "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)",
- "failure 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)",
- "futures 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)",
- "ipconfig 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
- "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
- "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)",
- "lru-cache 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
- "resolv-conf 0.6.3 (registry+https://github.com/rust-lang/crates.io-index)",
- "smallvec 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
- "tokio 0.2.20 (registry+https://github.com/rust-lang/crates.io-index)",
- "trust-dns-proto 0.18.0-alpha.2 (registry+https://github.com/rust-lang/crates.io-index)",
+ "cfg-if",
+ "failure",
+ "futures",
+ "ipconfig",
+ "lazy_static",
+ "log",
+ "lru-cache",
+ "resolv-conf",
+ "smallvec",
+ "tokio",
+ "trust-dns-proto",
 ]
 
 [[package]]
 name = "twoway"
 version = "0.2.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6b40075910de3a912adbd80b5d8bad6ad10a23eeb1f5bf9d4006839e899ba5bc"
 dependencies = [
- "memchr 2.3.3 (registry+https://github.com/rust-lang/crates.io-index)",
- "unchecked-index 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
+ "memchr",
+ "unchecked-index",
 ]
 
 [[package]]
 name = "typed-arena"
 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"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "373c8a200f9e67a0c95e62a4f52fbf80c23b4381c05a17845531982fa99e6b33"
 
 [[package]]
 name = "ucd-trie"
 version = "0.1.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "56dee185309b50d1f11bfedef0fe6d036842e3fb77413abef29f8f8d1c5d4c1c"
 
 [[package]]
 name = "unchecked-index"
 version = "0.2.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eeba86d422ce181a719445e51872fa30f1f7413b62becb52e95ec91aa262d85c"
 
 [[package]]
 name = "unicase"
 version = "2.6.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "50f37be617794602aabbeee0be4f259dc1778fabe05e2d67ee8f79326d5cb4f6"
 dependencies = [
- "version_check 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)",
+ "version_check 0.9.2",
 ]
 
 [[package]]
 name = "unicode-bidi"
 version = "0.3.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "49f2bd0c6468a8230e1db229cff8029217cf623c767ea5d60bfbd42729ea54d5"
 dependencies = [
- "matches 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)",
+ "matches",
 ]
 
 [[package]]
 name = "unicode-normalization"
-version = "0.1.12"
+version = "0.1.13"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6fb19cf769fa8c6a80a162df694621ebeb4dafb606470b2b2fce0be40a98a977"
 dependencies = [
- "smallvec 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "tinyvec",
 ]
 
 [[package]]
 name = "unicode-segmentation"
 version = "1.6.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e83e153d1053cbb5a118eeff7fd5be06ed99153f00dbcd8ae310c5fb2b22edc0"
 
 [[package]]
 name = "unicode-width"
 version = "0.1.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "caaa9d531767d1ff2150b9332433f32a24622147e5ebb1f26409d5da67afd479"
 
 [[package]]
 name = "unicode-xid"
 version = "0.2.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "826e7639553986605ec5979c7dd957c7895e93eabed50ab2ffa7f6128a75097c"
 
 [[package]]
 name = "unicode_categories"
 version = "0.1.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e"
 
 [[package]]
 name = "untrusted"
-version = "0.7.0"
+version = "0.7.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a"
 
 [[package]]
 name = "url"
 version = "2.1.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "829d4a8476c35c9bf0bbce5a3b23f4106f79728039b726d292bb93bc106787cb"
 dependencies = [
- "idna 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
- "matches 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)",
- "percent-encoding 2.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "idna",
+ "matches",
+ "percent-encoding",
+ "serde 1.0.114",
 ]
 
 [[package]]
 name = "uuid"
 version = "0.7.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "90dbc611eb48397705a6b0f6e917da23ae517e4d127123d2cf7674206627d32a"
+dependencies = [
+ "rand 0.6.5",
+]
+
+[[package]]
+name = "uuid"
+version = "0.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9fde2f6a4bea1d6e007c4ad38c6839fa71cbb63b6dbf5b595aa38dc9b1093c11"
 dependencies = [
- "rand 0.6.5 (registry+https://github.com/rust-lang/crates.io-index)",
+ "rand 0.7.3",
+ "serde 1.0.114",
 ]
 
 [[package]]
 name = "v_escape"
 version = "0.7.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "660b101c07b5d0863deb9e7fb3138777e858d6d2a79f9e6049a27d1cc77c6da6"
 dependencies = [
- "v_escape_derive 0.5.6 (registry+https://github.com/rust-lang/crates.io-index)",
+ "v_escape_derive",
 ]
 
 [[package]]
 name = "v_escape_derive"
 version = "0.5.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c2ca2a14bc3fc5b64d188b087a7d3a927df87b152e941ccfbc66672e20c467ae"
 dependencies = [
- "nom 4.2.3 (registry+https://github.com/rust-lang/crates.io-index)",
- "proc-macro2 1.0.10 (registry+https://github.com/rust-lang/crates.io-index)",
- "quote 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)",
- "syn 1.0.17 (registry+https://github.com/rust-lang/crates.io-index)",
+ "nom 4.2.3",
+ "proc-macro2",
+ "quote",
+ "syn",
 ]
 
 [[package]]
 name = "v_htmlescape"
 version = "0.4.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e33e939c0d8cf047514fb6ba7d5aac78bc56677a6938b2ee67000b91f2e97e41"
 dependencies = [
- "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)",
- "v_escape 0.7.4 (registry+https://github.com/rust-lang/crates.io-index)",
+ "cfg-if",
+ "v_escape",
 ]
 
 [[package]]
 name = "vcpkg"
-version = "0.2.8"
+version = "0.2.10"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6454029bf181f092ad1b853286f23e2c507d8e8194d01d92da4a55c274a5508c"
 
 [[package]]
 name = "vec_map"
-version = "0.8.1"
+version = "0.8.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191"
 
 [[package]]
 name = "version_check"
 version = "0.1.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "914b1a6776c4c929a602fafd8bc742e06365d4bcbe48c30f9cca5824f70dc9dd"
 
 [[package]]
 name = "version_check"
-version = "0.9.1"
+version = "0.9.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b5a972e5669d67ba988ce3dc826706fb0a8b01471c088cb0b6110b805cc36aed"
 
 [[package]]
 name = "wasi"
 version = "0.9.0+wasi-snapshot-preview1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519"
 
 [[package]]
 name = "wasm-bindgen"
-version = "0.2.60"
+version = "0.2.63"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4c2dc4aa152834bc334f506c1a06b866416a8b6697d5c9f75b9a689c8486def0"
 dependencies = [
- "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)",
- "wasm-bindgen-macro 0.2.60 (registry+https://github.com/rust-lang/crates.io-index)",
+ "cfg-if",
+ "wasm-bindgen-macro",
 ]
 
 [[package]]
 name = "wasm-bindgen-backend"
-version = "0.2.60"
+version = "0.2.63"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ded84f06e0ed21499f6184df0e0cb3494727b0c5da89534e0fcc55c51d812101"
 dependencies = [
- "bumpalo 3.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
- "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
- "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)",
- "proc-macro2 1.0.10 (registry+https://github.com/rust-lang/crates.io-index)",
- "quote 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)",
- "syn 1.0.17 (registry+https://github.com/rust-lang/crates.io-index)",
- "wasm-bindgen-shared 0.2.60 (registry+https://github.com/rust-lang/crates.io-index)",
+ "bumpalo",
+ "lazy_static",
+ "log",
+ "proc-macro2",
+ "quote",
+ "syn",
+ "wasm-bindgen-shared",
 ]
 
 [[package]]
 name = "wasm-bindgen-macro"
-version = "0.2.60"
+version = "0.2.63"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "838e423688dac18d73e31edce74ddfac468e37b1506ad163ffaf0a46f703ffe3"
 dependencies = [
- "quote 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)",
- "wasm-bindgen-macro-support 0.2.60 (registry+https://github.com/rust-lang/crates.io-index)",
+ "quote",
+ "wasm-bindgen-macro-support",
 ]
 
 [[package]]
 name = "wasm-bindgen-macro-support"
-version = "0.2.60"
+version = "0.2.63"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3156052d8ec77142051a533cdd686cba889537b213f948cd1d20869926e68e92"
 dependencies = [
- "proc-macro2 1.0.10 (registry+https://github.com/rust-lang/crates.io-index)",
- "quote 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)",
- "syn 1.0.17 (registry+https://github.com/rust-lang/crates.io-index)",
- "wasm-bindgen-backend 0.2.60 (registry+https://github.com/rust-lang/crates.io-index)",
- "wasm-bindgen-shared 0.2.60 (registry+https://github.com/rust-lang/crates.io-index)",
+ "proc-macro2",
+ "quote",
+ "syn",
+ "wasm-bindgen-backend",
+ "wasm-bindgen-shared",
 ]
 
 [[package]]
 name = "wasm-bindgen-shared"
-version = "0.2.60"
+version = "0.2.63"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c9ba19973a58daf4db6f352eda73dc0e289493cd29fb2632eb172085b6521acd"
 
 [[package]]
 name = "web-sys"
-version = "0.3.37"
+version = "0.3.40"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7b72fe77fd39e4bd3eaa4412fd299a0be6b3dfe9d2597e2f1c20beb968f41d17"
 dependencies = [
- "js-sys 0.3.37 (registry+https://github.com/rust-lang/crates.io-index)",
- "wasm-bindgen 0.2.60 (registry+https://github.com/rust-lang/crates.io-index)",
+ "js-sys",
+ "wasm-bindgen",
 ]
 
 [[package]]
 name = "webpki"
-version = "0.21.2"
+version = "0.21.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ab146130f5f790d45f82aeeb09e55a256573373ec64409fc19a6fb82fb1032ae"
 dependencies = [
- "ring 0.16.12 (registry+https://github.com/rust-lang/crates.io-index)",
- "untrusted 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "ring",
+ "untrusted",
 ]
 
 [[package]]
 name = "webpki-roots"
 version = "0.19.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f8eff4b7516a57307f9349c64bf34caa34b940b66fed4b2fb3136cb7386e5739"
 dependencies = [
- "webpki 0.21.2 (registry+https://github.com/rust-lang/crates.io-index)",
+ "webpki",
 ]
 
 [[package]]
 name = "widestring"
-version = "0.4.0"
+version = "0.4.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a763e303c0e0f23b0da40888724762e802a8ffefbc22de4127ef42493c2ea68c"
 
 [[package]]
 name = "winapi"
 version = "0.2.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "167dc9d6949a9b857f3451275e911c3f44255842c1f7a76f33c55103a909087a"
 
 [[package]]
 name = "winapi"
 version = "0.3.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8093091eeb260906a183e6ae1abdba2ef5ef2257a21801128899c3fc699229c6"
 dependencies = [
- "winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
- "winapi-x86_64-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "winapi-i686-pc-windows-gnu",
+ "winapi-x86_64-pc-windows-gnu",
 ]
 
 [[package]]
 name = "winapi-build"
 version = "0.1.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2d315eee3b34aca4797b2da6b13ed88266e6d612562a0c46390af8299fc699bc"
 
 [[package]]
 name = "winapi-i686-pc-windows-gnu"
 version = "0.4.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
 
 [[package]]
 name = "winapi-util"
-version = "0.1.4"
+version = "0.1.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178"
 dependencies = [
- "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)",
+ "winapi 0.3.8",
 ]
 
 [[package]]
 name = "winapi-x86_64-pc-windows-gnu"
 version = "0.4.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
 
 [[package]]
 name = "winreg"
 version = "0.6.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b2986deb581c4fe11b621998a5e53361efe6b48a151178d0cd9eeffa4dc6acc9"
 dependencies = [
- "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)",
+ "winapi 0.3.8",
 ]
 
 [[package]]
 name = "winutil"
 version = "0.1.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7daf138b6b14196e3830a588acf1e86966c694d3e8fb026fb105b8b5dca07e6e"
 dependencies = [
- "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)",
+ "winapi 0.3.8",
 ]
 
 [[package]]
 name = "ws2_32-sys"
 version = "0.2.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d59cefebd0c892fa2dd6de581e937301d8552cb44489cdff035c6187cb63fa5e"
 dependencies = [
- "winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)",
- "winapi-build 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
-]
-
-[metadata]
-"checksum activitypub 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d538a21b137ec0f63cc579ef4afa4ab13aa85b4f8af15a033683edd97c50718d"
-"checksum activitystreams-derive 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "65608fdeae5eb05485d5b71a3d2242d76b2b7413608c196d47eb4dff3eed7b85"
-"checksum activitystreams-traits 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "a0c2a3958d240f40eff1f31b5f679a6e0d4ce2a16812886a3ec0164f3a2ca517"
-"checksum activitystreams-types 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "0598820663a59e5eaafeeedd3a7f7efc93db4ed6172905baec05503095ba5c0e"
-"checksum actix 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)" = "a4af87564ff659dee8f9981540cac9418c45e910c8072fdedd643a262a38fcaf"
-"checksum actix-codec 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "09e55f0a5c2ca15795035d90c46bd0e73a5123b72f68f12596d6ba5282051380"
-"checksum actix-connect 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "c95cc9569221e9802bf4c377f6c18b90ef10227d787611decf79fd47d2a8e76c"
-"checksum actix-files 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "301482841d3d74483a446ead63cb7d362e187d2c8b603f13d91995621ea53c46"
-"checksum actix-http 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "c16664cc4fdea8030837ad5a845eb231fb93fc3c5c171edfefb52fad92ce9019"
-"checksum actix-macros 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "21705adc76bbe4bc98434890e73a89cd00c6015e5704a60bb6eea6c3b72316b6"
-"checksum actix-router 0.2.4 (registry+https://github.com/rust-lang/crates.io-index)" = "9d7a10ca4d94e8c8e7a87c5173aba1b97ba9a6563ca02b0e1cd23531093d3ec8"
-"checksum actix-rt 1.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "143fcc2912e0d1de2bcf4e2f720d2a60c28652ab4179685a1ee159e0fb3db227"
-"checksum actix-server 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "582a7173c281a4f46b5aa168a11e7f37183dcb71177a39312cc2264da7a632c9"
-"checksum actix-service 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)" = "d3e4fc95dfa7e24171b2d0bb46b85f8ab0e8499e4e3caec691fc4ea65c287564"
-"checksum actix-testing 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "48494745b72d0ea8ff0cf874aaf9b622a3ee03d7081ee0c04edea4f26d32c911"
-"checksum actix-threadpool 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "cf4082192601de5f303013709ff84d81ca6a1bc4af7fb24f367a500a23c6e84e"
-"checksum actix-tls 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "a4e5b4faaf105e9a6d389c606c298dcdb033061b00d532af9df56ff3a54995a8"
-"checksum actix-utils 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)" = "fcf8f5631bf01adec2267808f00e228b761c60c0584cc9fa0b5364f41d147f4e"
-"checksum actix-web 2.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "3158e822461040822f0dbf1735b9c2ce1f95f93b651d7a7aded00b1efbb1f635"
-"checksum actix-web-actors 2.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "dc1bd41bd66c4e9b5274cec87aac30168e63d64e96fd19db38edef6b46ba2982"
-"checksum actix-web-codegen 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "4f00371942083469785f7e28c540164af1913ee7c96a4534acb9cea92c39f057"
-"checksum actix_derive 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b95aceadaf327f18f0df5962fedc1bde2f870566a0b9f65c89508a3b1f79334c"
-"checksum adler32 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)" = "5d2e7343e7fc9de883d1b0341e0b13970f764c14101234857d2ddafa1cb1cac2"
-"checksum aho-corasick 0.7.10 (registry+https://github.com/rust-lang/crates.io-index)" = "8716408b8bc624ed7f65d223ddb9ac2d044c0547b6fa4b0d554f3a9540496ada"
-"checksum ansi_term 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b"
-"checksum arc-swap 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)" = "d663a8e9a99154b5fb793032533f6328da35e23aac63d5c152279aa8ba356825"
-"checksum arrayvec 0.4.12 (registry+https://github.com/rust-lang/crates.io-index)" = "cd9fd44efafa8690358b7408d253adf110036b88f55672a933f01d616ad9b1b9"
-"checksum ascii_utils 0.9.3 (registry+https://github.com/rust-lang/crates.io-index)" = "71938f30533e4d95a6d17aa530939da3842c2ab6f4f84b9dae68447e4129f74a"
-"checksum async-trait 0.1.30 (registry+https://github.com/rust-lang/crates.io-index)" = "da71fef07bc806586090247e971229289f64c210a278ee5ae419314eb386b31d"
-"checksum attohttpc 0.14.0 (registry+https://github.com/rust-lang/crates.io-index)" = "93610ce1c035e8a273fe56a19852e42798f3c019ca2726e52d2971197f116525"
-"checksum atty 0.2.14 (registry+https://github.com/rust-lang/crates.io-index)" = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8"
-"checksum autocfg 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)" = "1d49d90015b3c36167a20fe2810c5cd875ad504b39cff3d4eae7977e6b7c1cb2"
-"checksum autocfg 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "f8aac770f1885fd7e387acedd76065302551364496e46b3dd00860b2f8359b9d"
-"checksum awc 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "d7601d4d1d7ef2335d6597a41b5fe069f6ab799b85f53565ab390e7b7065aac5"
-"checksum backtrace 0.3.46 (registry+https://github.com/rust-lang/crates.io-index)" = "b1e692897359247cc6bb902933361652380af0f1b7651ae5c5013407f30e109e"
-"checksum backtrace-sys 0.1.35 (registry+https://github.com/rust-lang/crates.io-index)" = "7de8aba10a69c8e8d7622c5710229485ec32e9d55fdad160ea559c086fdcd118"
-"checksum base64 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)" = "0b25d992356d2eb0ed82172f5248873db5560c4721f564b13cb5193bda5e668e"
-"checksum base64 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b41b7ea54a0c9d92199de89e20e58d49f02f8e699814ef3fdf266f6f748d15c7"
-"checksum base64 0.12.0 (registry+https://github.com/rust-lang/crates.io-index)" = "7d5ca2cd0adc3f48f9e9ea5a6bbdf9ccc0bfade884847e484d452414c7ccffb3"
-"checksum base64 0.9.3 (registry+https://github.com/rust-lang/crates.io-index)" = "489d6c0ed21b11d038c31b6ceccca973e65d73ba3bd8ecb9a2babf5546164643"
-"checksum bcrypt 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "6f02d7d008a57bcb2251ba115b803934e02315edbde9a861c88713493e381b63"
-"checksum bitflags 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693"
-"checksum block-buffer 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)" = "c0940dc441f31689269e10ac70eb1002a3a1d3ad1390e030043662eb7fe4688b"
-"checksum block-cipher-trait 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)" = "1c924d49bd09e7c06003acda26cd9742e796e34282ec6c1189404dee0c1f4774"
-"checksum block-padding 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "fa79dedbb091f449f1f39e53edf88d5dbe95f895dae6135a8d7b881fb5af73f5"
-"checksum blowfish 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "6aeb80d00f2688459b8542068abd974cfb101e7a82182414a99b5026c0d85cc3"
-"checksum brotli-sys 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)" = "4445dea95f4c2b41cde57cc9fee236ae4dbae88d8fcbdb4750fc1bb5d86aaecd"
-"checksum brotli2 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)" = "0cb036c3eade309815c15ddbacec5b22c4d1f3983a774ab2eac2e3e9ea85568e"
-"checksum bufstream 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "40e38929add23cdf8a366df9b0e088953150724bcbe5fc330b0d8eb3b328eec8"
-"checksum bumpalo 3.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "12ae9db68ad7fac5fe51304d20f016c911539251075a214f8e663babefa35187"
-"checksum byte-tools 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "e3b5ca7a04898ad4bcd41c90c5285445ff5b791899bb1b0abdd2a2aa791211d7"
-"checksum byteorder 1.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "08c48aae112d48ed9f069b33538ea9e3e90aa263cfa3d1c24309612b1f7472de"
-"checksum bytes 0.5.4 (registry+https://github.com/rust-lang/crates.io-index)" = "130aac562c0dd69c56b3b1cc8ffd2e17be31d0b6c25b61c96b76231aa23e39e1"
-"checksum bytestring 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "fc7c05fa5172da78a62d9949d662d2ac89d4cc7355d7b49adee5163f1fb3f363"
-"checksum cc 1.0.50 (registry+https://github.com/rust-lang/crates.io-index)" = "95e28fa049fda1c330bcf9d723be7663a899c4679724b34c81e9f5a326aab8cd"
-"checksum cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)" = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822"
-"checksum chrono 0.4.11 (registry+https://github.com/rust-lang/crates.io-index)" = "80094f509cf8b5ae86a4966a39b3ff66cd7e2a3e594accec3743ff3fabeab5b2"
-"checksum clap 2.33.0 (registry+https://github.com/rust-lang/crates.io-index)" = "5067f5bb2d80ef5d68b4c87db81601f0b75bca627bc2ef76b141d7b846a3c6d9"
-"checksum cloudabi 0.0.3 (registry+https://github.com/rust-lang/crates.io-index)" = "ddfc5b9aa5d4507acaf872de71051dfd0e309860e88966e1051e462a077aac4f"
-"checksum comrak 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e17058cc536cf290563e88787d7b9e6030ce4742943017cc2ffb71f88034021c"
-"checksum config 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)" = "19b076e143e1d9538dde65da30f8481c2a6c44040edb8e02b9bf1351edb92ce3"
-"checksum copyless 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "6ff9c56c9fb2a49c05ef0e431485a22400af20d33226dc0764d891d09e724127"
-"checksum core-foundation 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "57d24c7a13c43e870e37c1556b74555437870a04514f7685f5b354e090567171"
-"checksum core-foundation-sys 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b3a71ab494c0b5b860bdc8407ae08978052417070c2ced38573a9157ad75b8ac"
-"checksum crc32fast 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ba125de2af0df55319f41944744ad91c71113bf74a4646efff39afe1f6842db1"
-"checksum crossbeam-channel 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "cced8691919c02aac3cb0a1bc2e9b73d89e832bf9a06fc579d4e71b68a2da061"
-"checksum crossbeam-utils 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)" = "c3c7c73a2d1e9fc0886a08b93e98eb643461230d5f1925e4036204d5f2e261a8"
-"checksum darling 0.10.2 (registry+https://github.com/rust-lang/crates.io-index)" = "0d706e75d87e35569db781a9b5e2416cff1236a47ed380831f959382ccd5f858"
-"checksum darling_core 0.10.2 (registry+https://github.com/rust-lang/crates.io-index)" = "f0c960ae2da4de88a91b2d920c2a7233b400bc33cb28453a2987822d8392519b"
-"checksum darling_macro 0.10.2 (registry+https://github.com/rust-lang/crates.io-index)" = "d9b5a2f4ac4969822c62224815d069952656cadc7084fdca9751e6d959189b72"
-"checksum derive_builder 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)" = "a2658621297f2cf68762a6f7dc0bb7e1ff2cfd6583daef8ee0fed6f7ec468ec0"
-"checksum derive_builder_core 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)" = "2791ea3e372c8495c0bc2033991d76b512cd799d07491fbd6890124db9458bef"
-"checksum derive_more 0.99.5 (registry+https://github.com/rust-lang/crates.io-index)" = "e2323f3f47db9a0e77ce7a300605d8d2098597fc451ed1a97bb1f6411bb550a7"
-"checksum diesel 1.4.4 (registry+https://github.com/rust-lang/crates.io-index)" = "33d7ca63eb2efea87a7f56a283acc49e2ce4b2bd54adf7465dc1d81fef13d8fc"
-"checksum diesel_derives 1.4.1 (registry+https://github.com/rust-lang/crates.io-index)" = "45f5098f628d02a7a0f68ddba586fb61e80edec3bdc1be3b921f4ceec60858d3"
-"checksum diesel_migrations 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "bf3cde8413353dc7f5d72fa8ce0b99a560a359d2c5ef1e5817ca731cd9008f4c"
-"checksum digest 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)" = "f3d0c8c8752312f9713efd397ff63acb9f85585afbf179282e720e7704954dd5"
-"checksum dotenv 0.15.0 (registry+https://github.com/rust-lang/crates.io-index)" = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f"
-"checksum dtoa 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)" = "4358a9e11b9a09cf52383b451b49a169e8d797b68aa02301ff586d70d9661ea3"
-"checksum either 1.5.3 (registry+https://github.com/rust-lang/crates.io-index)" = "bb1f6b1ce1c140482ea30ddd3335fc0024ac7ee112895426e0a629a6c20adfe3"
-"checksum email 0.0.20 (registry+https://github.com/rust-lang/crates.io-index)" = "91549a51bb0241165f13d57fc4c72cef063b4088fb078b019ecbf464a45f22e4"
-"checksum encoding 0.2.33 (registry+https://github.com/rust-lang/crates.io-index)" = "6b0d943856b990d12d3b55b359144ff341533e516d94098b1d3fc1ac666d36ec"
-"checksum encoding-index-japanese 1.20141219.5 (registry+https://github.com/rust-lang/crates.io-index)" = "04e8b2ff42e9a05335dbf8b5c6f7567e5591d0d916ccef4e0b1710d32a0d0c91"
-"checksum encoding-index-korean 1.20141219.5 (registry+https://github.com/rust-lang/crates.io-index)" = "4dc33fb8e6bcba213fe2f14275f0963fd16f0a02c878e3095ecfdf5bee529d81"
-"checksum encoding-index-simpchinese 1.20141219.5 (registry+https://github.com/rust-lang/crates.io-index)" = "d87a7194909b9118fc707194baa434a4e3b0fb6a5a757c73c3adb07aa25031f7"
-"checksum encoding-index-singlebyte 1.20141219.5 (registry+https://github.com/rust-lang/crates.io-index)" = "3351d5acffb224af9ca265f435b859c7c01537c0849754d3db3fdf2bfe2ae84a"
-"checksum encoding-index-tradchinese 1.20141219.5 (registry+https://github.com/rust-lang/crates.io-index)" = "fd0e20d5688ce3cab59eb3ef3a2083a5c77bf496cb798dc6fcdb75f323890c18"
-"checksum encoding_index_tests 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "a246d82be1c9d791c5dfde9a2bd045fc3cbba3fa2b11ad558f27d01712f00569"
-"checksum encoding_rs 0.8.22 (registry+https://github.com/rust-lang/crates.io-index)" = "cd8d03faa7fe0c1431609dfad7bbe827af30f82e1e2ae6f7ee4fca6bd764bc28"
-"checksum entities 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "b5320ae4c3782150d900b79807611a59a99fc9a1d61d686faafc24b93fc8d7ca"
-"checksum enum-as-inner 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)" = "bc4bfcfacb61d231109d1d55202c1f33263319668b168843e02ad4652725ec9c"
-"checksum env_logger 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)" = "44533bbbb3bb3c1fa17d9f2e4e38bbbaf8396ba82193c4cb1b6445d711445d36"
-"checksum failure 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)" = "d32e9bd16cc02eae7db7ef620b392808b89f6a5e16bb3497d159c6b92a0f4f86"
-"checksum failure_derive 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)" = "030a733c8287d6213886dd487564ff5c8f6aae10278b3588ed177f9d18f8d231"
-"checksum fake-simd 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "e88a8acf291dafb59c2d96e8f59828f3838bb1a70398823ade51a84de6a6deed"
-"checksum fast_chemail 0.9.6 (registry+https://github.com/rust-lang/crates.io-index)" = "495a39d30d624c2caabe6312bfead73e7717692b44e0b32df168c275a2e8e9e4"
-"checksum flate2 1.0.14 (registry+https://github.com/rust-lang/crates.io-index)" = "2cfff41391129e0a856d6d822600b8d71179d46879e310417eb9c762eb178b42"
-"checksum fnv 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)" = "2fad85553e09a6f881f739c29f0b00b0f01357c743266d478b68951ce23285f3"
-"checksum foreign-types 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)" = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
-"checksum foreign-types-shared 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
-"checksum fuchsia-cprng 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba"
-"checksum fuchsia-zircon 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "2e9763c69ebaae630ba35f74888db465e49e259ba1bc0eda7d06f4a067615d82"
-"checksum fuchsia-zircon-sys 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "3dcaa9ae7725d12cdb85b3ad99a434db70b468c09ded17e012d86b5c1010f7a7"
-"checksum futures 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "5c329ae8753502fb44ae4fc2b622fa2a94652c41e795143765ba0927f92ab780"
-"checksum futures-channel 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "f0c77d04ce8edd9cb903932b608268b3fffec4163dc053b3b402bf47eac1f1a8"
-"checksum futures-core 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "f25592f769825e89b92358db00d26f965761e094951ac44d3663ef25b7ac464a"
-"checksum futures-executor 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "f674f3e1bcb15b37284a90cedf55afdba482ab061c407a9c0ebbd0f3109741ba"
-"checksum futures-io 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "a638959aa96152c7a4cddf50fcb1e3fede0583b27157c26e67d6f99904090dc6"
-"checksum futures-macro 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "9a5081aa3de1f7542a794a397cde100ed903b0630152d0973479018fd85423a7"
-"checksum futures-sink 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "3466821b4bc114d95b087b850a724c6f83115e929bc88f1fa98a3304a944c8a6"
-"checksum futures-task 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "7b0a34e53cf6cdcd0178aa573aed466b646eb3db769570841fda0c7ede375a27"
-"checksum futures-util 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "22766cf25d64306bedf0384da004d05c9974ab104fcc4528f1236181c18004c5"
-"checksum fxhash 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c"
-"checksum generic-array 0.12.3 (registry+https://github.com/rust-lang/crates.io-index)" = "c68f0274ae0e023facc3c97b2e00f076be70e254bc851d972503b328db79b2ec"
-"checksum getrandom 0.1.14 (registry+https://github.com/rust-lang/crates.io-index)" = "7abc8dd8451921606d809ba32e95b6111925cd2906060d2dcc29c070220503eb"
-"checksum h2 0.2.4 (registry+https://github.com/rust-lang/crates.io-index)" = "377038bf3c89d18d6ca1431e7a5027194fbd724ca10592b9487ede5e8e144f42"
-"checksum heck 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "20564e78d53d2bb135c343b3f47714a56af2061f1c928fdb541dc7b9fdd94205"
-"checksum hermit-abi 0.1.11 (registry+https://github.com/rust-lang/crates.io-index)" = "8a0d737e0f947a1864e93d33fdef4af8445a00d1ed8dc0c8ddb73139ea6abf15"
-"checksum hostname 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "21ceb46a83a85e824ef93669c8b390009623863b5c195d1ba747292c0c72f94e"
-"checksum hostname 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "3c731c3e10504cc8ed35cfe2f1db4c9274c3d35fa486e3b31df46f068ef3e867"
-"checksum htmlescape 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "e9025058dae765dee5070ec375f591e2ba14638c63feff74f13805a72e523163"
-"checksum http 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "28d569972648b2c512421b5f2a405ad6ac9666547189d0c5477a3f200f3e02f9"
-"checksum httparse 1.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "cd179ae861f0c2e53da70d892f5f3029f9594be0c41dc5269cd371691b1dc2f9"
-"checksum humantime 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "df004cfca50ef23c36850aaaa59ad52cc70d0e90243c3c7737a4dd32dc7a3c4f"
-"checksum ident_case 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
-"checksum idna 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "02e2673c30ee86b5b96a9cb52ad15718aa1f966f5ab9ad54a8b95d5ca33120a9"
-"checksum indexmap 1.3.2 (registry+https://github.com/rust-lang/crates.io-index)" = "076f042c5b7b98f31d205f1249267e12a6518c1481e9dae9764af19b707d2292"
-"checksum iovec 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "b2b3ea6ff95e175473f8ffe6a7eb7c00d054240321b84c57051175fe3c1e075e"
-"checksum ipconfig 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "aa79fa216fbe60834a9c0737d7fcd30425b32d1c58854663e24d4c4b328ed83f"
-"checksum itoa 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)" = "b8b7a7c0c47db5545ed3fef7468ee7bb5b74691498139e4b3f6a20685dc6dd8e"
-"checksum js-sys 0.3.37 (registry+https://github.com/rust-lang/crates.io-index)" = "6a27d435371a2fa5b6d2b028a74bbdb1234f308da363226a2854ca3ff8ba7055"
-"checksum jsonwebtoken 7.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d11f9e80a85927748a334df8e4f6782a04033517bb28f3863a563daad882da7f"
-"checksum kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7507624b29483431c0ba2d82aece8ca6cdba9382bff4ddd0f7490560c056098d"
-"checksum language-tags 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "a91d884b6667cd606bb5a69aa0c99ba811a115fc68915e7056ec08a46e93199a"
-"checksum lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
-"checksum lettre 0.9.3 (registry+https://github.com/rust-lang/crates.io-index)" = "bf43f3202a879fbdab4ecafec3349b0139f81d31c626246d53bcbb546253ffaa"
-"checksum lettre_email 0.9.4 (registry+https://github.com/rust-lang/crates.io-index)" = "fd02480f8dcf48798e62113974d6ccca2129a51d241fa20f1ea349c8a42559d5"
-"checksum lexical-core 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)" = "d7043aa5c05dd34fb73b47acb8c3708eac428de4545ea3682ed2f11293ebd890"
-"checksum libc 0.2.69 (registry+https://github.com/rust-lang/crates.io-index)" = "99e85c08494b21a9054e7fe1374a732aeadaff3980b6990b94bfd3a70f690005"
-"checksum linked-hash-map 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "6d262045c5b87c0861b3f004610afd0e2c851e2908d08b6c870cbb9d5f494ecd"
-"checksum linked-hash-map 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)" = "ae91b68aebc4ddb91978b11a1b02ddd8602a05ec19002801c5666000e05e0f83"
-"checksum lock_api 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "c4da24a77a3d8a6d4862d95f72e6fdb9c09a643ecdb402d754004a557f2bec75"
-"checksum log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)" = "14b6052be84e6b71ab17edffc2eeabf5c2c3ae1fdb464aae35ac50c67a44e1f7"
-"checksum lru-cache 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "31e24f1ad8321ca0e8a1e0ac13f23cb668e6f5466c2c57319f6a5cf1cc8e3b1c"
-"checksum maplit 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d"
-"checksum match_cfg 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ffbee8634e0d45d258acb448e7eaab3fce7a0a467395d4d9f228e3c1f01fb2e4"
-"checksum matches 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)" = "7ffc5c5338469d4d3ea17d269fa8ea3512ad247247c30bd2df69e68309ed0a08"
-"checksum maybe-uninit 2.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "60302e4db3a61da70c0cb7991976248362f30319e88850c487b9b95bbf059e00"
-"checksum memchr 2.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "3728d817d99e5ac407411fa471ff9800a778d88a24685968b36824eaf4bee400"
-"checksum migrations_internals 1.4.1 (registry+https://github.com/rust-lang/crates.io-index)" = "2b4fc84e4af020b837029e017966f86a1c2d5e83e64b589963d5047525995860"
-"checksum migrations_macros 1.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "9753f12909fd8d923f75ae5c3258cae1ed3c8ec052e1b38c93c21a6d157f789c"
-"checksum mime 0.3.16 (registry+https://github.com/rust-lang/crates.io-index)" = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d"
-"checksum mime_guess 2.0.3 (registry+https://github.com/rust-lang/crates.io-index)" = "2684d4c2e97d99848d30b324b00c8fcc7e5c897b7cbb5819b09e7c90e8baf212"
-"checksum miniz_oxide 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)" = "aa679ff6578b1cddee93d7e82e263b94a575e0bfced07284eb0c037c1d2416a5"
-"checksum mio 0.6.21 (registry+https://github.com/rust-lang/crates.io-index)" = "302dec22bcf6bae6dfb69c647187f4b4d0fb6f535521f7bc022430ce8e12008f"
-"checksum mio-uds 0.6.7 (registry+https://github.com/rust-lang/crates.io-index)" = "966257a94e196b11bb43aca423754d87429960a768de9414f3691d6957abf125"
-"checksum miow 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "8c1f2f3b1cf331de6896aabf6e9d55dca90356cc9960cca7eaaf408a355ae919"
-"checksum native-tls 0.2.4 (registry+https://github.com/rust-lang/crates.io-index)" = "2b0d88c06fe90d5ee94048ba40409ef1d9315d86f6f38c2efdaad4fb50c58b2d"
-"checksum net2 0.2.33 (registry+https://github.com/rust-lang/crates.io-index)" = "42550d9fb7b6684a6d404d9fa7250c2eb2646df731d1c06afc06dcee9e1bcf88"
-"checksum nodrop 0.1.14 (registry+https://github.com/rust-lang/crates.io-index)" = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb"
-"checksum nom 4.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "2ad2a91a8e869eeb30b9cb3119ae87773a8f4ae617f41b1eb9c154b2905f7bd6"
-"checksum nom 5.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "0b471253da97532da4b61552249c521e01e736071f71c1a4f7ebbfbf0a06aad6"
-"checksum num-bigint 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)" = "090c7f9998ee0ff65aa5b723e4009f7b217707f1fb5ea551329cc4d6231fb304"
-"checksum num-integer 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)" = "3f6ea62e9d81a77cd3ee9a2a5b9b609447857f3d358704331e4ef39eb247fcba"
-"checksum num-traits 0.1.43 (registry+https://github.com/rust-lang/crates.io-index)" = "92e5113e9fd4cc14ded8e499429f396a20f98c772a47cc8622a736e1ec843c31"
-"checksum num-traits 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)" = "c62be47e61d1842b9170f0fdeec8eba98e60e90e5446449a0545e5152acd7096"
-"checksum num_cpus 1.13.0 (registry+https://github.com/rust-lang/crates.io-index)" = "05499f3756671c15885fee9034446956fff3f243d6077b91e5767df161f766b3"
-"checksum opaque-debug 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "2839e79665f131bdb5782e51f2c6c9599c133c6098982a54c794358bf432529c"
-"checksum openssl 0.10.29 (registry+https://github.com/rust-lang/crates.io-index)" = "cee6d85f4cb4c4f59a6a85d5b68a233d280c82e29e822913b9c8b129fbf20bdd"
-"checksum openssl-probe 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "77af24da69f9d9341038eba93a073b1fdaaa1b788221b00a69bce9e762cb32de"
-"checksum openssl-sys 0.9.58 (registry+https://github.com/rust-lang/crates.io-index)" = "a842db4709b604f0fe5d1170ae3565899be2ad3d9cbc72dedc789ac0511f78de"
-"checksum parking_lot 0.10.2 (registry+https://github.com/rust-lang/crates.io-index)" = "d3a704eb390aafdc107b0e392f56a82b668e3a71366993b5340f5833fd62505e"
-"checksum parking_lot_core 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)" = "0e136c1904604defe99ce5fd71a28d473fa60a12255d511aa78a9ddf11237aeb"
-"checksum pem 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "a1581760c757a756a41f0ee3ff01256227bdf64cb752839779b95ffb01c59793"
-"checksum percent-encoding 2.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e"
-"checksum pest 2.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "10f4872ae94d7b90ae48754df22fd42ad52ce740b8f370b03da4835417403e53"
-"checksum pest_derive 2.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "833d1ae558dc601e9a60366421196a8d94bc0ac980476d0b67e1d0988d72b2d0"
-"checksum pest_generator 2.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "99b8db626e31e5b81787b9783425769681b347011cc59471e33ea46d2ea0cf55"
-"checksum pest_meta 2.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "54be6e404f5317079812fc8f9f5279de376d8856929e21c184ecf6bbd692a11d"
-"checksum pin-project 0.4.9 (registry+https://github.com/rust-lang/crates.io-index)" = "6f6a7f5eee6292c559c793430c55c00aea9d3b3d1905e855806ca4d7253426a2"
-"checksum pin-project-internal 0.4.9 (registry+https://github.com/rust-lang/crates.io-index)" = "8988430ce790d8682672117bc06dda364c0be32d3abd738234f19f3240bad99a"
-"checksum pin-project-lite 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "237844750cfbb86f67afe27eee600dfbbcb6188d734139b534cbfbf4f96792ae"
-"checksum pin-utils 0.1.0-alpha.4 (registry+https://github.com/rust-lang/crates.io-index)" = "5894c618ce612a3fa23881b152b608bafb8c56cfc22f434a3ba3120b40f7b587"
-"checksum pkg-config 0.3.17 (registry+https://github.com/rust-lang/crates.io-index)" = "05da548ad6865900e60eaba7f589cc0783590a92e940c26953ff81ddbab2d677"
-"checksum ppv-lite86 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)" = "74490b50b9fbe561ac330df47c08f3f33073d2d00c150f719147d7c54522fa1b"
-"checksum pq-sys 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)" = "6ac25eee5a0582f45a67e837e350d784e7003bd29a5f460796772061ca49ffda"
-"checksum proc-macro-hack 0.5.15 (registry+https://github.com/rust-lang/crates.io-index)" = "0d659fe7c6d27f25e9d80a1a094c223f5246f6a6596453e09d7229bf42750b63"
-"checksum proc-macro-nested 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "8e946095f9d3ed29ec38de908c22f95d9ac008e424c7bcae54c75a79c527c694"
-"checksum proc-macro2 1.0.10 (registry+https://github.com/rust-lang/crates.io-index)" = "df246d292ff63439fea9bc8c0a270bed0e390d5ebd4db4ba15aba81111b5abe3"
-"checksum quick-error 1.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0"
-"checksum quick-xml 0.17.2 (registry+https://github.com/rust-lang/crates.io-index)" = "fe1e430bdcf30c9fdc25053b9c459bb1a4672af4617b6c783d7d91dc17c6bbb0"
-"checksum quote 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)" = "2bdc6c187c65bca4260c9011c9e3132efe4909da44726bad24cf7572ae338d7f"
-"checksum r2d2 0.8.8 (registry+https://github.com/rust-lang/crates.io-index)" = "1497e40855348e4a8a40767d8e55174bce1e445a3ac9254ad44ad468ee0485af"
-"checksum rand 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)" = "552840b97013b1a26992c11eac34bdd778e464601a4c2054b5f0bff7c6761293"
-"checksum rand 0.6.5 (registry+https://github.com/rust-lang/crates.io-index)" = "6d71dacdc3c88c1fde3885a3be3fbab9f35724e6ce99467f7d9c5026132184ca"
-"checksum rand 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)" = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03"
-"checksum rand_chacha 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "556d3a1ca6600bfcbab7c7c91ccb085ac7fbbcd70e008a98742e7847f4f7bcef"
-"checksum rand_chacha 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402"
-"checksum rand_core 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "7a6fdeb83b075e8266dcc8762c22776f6877a63111121f5f8c7411e5be7eed4b"
-"checksum rand_core 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "9c33a3c44ca05fa6f1807d8e6743f3824e8509beca625669633be0acbdf509dc"
-"checksum rand_core 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19"
-"checksum rand_hc 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "7b40677c7be09ae76218dc623efbf7b18e34bced3f38883af07bb75630a21bc4"
-"checksum rand_hc 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c"
-"checksum rand_isaac 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "ded997c9d5f13925be2a6fd7e66bf1872597f759fd9dd93513dd7e92e5a5ee08"
-"checksum rand_jitter 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "1166d5c91dc97b88d1decc3285bb0a99ed84b05cfd0bc2341bdf2d43fc41e39b"
-"checksum rand_os 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "7b75f676a1e053fc562eafbb47838d67c84801e38fc1ba459e8f180deabd5071"
-"checksum rand_pcg 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "abf9b09b01790cfe0364f52bf32995ea3c39f4d2dd011eac241d2914146d0b44"
-"checksum rand_xorshift 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "cbf7e9e623549b0e21f6e97cf8ecf247c1a8fd2e8a992ae265314300b2455d5c"
-"checksum rdrand 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "678054eb77286b51581ba43620cc911abf02758c91f93f479767aed0f90458b2"
-"checksum redox_syscall 0.1.56 (registry+https://github.com/rust-lang/crates.io-index)" = "2439c63f3f6139d1b57529d16bc3b8bb855230c8efcc5d3a896c8bea7c3b1e84"
-"checksum regex 1.3.7 (registry+https://github.com/rust-lang/crates.io-index)" = "a6020f034922e3194c711b82a627453881bc4682166cabb07134a10c26ba7692"
-"checksum regex-syntax 0.6.17 (registry+https://github.com/rust-lang/crates.io-index)" = "7fe5bd57d1d7414c6b5ed48563a2c855d995ff777729dcd91c369ec7fea395ae"
-"checksum remove_dir_all 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)" = "4a83fa3702a688b9359eccba92d153ac33fd2e8462f9e0e3fdf155239ea7792e"
-"checksum resolv-conf 0.6.3 (registry+https://github.com/rust-lang/crates.io-index)" = "11834e137f3b14e309437a8276714eed3a80d1ef894869e510f2c0c0b98b9f4a"
-"checksum ring 0.16.12 (registry+https://github.com/rust-lang/crates.io-index)" = "1ba5a8ec64ee89a76c98c549af81ff14813df09c3e6dc4766c3856da48597a0c"
-"checksum rss 1.9.0 (registry+https://github.com/rust-lang/crates.io-index)" = "99979205510c60f80a119dedbabd0b8426517384edf205322f8bcd51796bcef9"
-"checksum rustc-demangle 0.1.16 (registry+https://github.com/rust-lang/crates.io-index)" = "4c691c0e608126e00913e33f0ccf3727d5fc84573623b8d65b2df340b5201783"
-"checksum rustc_version 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "138e3e0acb6c9fb258b19b67cb8abd63c00679d2851805ea151465464fe9030a"
-"checksum rustls 0.17.0 (registry+https://github.com/rust-lang/crates.io-index)" = "c0d4a31f5d68413404705d6982529b0e11a9aacd4839d1d6222ee3b8cb4015e1"
-"checksum ryu 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)" = "535622e6be132bccd223f4bb2b8ac8d53cda3c7a6394944d3b2b33fb974f9d76"
-"checksum safemem 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "ef703b7cb59335eae2eb93ceb664c0eb7ea6bf567079d843e09420219668e072"
-"checksum schannel 0.1.19 (registry+https://github.com/rust-lang/crates.io-index)" = "8f05ba609c234e60bee0d547fe94a4c7e9da733d1c962cf6e59efa4cd9c8bc75"
-"checksum scheduled-thread-pool 0.2.4 (registry+https://github.com/rust-lang/crates.io-index)" = "0988d7fdf88d5e5fcf5923a0f1e8ab345f3e98ab4bc6bc45a2d5ff7f7458fbf6"
-"checksum scopeguard 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
-"checksum sct 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e3042af939fca8c3453b7af0f1c66e533a15a86169e39de2657310ade8f98d3c"
-"checksum security-framework 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)" = "64808902d7d99f78eaddd2b4e2509713babc3dc3c85ad6f4c447680f3c01e535"
-"checksum security-framework-sys 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)" = "17bf11d99252f512695eb468de5516e5cf75455521e69dfe343f3b74e4748405"
-"checksum semver 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)" = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403"
-"checksum semver-parser 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3"
-"checksum serde 0.8.23 (registry+https://github.com/rust-lang/crates.io-index)" = "9dad3f759919b92c3068c696c15c3d17238234498bbdcc80f2c469606f948ac8"
-"checksum serde 1.0.106 (registry+https://github.com/rust-lang/crates.io-index)" = "36df6ac6412072f67cf767ebbde4133a5b2e88e76dc6187fa7104cd16f783399"
-"checksum serde-hjson 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)" = "6a3a4e0ea8a88553209f6cc6cfe8724ecad22e1acf372793c27d995290fe74f8"
-"checksum serde_derive 1.0.106 (registry+https://github.com/rust-lang/crates.io-index)" = "9e549e3abf4fb8621bd1609f11dfc9f5e50320802273b12f3811a67e6716ea6c"
-"checksum serde_json 1.0.52 (registry+https://github.com/rust-lang/crates.io-index)" = "a7894c8ed05b7a3a279aeb79025fdec1d3158080b75b98a08faf2806bb799edd"
-"checksum serde_test 0.8.23 (registry+https://github.com/rust-lang/crates.io-index)" = "110b3dbdf8607ec493c22d5d947753282f3bae73c0f56d322af1e8c78e4c23d5"
-"checksum serde_urlencoded 0.6.1 (registry+https://github.com/rust-lang/crates.io-index)" = "9ec5d77e2d4c73717816afac02670d5c4f534ea95ed430442cad02e7a6e32c97"
-"checksum sha-1 0.8.2 (registry+https://github.com/rust-lang/crates.io-index)" = "f7d94d0bede923b3cea61f3f1ff57ff8cdfd77b400fb8f9998949e0cf04163df"
-"checksum sha1 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "2579985fda508104f7587689507983eadd6a6e84dd35d6d115361f530916fa0d"
-"checksum sha2 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)" = "27044adfd2e1f077f649f59deb9490d3941d674002f7d062870a60ebe9bd47a0"
-"checksum signal-hook-registry 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "94f478ede9f64724c5d173d7bb56099ec3e2d9fc2774aac65d34b8b890405f41"
-"checksum simple_asn1 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "2b25ecba7165254f0c97d6c22a64b1122a03634b18d20a34daf21e18f892e618"
-"checksum slab 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "c111b5bd5695e56cffe5129854aa230b39c93a305372fdbb2668ca2394eea9f8"
-"checksum smallvec 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "05720e22615919e4734f6a99ceae50d00226c3c5aca406e102ebc33298214e0a"
-"checksum socket2 0.3.12 (registry+https://github.com/rust-lang/crates.io-index)" = "03088793f677dce356f3ccc2edb1b314ad191ab702a5de3faf49304f7e104918"
-"checksum spin 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)" = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d"
-"checksum static_assertions 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "7f3eb36b47e512f8f1c9e3d10c2c1965bc992bd9cdb024fa581e2194501c83d3"
-"checksum strsim 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a"
-"checksum strsim 0.9.3 (registry+https://github.com/rust-lang/crates.io-index)" = "6446ced80d6c486436db5c078dde11a9f73d42b57fb273121e160b84f63d894c"
-"checksum strum 0.18.0 (registry+https://github.com/rust-lang/crates.io-index)" = "57bd81eb48f4c437cadc685403cad539345bf703d78e63707418431cecd4522b"
-"checksum strum_macros 0.18.0 (registry+https://github.com/rust-lang/crates.io-index)" = "87c85aa3f8ea653bfd3ddf25f7ee357ee4d204731f6aa9ad04002306f6e2774c"
-"checksum syn 1.0.17 (registry+https://github.com/rust-lang/crates.io-index)" = "0df0eb663f387145cab623dea85b09c2c5b4b0aef44e945d928e682fce71bb03"
-"checksum synstructure 0.12.3 (registry+https://github.com/rust-lang/crates.io-index)" = "67656ea1dc1b41b1451851562ea232ec2e5a80242139f7e679ceccfb5d61f545"
-"checksum tempfile 3.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "7a6e24d9338a0a5be79593e2fa15a648add6138caa803e2d5bc782c371732ca9"
-"checksum termcolor 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "bb6bfa289a4d7c5766392812c0a1f4c1ba45afa1ad47803c11e1f407d846d75f"
-"checksum textwrap 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060"
-"checksum thiserror 1.0.19 (registry+https://github.com/rust-lang/crates.io-index)" = "b13f926965ad00595dd129fa12823b04bbf866e9085ab0a5f2b05b850fbfc344"
-"checksum thiserror-impl 1.0.19 (registry+https://github.com/rust-lang/crates.io-index)" = "893582086c2f98cde18f906265a65b5030a074b1046c674ae898be6519a7f479"
-"checksum thread_local 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "d40c6d1b69745a6ec6fb1ca717914848da4b44ae29d9b3080cbee91d72a69b14"
-"checksum threadpool 1.7.1 (registry+https://github.com/rust-lang/crates.io-index)" = "e2f0c90a5f3459330ac8bc0d2f879c693bb7a2f59689c1083fc4ef83834da865"
-"checksum time 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)" = "db8dcfca086c1143c9270ac42a2bbd8a7ee477b78ac8e45b19abfb0cbede4b6f"
-"checksum tokio 0.2.20 (registry+https://github.com/rust-lang/crates.io-index)" = "05c1d570eb1a36f0345a5ce9c6c6e665b70b73d11236912c0b477616aeec47b1"
-"checksum tokio-util 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "571da51182ec208780505a32528fc5512a8fe1443ab960b3f2f3ef093cd16930"
-"checksum tokio-util 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "be8242891f2b6cbef26a2d7e8605133c2c554cd35b3e4948ea892d6d68436499"
-"checksum trust-dns-proto 0.18.0-alpha.2 (registry+https://github.com/rust-lang/crates.io-index)" = "2a7f3a2ab8a919f5eca52a468866a67ed7d3efa265d48a652a9a3452272b413f"
-"checksum trust-dns-resolver 0.18.0-alpha.2 (registry+https://github.com/rust-lang/crates.io-index)" = "6f90b1502b226f8b2514c6d5b37bafa8c200d7ca4102d57dc36ee0f3b7a04a2f"
-"checksum twoway 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "6b40075910de3a912adbd80b5d8bad6ad10a23eeb1f5bf9d4006839e899ba5bc"
-"checksum typed-arena 1.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "a9b2228007eba4120145f785df0f6c92ea538f5a3635a612ecf4e334c8c1446d"
-"checksum typenum 1.12.0 (registry+https://github.com/rust-lang/crates.io-index)" = "373c8a200f9e67a0c95e62a4f52fbf80c23b4381c05a17845531982fa99e6b33"
-"checksum ucd-trie 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "56dee185309b50d1f11bfedef0fe6d036842e3fb77413abef29f8f8d1c5d4c1c"
-"checksum unchecked-index 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "eeba86d422ce181a719445e51872fa30f1f7413b62becb52e95ec91aa262d85c"
-"checksum unicase 2.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "50f37be617794602aabbeee0be4f259dc1778fabe05e2d67ee8f79326d5cb4f6"
-"checksum unicode-bidi 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "49f2bd0c6468a8230e1db229cff8029217cf623c767ea5d60bfbd42729ea54d5"
-"checksum unicode-normalization 0.1.12 (registry+https://github.com/rust-lang/crates.io-index)" = "5479532badd04e128284890390c1e876ef7a993d0570b3597ae43dfa1d59afa4"
-"checksum unicode-segmentation 1.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e83e153d1053cbb5a118eeff7fd5be06ed99153f00dbcd8ae310c5fb2b22edc0"
-"checksum unicode-width 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)" = "caaa9d531767d1ff2150b9332433f32a24622147e5ebb1f26409d5da67afd479"
-"checksum unicode-xid 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "826e7639553986605ec5979c7dd957c7895e93eabed50ab2ffa7f6128a75097c"
-"checksum unicode_categories 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e"
-"checksum untrusted 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "60369ef7a31de49bcb3f6ca728d4ba7300d9a1658f94c727d4cab8c8d9f4aece"
-"checksum url 2.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "829d4a8476c35c9bf0bbce5a3b23f4106f79728039b726d292bb93bc106787cb"
-"checksum uuid 0.7.4 (registry+https://github.com/rust-lang/crates.io-index)" = "90dbc611eb48397705a6b0f6e917da23ae517e4d127123d2cf7674206627d32a"
-"checksum v_escape 0.7.4 (registry+https://github.com/rust-lang/crates.io-index)" = "660b101c07b5d0863deb9e7fb3138777e858d6d2a79f9e6049a27d1cc77c6da6"
-"checksum v_escape_derive 0.5.6 (registry+https://github.com/rust-lang/crates.io-index)" = "c2ca2a14bc3fc5b64d188b087a7d3a927df87b152e941ccfbc66672e20c467ae"
-"checksum v_htmlescape 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)" = "e33e939c0d8cf047514fb6ba7d5aac78bc56677a6938b2ee67000b91f2e97e41"
-"checksum vcpkg 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)" = "3fc439f2794e98976c88a2a2dafce96b930fe8010b0a256b3c2199a773933168"
-"checksum vec_map 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)" = "05c78687fb1a80548ae3250346c3db86a80a7cdd77bda190189f2d0a0987c81a"
-"checksum version_check 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "914b1a6776c4c929a602fafd8bc742e06365d4bcbe48c30f9cca5824f70dc9dd"
-"checksum version_check 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)" = "078775d0255232fb988e6fccf26ddc9d1ac274299aaedcedce21c6f72cc533ce"
-"checksum wasi 0.9.0+wasi-snapshot-preview1 (registry+https://github.com/rust-lang/crates.io-index)" = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519"
-"checksum wasm-bindgen 0.2.60 (registry+https://github.com/rust-lang/crates.io-index)" = "2cc57ce05287f8376e998cbddfb4c8cb43b84a7ec55cf4551d7c00eef317a47f"
-"checksum wasm-bindgen-backend 0.2.60 (registry+https://github.com/rust-lang/crates.io-index)" = "d967d37bf6c16cca2973ca3af071d0a2523392e4a594548155d89a678f4237cd"
-"checksum wasm-bindgen-macro 0.2.60 (registry+https://github.com/rust-lang/crates.io-index)" = "8bd151b63e1ea881bb742cd20e1d6127cef28399558f3b5d415289bc41eee3a4"
-"checksum wasm-bindgen-macro-support 0.2.60 (registry+https://github.com/rust-lang/crates.io-index)" = "d68a5b36eef1be7868f668632863292e37739656a80fc4b9acec7b0bd35a4931"
-"checksum wasm-bindgen-shared 0.2.60 (registry+https://github.com/rust-lang/crates.io-index)" = "daf76fe7d25ac79748a37538b7daeed1c7a6867c92d3245c12c6222e4a20d639"
-"checksum web-sys 0.3.37 (registry+https://github.com/rust-lang/crates.io-index)" = "2d6f51648d8c56c366144378a33290049eafdd784071077f6fe37dae64c1c4cb"
-"checksum webpki 0.21.2 (registry+https://github.com/rust-lang/crates.io-index)" = "f1f50e1972865d6b1adb54167d1c8ed48606004c2c9d0ea5f1eeb34d95e863ef"
-"checksum webpki-roots 0.19.0 (registry+https://github.com/rust-lang/crates.io-index)" = "f8eff4b7516a57307f9349c64bf34caa34b940b66fed4b2fb3136cb7386e5739"
-"checksum widestring 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "effc0e4ff8085673ea7b9b2e3c73f6bd4d118810c9009ed8f1e16bd96c331db6"
-"checksum winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)" = "167dc9d6949a9b857f3451275e911c3f44255842c1f7a76f33c55103a909087a"
-"checksum winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)" = "8093091eeb260906a183e6ae1abdba2ef5ef2257a21801128899c3fc699229c6"
-"checksum winapi-build 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "2d315eee3b34aca4797b2da6b13ed88266e6d612562a0c46390af8299fc699bc"
-"checksum winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
-"checksum winapi-util 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "fa515c5163a99cc82bab70fd3bfdd36d827be85de63737b40fcef2ce084a436e"
-"checksum winapi-x86_64-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
-"checksum winreg 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)" = "b2986deb581c4fe11b621998a5e53361efe6b48a151178d0cd9eeffa4dc6acc9"
-"checksum winutil 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "7daf138b6b14196e3830a588acf1e86966c694d3e8fb026fb105b8b5dca07e6e"
-"checksum ws2_32-sys 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "d59cefebd0c892fa2dd6de581e937301d8552cb44489cdff035c6187cb63fa5e"
+ "winapi 0.2.8",
+ "winapi-build",
+]
index 43933e5318225890ad4e3c657b1232fac1e6cb12..52a9bae8e9e041a986a5faf250597931bbf1b198 100644 (file)
@@ -8,15 +8,17 @@ edition = "2018"
 lto = true
 
 [dependencies]
-diesel = { version = "1.4.2", features = ["postgres","chrono", "r2d2", "64-column-tables"] }
+diesel = { version = "1.4.4", features = ["postgres","chrono","r2d2","64-column-tables","serde_json"] }
 diesel_migrations = "1.4.0"
 dotenv = "0.15.0"
-bcrypt = "0.7.0"
-activitypub = "0.2.0"
-chrono = "0.4.7"
+activitystreams = "0.6.2"
+activitystreams-new = { git = "https://git.asonix.dog/asonix/activitystreams-sketch" }
+activitystreams-ext = { git = "https://git.asonix.dog/asonix/activitystreams-ext" }
+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_json = "1.0.52"
-serde = "1.0.105"
+serde = { version = "1.0.105", features = ["derive"] }
 actix = "0.9.0"
 actix-web = "2.0.0"
 actix-files = "0.2.1"
@@ -35,9 +37,16 @@ lettre_email = "0.9.4"
 sha2 = "0.8.1"
 rss = "1.9.0"
 htmlescape = "0.3.1"
+url = { version = "2.1.1", features = ["serde"] }
 config = {version = "0.10.1", default-features = false, features = ["hjson"] }
 percent-encoding = "2.1.0"
 attohttpc = { version = "0.14.0", default-features = false, features = ["tls-rustls"] }
 comrak = "0.7"
-tokio = "0.2.20"
-futures = "0.3.4"
+openssl = "0.10"
+http = "0.2.1"
+http-signature-normalization = "0.5.1"
+base64 = "0.12.1"
+tokio = "0.2.21"
+futures = "0.3.5"
+itertools = "0.9.0"
+uuid = { version = "0.8", features = ["serde", "v4"] }
diff --git a/server/config/config.hjson b/server/config/config.hjson
new file mode 100644 (file)
index 0000000..96eff45
--- /dev/null
@@ -0,0 +1,4 @@
+{
+  hostname: "localhost:8536"
+  federation_enabled: true
+}
\ No newline at end of file
index 97b9429c16812ba43f607a6daf720b5a16b1e721..61951321e0689e6a849a8f4cdbcd629a195a3eed 100644 (file)
@@ -26,7 +26,7 @@
     pool_size: 5
   }
   # the domain name of your instance (eg "dev.lemmy.ml")
-  hostname: "my_domain"
+  hostname: null
   # address where lemmy should listen for incoming requests
   bind: "0.0.0.0"
   # port where lemmy should listen for incoming requests
@@ -35,9 +35,6 @@
   jwt_secret: "changeme"
   # The dir for the front end
   front_end_dir: "../ui/dist"
-  # whether to enable activitypub federation. this feature is in alpha, do not enable in production, as might
-  # cause problems like remote instances fetching and permanently storing bad data.
-  federation_enabled: false
   # rate limits for various user actions, by user ip
   rate_limit: {
     # maximum number of messages created in interval
     # interval length for registration limit
     register_per_second: 3600
   }
+  # settings related to activitypub federation
+  federation: {
+    # whether to enable activitypub federation. this feature is in alpha, do not enable in production.
+    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
+    allowed_instances: ""
+  }
 #  # email sending configuration
 #  email: {
 #    # hostname of the smtp server
diff --git a/server/migrations/2020-03-26-192410_add_activitypub_tables/down.sql b/server/migrations/2020-03-26-192410_add_activitypub_tables/down.sql
new file mode 100644 (file)
index 0000000..b171062
--- /dev/null
@@ -0,0 +1,16 @@
+drop table activity;
+
+alter table user_ 
+drop column actor_id, 
+drop column private_key,
+drop column public_key,
+drop column bio,
+drop column local,
+drop column last_refreshed_at;
+
+alter table community 
+drop column actor_id, 
+drop column private_key,
+drop column public_key,
+drop column local,
+drop column last_refreshed_at;
diff --git a/server/migrations/2020-03-26-192410_add_activitypub_tables/up.sql b/server/migrations/2020-03-26-192410_add_activitypub_tables/up.sql
new file mode 100644 (file)
index 0000000..8fe3b8e
--- /dev/null
@@ -0,0 +1,36 @@
+-- The Activitypub activity table
+-- All user actions must create a row here.
+create table activity (
+  id serial primary key,
+  user_id int references user_ on update cascade on delete cascade not null, -- Ensures that the user is set up here.
+  data jsonb not null,
+  local boolean not null default true,
+  published timestamp not null default now(),
+  updated timestamp
+);
+
+-- Making sure that id is unique
+create unique index idx_activity_unique_apid on activity ((data ->> 'id'::text));
+
+-- Add federation columns to the two actor tables
+alter table user_ 
+-- TODO uniqueness constraints should be added on these 3 columns later
+add column actor_id character varying(255) not null default 'changeme', -- This needs to be checked and updated in code, building from the site url if local
+add column bio text, -- not on community, already has description
+add column local boolean not null default true,
+add column private_key text, -- These need to be generated from code
+add column public_key text,
+add column last_refreshed_at timestamp not null default now() -- Used to re-fetch federated actor periodically
+;
+
+-- Community
+alter table community 
+add column actor_id character varying(255) not null default 'changeme', -- This needs to be checked and updated in code, building from the site url if local
+add column local boolean not null default true,
+add column private_key text, -- These need to be generated from code
+add column public_key text,
+add column last_refreshed_at timestamp not null default now() -- Used to re-fetch federated actor periodically
+;
+
+-- Don't worry about rebuilding the views right now.
+
diff --git a/server/migrations/2020-04-03-194936_add_activitypub_for_posts_and_comments/down.sql b/server/migrations/2020-04-03-194936_add_activitypub_for_posts_and_comments/down.sql
new file mode 100644 (file)
index 0000000..50c95bb
--- /dev/null
@@ -0,0 +1,7 @@
+alter table post 
+drop column ap_id, 
+drop column local;
+
+alter table comment 
+drop column ap_id, 
+drop column local;
diff --git a/server/migrations/2020-04-03-194936_add_activitypub_for_posts_and_comments/up.sql b/server/migrations/2020-04-03-194936_add_activitypub_for_posts_and_comments/up.sql
new file mode 100644 (file)
index 0000000..a3fb956
--- /dev/null
@@ -0,0 +1,14 @@
+-- Add federation columns to post, comment
+
+alter table post
+-- TODO uniqueness constraints should be added on these 3 columns later
+add column ap_id character varying(255) not null default 'changeme', -- This needs to be checked and updated in code, building from the site url if local
+add column local boolean not null default true
+;
+
+alter table comment
+-- TODO uniqueness constraints should be added on these 3 columns later
+add column ap_id character varying(255) not null default 'changeme', -- This needs to be checked and updated in code, building from the site url if local
+add column local boolean not null default true
+;
+
diff --git a/server/migrations/2020-04-07-135912_add_user_community_apub_constraints/down.sql b/server/migrations/2020-04-07-135912_add_user_community_apub_constraints/down.sql
new file mode 100644 (file)
index 0000000..faf24fd
--- /dev/null
@@ -0,0 +1,36 @@
+-- User table
+drop view user_view cascade;
+
+alter table user_ 
+add column fedi_name varchar(40) not null default 'changeme';
+
+alter table user_
+add constraint user__name_fedi_name_key unique (name, fedi_name);
+
+-- Community
+alter table community
+add constraint community_name_key unique (name);
+
+
+create view user_view as 
+select 
+u.id,
+u.name,
+u.avatar,
+u.email,
+u.matrix_user_id,
+u.fedi_name,
+u.admin,
+u.banned,
+u.show_avatars,
+u.send_notifications_to_email,
+u.published,
+(select count(*) from post p where p.creator_id = u.id) as number_of_posts,
+(select coalesce(sum(score), 0) from post p, post_like pl where u.id = p.creator_id and p.id = pl.post_id) as post_score,
+(select count(*) from comment c where c.creator_id = u.id) as number_of_comments,
+(select coalesce(sum(score), 0) from comment c, comment_like cl where u.id = c.creator_id and c.id = cl.comment_id) as comment_score
+from user_ u;
+
+create materialized view user_mview as select * from user_view;
+
+create unique index idx_user_mview_id on user_mview (id);
diff --git a/server/migrations/2020-04-07-135912_add_user_community_apub_constraints/up.sql b/server/migrations/2020-04-07-135912_add_user_community_apub_constraints/up.sql
new file mode 100644 (file)
index 0000000..de65191
--- /dev/null
@@ -0,0 +1,38 @@
+-- User table
+
+-- Need to regenerate user_view, user_mview
+drop view user_view cascade;
+
+-- Remove the fedi_name constraint, drop that useless column
+alter table user_ 
+drop constraint user__name_fedi_name_key;
+
+alter table user_
+drop column fedi_name;
+
+-- Community
+alter table community
+drop constraint community_name_key;
+
+create view user_view as 
+select 
+u.id,
+u.name,
+u.avatar,
+u.email,
+u.matrix_user_id,
+u.admin,
+u.banned,
+u.show_avatars,
+u.send_notifications_to_email,
+u.published,
+(select count(*) from post p where p.creator_id = u.id) as number_of_posts,
+(select coalesce(sum(score), 0) from post p, post_like pl where u.id = p.creator_id and p.id = pl.post_id) as post_score,
+(select count(*) from comment c where c.creator_id = u.id) as number_of_comments,
+(select coalesce(sum(score), 0) from comment c, comment_like cl where u.id = c.creator_id and c.id = cl.comment_id) as comment_score
+from user_ u;
+
+create materialized view user_mview as select * from user_view;
+
+create unique index idx_user_mview_id on user_mview (id);
+
diff --git a/server/migrations/2020-04-14-163701_update_views_for_activitypub/down.sql b/server/migrations/2020-04-14-163701_update_views_for_activitypub/down.sql
new file mode 100644 (file)
index 0000000..ce2dde3
--- /dev/null
@@ -0,0 +1,440 @@
+-- user_view
+drop view user_view cascade;
+
+create view user_view as 
+select 
+u.id,
+u.name,
+u.avatar,
+u.email,
+u.matrix_user_id,
+u.admin,
+u.banned,
+u.show_avatars,
+u.send_notifications_to_email,
+u.published,
+(select count(*) from post p where p.creator_id = u.id) as number_of_posts,
+(select coalesce(sum(score), 0) from post p, post_like pl where u.id = p.creator_id and p.id = pl.post_id) as post_score,
+(select count(*) from comment c where c.creator_id = u.id) as number_of_comments,
+(select coalesce(sum(score), 0) from comment c, comment_like cl where u.id = c.creator_id and c.id = cl.comment_id) as comment_score
+from user_ u;
+
+create materialized view user_mview as select * from user_view;
+
+create unique index idx_user_mview_id on user_mview (id);
+
+-- community_view
+drop view community_aggregates_view cascade;
+create view community_aggregates_view as
+select c.*,
+(select name from user_ u where c.creator_id = u.id) as creator_name,
+(select avatar from user_ u where c.creator_id = u.id) as creator_avatar,
+(select name from category ct where c.category_id = ct.id) as category_name,
+(select count(*) from community_follower cf where cf.community_id = c.id) as number_of_subscribers,
+(select count(*) from post p where p.community_id = c.id) as number_of_posts,
+(select count(*) from comment co, post p where c.id = p.community_id and p.id = co.post_id) as number_of_comments,
+hot_rank((select count(*) from community_follower cf where cf.community_id = c.id), c.published) as hot_rank
+from community c;
+
+create materialized view community_aggregates_mview as select * from community_aggregates_view;
+
+create unique index idx_community_aggregates_mview_id on community_aggregates_mview (id);
+
+create view community_view as
+with all_community as
+(
+  select
+  ca.*
+  from community_aggregates_view ca
+)
+
+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 all_community ac
+
+union all
+
+select 
+ac.*,
+null as user_id,
+null as subscribed
+from all_community ac
+;
+
+create view community_mview as
+with all_community as
+(
+  select
+  ca.*
+  from community_aggregates_mview ca
+)
+
+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 all_community ac
+
+union all
+
+select 
+ac.*,
+null as user_id,
+null as subscribed
+from all_community ac
+;
+
+-- community views
+drop view community_moderator_view;
+drop view community_follower_view;
+drop view community_user_ban_view;
+
+create view community_moderator_view as 
+select *,
+(select name from user_ u where cm.user_id = u.id) as user_name,
+(select avatar from user_ u where cm.user_id = u.id),
+(select name from community c where cm.community_id = c.id) as community_name
+from community_moderator cm;
+
+create view community_follower_view as 
+select *,
+(select name from user_ u where cf.user_id = u.id) as user_name,
+(select avatar from user_ u where cf.user_id = u.id),
+(select name from community c where cf.community_id = c.id) as community_name
+from community_follower cf;
+
+create view community_user_ban_view as 
+select *,
+(select name from user_ u where cm.user_id = u.id) as user_name,
+(select avatar from user_ u where cm.user_id = u.id),
+(select name from community c where cm.community_id = c.id) as community_name
+from community_user_ban cm;
+
+-- post_view
+drop view post_view;
+drop view post_mview;
+drop materialized view post_aggregates_mview;
+drop view post_aggregates_view;
+
+-- regen post view
+create view post_aggregates_view as
+select        
+p.*,
+(select u.banned from user_ u where p.creator_id = u.id) as banned,
+(select cb.id::bool from community_user_ban cb where p.creator_id = cb.user_id and p.community_id = cb.community_id) as banned_from_community,
+(select name from user_ where p.creator_id = user_.id) as creator_name,
+(select avatar from user_ where p.creator_id = user_.id) as creator_avatar,
+(select name from community where p.community_id = community.id) as community_name,
+(select removed from community c where p.community_id = c.id) as community_removed,
+(select deleted from community c where p.community_id = c.id) as community_deleted,
+(select nsfw from community c where p.community_id = c.id) as community_nsfw,
+(select count(*) from comment where comment.post_id = p.id) as number_of_comments,
+coalesce(sum(pl.score), 0) as score,
+count (case when pl.score = 1 then 1 else null end) as upvotes,
+count (case when pl.score = -1 then 1 else null end) as downvotes,
+hot_rank(coalesce(sum(pl.score) , 0), 
+  (
+    case when (p.published < ('now'::timestamp - '1 month'::interval)) then p.published -- Prevents necro-bumps
+    else greatest(c.recent_comment_time, p.published)
+    end
+  )
+) as hot_rank,
+(
+  case when (p.published < ('now'::timestamp - '1 month'::interval)) then p.published -- Prevents necro-bumps
+  else greatest(c.recent_comment_time, p.published)
+  end
+) as newest_activity_time
+from post p
+left join post_like pl on p.id = pl.post_id
+left join (
+  select post_id, 
+  max(published) as recent_comment_time
+  from comment
+  group by 1
+) c on p.id = c.post_id
+group by p.id, c.recent_comment_time;
+
+create materialized view post_aggregates_mview as select * from post_aggregates_view;
+
+create unique index idx_post_aggregates_mview_id on post_aggregates_mview (id);
+
+create view post_view as 
+with all_post as (
+  select
+  pa.*
+  from post_aggregates_view pa
+)
+select
+ap.*,
+u.id as user_id,
+coalesce(pl.score, 0) as my_vote,
+(select cf.id::bool from community_follower cf where u.id = cf.user_id and cf.community_id = ap.community_id) as subscribed,
+(select pr.id::bool from post_read pr where u.id = pr.user_id and pr.post_id = ap.id) as read,
+(select ps.id::bool from post_saved ps where u.id = ps.user_id and ps.post_id = ap.id) as saved
+from user_ u
+cross join all_post ap
+left join post_like pl on u.id = pl.user_id and ap.id = pl.post_id
+
+union all
+
+select 
+ap.*,
+null as user_id,
+null as my_vote,
+null as subscribed,
+null as read,
+null as saved
+from all_post ap
+;
+
+create view post_mview as 
+with all_post as (
+  select
+  pa.*
+  from post_aggregates_mview pa
+)
+select
+ap.*,
+u.id as user_id,
+coalesce(pl.score, 0) as my_vote,
+(select cf.id::bool from community_follower cf where u.id = cf.user_id and cf.community_id = ap.community_id) as subscribed,
+(select pr.id::bool from post_read pr where u.id = pr.user_id and pr.post_id = ap.id) as read,
+(select ps.id::bool from post_saved ps where u.id = ps.user_id and ps.post_id = ap.id) as saved
+from user_ u
+cross join all_post ap
+left join post_like pl on u.id = pl.user_id and ap.id = pl.post_id
+
+union all
+
+select 
+ap.*,
+null as user_id,
+null as my_vote,
+null as subscribed,
+null as read,
+null as saved
+from all_post ap
+;
+
+
+-- reply_view, comment_view, user_mention
+drop view reply_view;
+drop view user_mention_view;
+drop view user_mention_mview;
+drop view comment_view;
+drop view comment_mview;
+drop materialized view comment_aggregates_mview;
+drop view comment_aggregates_view;
+
+-- reply and comment view
+create view comment_aggregates_view as
+select        
+c.*,
+(select community_id from post p where p.id = c.post_id),
+(select co.name from post p, community co where p.id = c.post_id and p.community_id = co.id) as community_name,
+(select u.banned from user_ u where c.creator_id = u.id) as banned,
+(select cb.id::bool from community_user_ban cb, post p where c.creator_id = cb.user_id and p.id = c.post_id and p.community_id = cb.community_id) as banned_from_community,
+(select name from user_ where c.creator_id = user_.id) as creator_name,
+(select avatar from user_ where c.creator_id = user_.id) as creator_avatar,
+coalesce(sum(cl.score), 0) as score,
+count (case when cl.score = 1 then 1 else null end) as upvotes,
+count (case when cl.score = -1 then 1 else null end) as downvotes,
+hot_rank(coalesce(sum(cl.score) , 0), c.published) as hot_rank
+from comment c
+left join comment_like cl on c.id = cl.comment_id
+group by c.id;
+
+create materialized view comment_aggregates_mview as select * from comment_aggregates_view;
+
+create unique index idx_comment_aggregates_mview_id on comment_aggregates_mview (id);
+
+create view comment_view as
+with all_comment as
+(
+  select
+  ca.*
+  from comment_aggregates_view ca
+)
+
+select
+ac.*,
+u.id as user_id,
+coalesce(cl.score, 0) as my_vote,
+(select cf.id::boolean from community_follower cf where u.id = cf.user_id and ac.community_id = cf.community_id) as subscribed,
+(select cs.id::bool from comment_saved cs where u.id = cs.user_id and cs.comment_id = ac.id) as saved
+from user_ u
+cross join all_comment ac
+left join comment_like cl on u.id = cl.user_id and ac.id = cl.comment_id
+
+union all
+
+select 
+    ac.*,
+    null as user_id, 
+    null as my_vote,
+    null as subscribed,
+    null as saved
+from all_comment ac
+;
+
+create view comment_mview as
+with all_comment as
+(
+  select
+  ca.*
+  from comment_aggregates_mview ca
+)
+
+select
+ac.*,
+u.id as user_id,
+coalesce(cl.score, 0) as my_vote,
+(select cf.id::boolean from community_follower cf where u.id = cf.user_id and ac.community_id = cf.community_id) as subscribed,
+(select cs.id::bool from comment_saved cs where u.id = cs.user_id and cs.comment_id = ac.id) as saved
+from user_ u
+cross join all_comment ac
+left join comment_like cl on u.id = cl.user_id and ac.id = cl.comment_id
+
+union all
+
+select 
+    ac.*,
+    null as user_id, 
+    null as my_vote,
+    null as subscribed,
+    null as saved
+from all_comment ac
+;
+
+-- Do the reply_view referencing the comment_mview
+create view reply_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_mview cv, closereply
+where closereply.id = cv.id
+;
+
+-- user mention
+create view user_mention_view as
+select 
+    c.id,
+    um.id as user_mention_id,
+    c.creator_id,
+    c.post_id,
+    c.parent_id,
+    c.content,
+    c.removed,
+    um.read,
+    c.published,
+    c.updated,
+    c.deleted,
+    c.community_id,
+    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
+from user_mention um, comment_view c
+where um.comment_id = c.id;
+
+
+create view user_mention_mview as 
+with all_comment as
+(
+  select
+  ca.*
+  from comment_aggregates_mview ca
+)
+
+select
+    ac.id,
+    um.id as user_mention_id,
+    ac.creator_id,
+    ac.post_id,
+    ac.parent_id,
+    ac.content,
+    ac.removed,
+    um.read,
+    ac.published,
+    ac.updated,
+    ac.deleted,
+    ac.community_id,
+    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
+from user_ u
+cross join all_comment 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.post_id,
+    ac.parent_id,
+    ac.content,
+    ac.removed,
+    um.read,
+    ac.published,
+    ac.updated,
+    ac.deleted,
+    ac.community_id,
+    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
+from all_comment ac
+left join user_mention um on um.comment_id = ac.id
+;
+
diff --git a/server/migrations/2020-04-14-163701_update_views_for_activitypub/up.sql b/server/migrations/2020-04-14-163701_update_views_for_activitypub/up.sql
new file mode 100644 (file)
index 0000000..02499fc
--- /dev/null
@@ -0,0 +1,497 @@
+-- user_view
+drop view user_view cascade;
+
+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,
+(select count(*) from post p where p.creator_id = u.id) as number_of_posts,
+(select coalesce(sum(score), 0) from post p, post_like pl where u.id = p.creator_id and p.id = pl.post_id) as post_score,
+(select count(*) from comment c where c.creator_id = u.id) as number_of_comments,
+(select coalesce(sum(score), 0) from comment c, comment_like cl where u.id = c.creator_id and c.id = cl.comment_id) as comment_score
+from user_ u;
+
+create materialized view user_mview as select * from user_view;
+
+create unique index idx_user_mview_id on user_mview (id);
+
+-- community_view
+drop view community_aggregates_view cascade;
+create view community_aggregates_view as
+-- Now that there's public and private keys, you have to be explicit here
+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,
+(select actor_id from user_ u where c.creator_id = u.id) as creator_actor_id,
+(select local from user_ u where c.creator_id = u.id) as creator_local,
+(select name from user_ u where c.creator_id = u.id) as creator_name,
+(select avatar from user_ u where c.creator_id = u.id) as creator_avatar,
+(select name from category ct where c.category_id = ct.id) as category_name,
+(select count(*) from community_follower cf where cf.community_id = c.id) as number_of_subscribers,
+(select count(*) from post p where p.community_id = c.id) as number_of_posts,
+(select count(*) from comment co, post p where c.id = p.community_id and p.id = co.post_id) as number_of_comments,
+hot_rank((select count(*) from community_follower cf where cf.community_id = c.id), c.published) as hot_rank
+from community c;
+
+create materialized view community_aggregates_mview as select * from community_aggregates_view;
+
+create unique index idx_community_aggregates_mview_id on community_aggregates_mview (id);
+
+create view community_view as
+with all_community as
+(
+  select
+  ca.*
+  from community_aggregates_view ca
+)
+
+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 all_community ac
+
+union all
+
+select 
+ac.*,
+null as user_id,
+null as subscribed
+from all_community ac
+;
+
+create view community_mview as
+with all_community as
+(
+  select
+  ca.*
+  from community_aggregates_mview ca
+)
+
+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 all_community ac
+
+union all
+
+select 
+ac.*,
+null as user_id,
+null as subscribed
+from all_community ac
+;
+
+-- community views
+drop view community_moderator_view;
+drop view community_follower_view;
+drop view community_user_ban_view;
+
+create view community_moderator_view as 
+select *,
+(select actor_id from user_ u where cm.user_id = u.id) as user_actor_id,
+(select local from user_ u where cm.user_id = u.id) as user_local,
+(select name from user_ u where cm.user_id = u.id) as user_name,
+(select avatar from user_ u where cm.user_id = u.id),
+(select actor_id from community c where cm.community_id = c.id) as community_actor_id,
+(select local from community c where cm.community_id = c.id) as community_local,
+(select name from community c where cm.community_id = c.id) as community_name
+from community_moderator cm;
+
+create view community_follower_view as 
+select *,
+(select actor_id from user_ u where cf.user_id = u.id) as user_actor_id,
+(select local from user_ u where cf.user_id = u.id) as user_local,
+(select name from user_ u where cf.user_id = u.id) as user_name,
+(select avatar from user_ u where cf.user_id = u.id),
+(select actor_id from community c where cf.community_id = c.id) as community_actor_id,
+(select local from community c where cf.community_id = c.id) as community_local,
+(select name from community c where cf.community_id = c.id) as community_name
+from community_follower cf;
+
+create view community_user_ban_view as 
+select *,
+(select actor_id from user_ u where cm.user_id = u.id) as user_actor_id,
+(select local from user_ u where cm.user_id = u.id) as user_local,
+(select name from user_ u where cm.user_id = u.id) as user_name,
+(select avatar from user_ u where cm.user_id = u.id),
+(select actor_id from community c where cm.community_id = c.id) as community_actor_id,
+(select local from community c where cm.community_id = c.id) as community_local,
+(select name from community c where cm.community_id = c.id) as community_name
+from community_user_ban cm;
+
+-- post_view
+drop view post_view;
+drop view post_mview;
+drop materialized view post_aggregates_mview;
+drop view post_aggregates_view;
+
+-- regen post view
+create view post_aggregates_view as
+select        
+p.*,
+(select u.banned from user_ u where p.creator_id = u.id) as banned,
+(select cb.id::bool from community_user_ban cb where p.creator_id = cb.user_id and p.community_id = cb.community_id) as banned_from_community,
+(select actor_id from user_ where p.creator_id = user_.id) as creator_actor_id,
+(select local from user_ where p.creator_id = user_.id) as creator_local,
+(select name from user_ where p.creator_id = user_.id) as creator_name,
+(select avatar from user_ where p.creator_id = user_.id) as creator_avatar,
+(select actor_id from community where p.community_id = community.id) as community_actor_id,
+(select local from community where p.community_id = community.id) as community_local,
+(select name from community where p.community_id = community.id) as community_name,
+(select removed from community c where p.community_id = c.id) as community_removed,
+(select deleted from community c where p.community_id = c.id) as community_deleted,
+(select nsfw from community c where p.community_id = c.id) as community_nsfw,
+(select count(*) from comment where comment.post_id = p.id) as number_of_comments,
+coalesce(sum(pl.score), 0) as score,
+count (case when pl.score = 1 then 1 else null end) as upvotes,
+count (case when pl.score = -1 then 1 else null end) as downvotes,
+hot_rank(coalesce(sum(pl.score) , 0), 
+  (
+    case when (p.published < ('now'::timestamp - '1 month'::interval)) then p.published -- Prevents necro-bumps
+    else greatest(c.recent_comment_time, p.published)
+    end
+  )
+) as hot_rank,
+(
+  case when (p.published < ('now'::timestamp - '1 month'::interval)) then p.published -- Prevents necro-bumps
+  else greatest(c.recent_comment_time, p.published)
+  end
+) as newest_activity_time
+from post p
+left join post_like pl on p.id = pl.post_id
+left join (
+  select post_id, 
+  max(published) as recent_comment_time
+  from comment
+  group by 1
+) c on p.id = c.post_id
+group by p.id, c.recent_comment_time;
+
+create materialized view post_aggregates_mview as select * from post_aggregates_view;
+
+create unique index idx_post_aggregates_mview_id on post_aggregates_mview (id);
+
+create view post_view as 
+with all_post as (
+  select
+  pa.*
+  from post_aggregates_view pa
+)
+select
+ap.*,
+u.id as user_id,
+coalesce(pl.score, 0) as my_vote,
+(select cf.id::bool from community_follower cf where u.id = cf.user_id and cf.community_id = ap.community_id) as subscribed,
+(select pr.id::bool from post_read pr where u.id = pr.user_id and pr.post_id = ap.id) as read,
+(select ps.id::bool from post_saved ps where u.id = ps.user_id and ps.post_id = ap.id) as saved
+from user_ u
+cross join all_post ap
+left join post_like pl on u.id = pl.user_id and ap.id = pl.post_id
+
+union all
+
+select 
+ap.*,
+null as user_id,
+null as my_vote,
+null as subscribed,
+null as read,
+null as saved
+from all_post ap
+;
+
+create view post_mview as 
+with all_post as (
+  select
+  pa.*
+  from post_aggregates_mview pa
+)
+select
+ap.*,
+u.id as user_id,
+coalesce(pl.score, 0) as my_vote,
+(select cf.id::bool from community_follower cf where u.id = cf.user_id and cf.community_id = ap.community_id) as subscribed,
+(select pr.id::bool from post_read pr where u.id = pr.user_id and pr.post_id = ap.id) as read,
+(select ps.id::bool from post_saved ps where u.id = ps.user_id and ps.post_id = ap.id) as saved
+from user_ u
+cross join all_post ap
+left join post_like pl on u.id = pl.user_id and ap.id = pl.post_id
+
+union all
+
+select 
+ap.*,
+null as user_id,
+null as my_vote,
+null as subscribed,
+null as read,
+null as saved
+from all_post ap
+;
+
+
+-- reply_view, comment_view, user_mention
+drop view reply_view;
+drop view user_mention_view;
+drop view user_mention_mview;
+drop view comment_view;
+drop view comment_mview;
+drop materialized view comment_aggregates_mview;
+drop view comment_aggregates_view;
+
+-- reply and comment view
+create view comment_aggregates_view as
+select        
+c.*,
+(select community_id from post p where p.id = c.post_id),
+(select co.actor_id from post p, community co where p.id = c.post_id and p.community_id = co.id) as community_actor_id,
+(select co.local from post p, community co where p.id = c.post_id and p.community_id = co.id) as community_local,
+(select co.name from post p, community co where p.id = c.post_id and p.community_id = co.id) as community_name,
+(select u.banned from user_ u where c.creator_id = u.id) as banned,
+(select cb.id::bool from community_user_ban cb, post p where c.creator_id = cb.user_id and p.id = c.post_id and p.community_id = cb.community_id) as banned_from_community,
+(select actor_id from user_ where c.creator_id = user_.id) as creator_actor_id,
+(select local from user_ where c.creator_id = user_.id) as creator_local,
+(select name from user_ where c.creator_id = user_.id) as creator_name,
+(select avatar from user_ where c.creator_id = user_.id) as creator_avatar,
+coalesce(sum(cl.score), 0) as score,
+count (case when cl.score = 1 then 1 else null end) as upvotes,
+count (case when cl.score = -1 then 1 else null end) as downvotes,
+hot_rank(coalesce(sum(cl.score) , 0), c.published) as hot_rank
+from comment c
+left join comment_like cl on c.id = cl.comment_id
+group by c.id;
+
+create materialized view comment_aggregates_mview as select * from comment_aggregates_view;
+
+create unique index idx_comment_aggregates_mview_id on comment_aggregates_mview (id);
+
+create view comment_view as
+with all_comment as
+(
+  select
+  ca.*
+  from comment_aggregates_view ca
+)
+
+select
+ac.*,
+u.id as user_id,
+coalesce(cl.score, 0) as my_vote,
+(select cf.id::boolean from community_follower cf where u.id = cf.user_id and ac.community_id = cf.community_id) as subscribed,
+(select cs.id::bool from comment_saved cs where u.id = cs.user_id and cs.comment_id = ac.id) as saved
+from user_ u
+cross join all_comment ac
+left join comment_like cl on u.id = cl.user_id and ac.id = cl.comment_id
+
+union all
+
+select 
+    ac.*,
+    null as user_id, 
+    null as my_vote,
+    null as subscribed,
+    null as saved
+from all_comment ac
+;
+
+create view comment_mview as
+with all_comment as
+(
+  select
+  ca.*
+  from comment_aggregates_mview ca
+)
+
+select
+ac.*,
+u.id as user_id,
+coalesce(cl.score, 0) as my_vote,
+(select cf.id::boolean from community_follower cf where u.id = cf.user_id and ac.community_id = cf.community_id) as subscribed,
+(select cs.id::bool from comment_saved cs where u.id = cs.user_id and cs.comment_id = ac.id) as saved
+from user_ u
+cross join all_comment ac
+left join comment_like cl on u.id = cl.user_id and ac.id = cl.comment_id
+
+union all
+
+select 
+    ac.*,
+    null as user_id, 
+    null as my_vote,
+    null as subscribed,
+    null as saved
+from all_comment ac
+;
+
+-- Do the reply_view referencing the comment_mview
+create view reply_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_mview cv, closereply
+where closereply.id = cv.id
+;
+
+-- user mention
+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.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_mview as 
+with all_comment as
+(
+  select
+  ca.*
+  from comment_aggregates_mview ca
+)
+
+select
+    ac.id,
+    um.id as user_mention_id,
+    ac.creator_id,
+    ac.creator_actor_id,
+    ac.creator_local,
+    ac.post_id,
+    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 all_comment 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.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 all_comment ac
+left join user_mention um on um.comment_id = ac.id
+;
+
diff --git a/server/migrations/2020-04-21-123957_remove_unique_user_constraints/down.sql b/server/migrations/2020-04-21-123957_remove_unique_user_constraints/down.sql
new file mode 100644 (file)
index 0000000..a172581
--- /dev/null
@@ -0,0 +1,4 @@
+-- The username index
+drop index idx_user_name_lower_actor_id;
+create unique index idx_user_name_lower on user_ (lower(name));
+
diff --git a/server/migrations/2020-04-21-123957_remove_unique_user_constraints/up.sql b/server/migrations/2020-04-21-123957_remove_unique_user_constraints/up.sql
new file mode 100644 (file)
index 0000000..969eab0
--- /dev/null
@@ -0,0 +1,2 @@
+drop index idx_user_name_lower;
+create unique index idx_user_name_lower_actor_id on user_ (lower(name), lower(actor_id));
diff --git a/server/migrations/2020-05-05-210233_add_activitypub_for_private_messages/down.sql b/server/migrations/2020-05-05-210233_add_activitypub_for_private_messages/down.sql
new file mode 100644 (file)
index 0000000..15c9285
--- /dev/null
@@ -0,0 +1,21 @@
+drop materialized view private_message_mview;
+drop view private_message_view;
+
+alter table private_message 
+drop column ap_id, 
+drop column local;
+
+create view private_message_view as 
+select        
+pm.*,
+u.name as creator_name,
+u.avatar as creator_avatar,
+u2.name as recipient_name,
+u2.avatar as recipient_avatar
+from private_message pm
+inner join user_ u on u.id = pm.creator_id
+inner join user_ u2 on u2.id = pm.recipient_id;
+
+create materialized view private_message_mview as select * from private_message_view;
+
+create unique index idx_private_message_mview_id on private_message_mview (id);
diff --git a/server/migrations/2020-05-05-210233_add_activitypub_for_private_messages/up.sql b/server/migrations/2020-05-05-210233_add_activitypub_for_private_messages/up.sql
new file mode 100644 (file)
index 0000000..627be1f
--- /dev/null
@@ -0,0 +1,25 @@
+alter table private_message
+add column ap_id character varying(255) not null default 'changeme', -- This needs to be checked and updated in code, building from the site url if local
+add column local boolean not null default true
+;
+
+drop materialized view private_message_mview;
+drop view private_message_view;
+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;
+
+create materialized view private_message_mview as select * from private_message_view;
+
+create unique index idx_private_message_mview_id on private_message_mview (id);
index 058c72674877341c2d841479c2076b5db9d146ac..369cba5c3d399d8483e3033a8ad492a39c7976d4 100644 (file)
@@ -1,4 +1,42 @@
-use super::*;
+use crate::{
+  api::{APIError, Oper, Perform},
+  apub::{ApubLikeableType, ApubObjectType},
+  db::{
+    comment::*,
+    comment_view::*,
+    community_view::*,
+    moderator::*,
+    post::*,
+    site_view::*,
+    user::*,
+    user_mention::*,
+    user_view::*,
+    Crud,
+    Likeable,
+    ListingType,
+    Saveable,
+    SortType,
+  },
+  naive_now,
+  remove_slurs,
+  scrape_text_for_mentions,
+  send_email,
+  settings::Settings,
+  websocket::{
+    server::{JoinCommunityRoom, SendComment},
+    UserOperation,
+    WebsocketInfo,
+  },
+  MentionData,
+};
+use diesel::{
+  r2d2::{ConnectionManager, Pool},
+  PgConnection,
+};
+use failure::Error;
+use log::error;
+use serde::{Deserialize, Serialize};
+use std::str::FromStr;
 
 #[derive(Serialize, Deserialize)]
 pub struct CreateComment {
@@ -76,8 +114,6 @@ impl Perform for Oper<CreateComment> {
 
     let user_id = claims.id;
 
-    let hostname = &format!("https://{}", Settings::get().hostname);
-
     let conn = pool.get()?;
 
     // Check for a community ban
@@ -87,7 +123,8 @@ impl Perform for Oper<CreateComment> {
     }
 
     // Check for a site ban
-    if UserView::read(&conn, user_id)?.banned {
+    let user = User_::read(&conn, user_id)?;
+    if user.banned {
       return Err(APIError::err("site_ban").into());
     }
 
@@ -101,7 +138,10 @@ impl Perform for Oper<CreateComment> {
       removed: None,
       deleted: None,
       read: None,
+      published: None,
       updated: None,
+      ap_id: "changeme".into(),
+      local: true,
     };
 
     let inserted_comment = match Comment::create(&conn, &comment_form) {
@@ -109,107 +149,16 @@ impl Perform for Oper<CreateComment> {
       Err(_e) => return Err(APIError::err("couldnt_create_comment").into()),
     };
 
-    let mut recipient_ids = Vec::new();
-
-    // Scan the comment for user mentions, add those rows
-    let extracted_usernames = extract_usernames(&comment_form.content);
-
-    for username_mention in &extracted_usernames {
-      if let Ok(mention_user) = User_::read_from_name(&conn, (*username_mention).to_string()) {
-        // You can't mention yourself
-        // At some point, make it so you can't tag the parent creator either
-        // This can cause two notifications, one for reply and the other for mention
-        if mention_user.id != user_id {
-          recipient_ids.push(mention_user.id);
-
-          let user_mention_form = UserMentionForm {
-            recipient_id: mention_user.id,
-            comment_id: inserted_comment.id,
-            read: None,
-          };
-
-          // Allow this to fail softly, since comment edits might re-update or replace it
-          // Let the uniqueness handle this fail
-          match UserMention::create(&conn, &user_mention_form) {
-            Ok(_mention) => (),
-            Err(_e) => error!("{}", &_e),
-          };
-
-          // Send an email to those users that have notifications on
-          if mention_user.send_notifications_to_email {
-            if let Some(mention_email) = mention_user.email {
-              let subject = &format!(
-                "{} - Mentioned by {}",
-                Settings::get().hostname,
-                claims.username
-              );
-              let html = &format!(
-                "<h1>User Mention</h1><br><div>{} - {}</div><br><a href={}/inbox>inbox</a>",
-                claims.username, comment_form.content, hostname
-              );
-              match send_email(subject, &mention_email, &mention_user.name, html) {
-                Ok(_o) => _o,
-                Err(e) => error!("{}", e),
-              };
-            }
-          }
-        }
-      }
-    }
+    let updated_comment = match Comment::update_ap_id(&conn, inserted_comment.id) {
+      Ok(comment) => comment,
+      Err(_e) => return Err(APIError::err("couldnt_create_comment").into()),
+    };
 
-    // Send notifs to the parent commenter / poster
-    match data.parent_id {
-      Some(parent_id) => {
-        let parent_comment = Comment::read(&conn, parent_id)?;
-        if parent_comment.creator_id != user_id {
-          let parent_user = User_::read(&conn, parent_comment.creator_id)?;
-          recipient_ids.push(parent_user.id);
+    updated_comment.send_create(&user, &conn)?;
 
-          if parent_user.send_notifications_to_email {
-            if let Some(comment_reply_email) = parent_user.email {
-              let subject = &format!(
-                "{} - Reply from {}",
-                Settings::get().hostname,
-                claims.username
-              );
-              let html = &format!(
-                "<h1>Comment Reply</h1><br><div>{} - {}</div><br><a href={}/inbox>inbox</a>",
-                claims.username, comment_form.content, hostname
-              );
-              match send_email(subject, &comment_reply_email, &parent_user.name, html) {
-                Ok(_o) => _o,
-                Err(e) => error!("{}", e),
-              };
-            }
-          }
-        }
-      }
-      // Its a post
-      None => {
-        if post.creator_id != user_id {
-          let parent_user = User_::read(&conn, post.creator_id)?;
-          recipient_ids.push(parent_user.id);
-
-          if parent_user.send_notifications_to_email {
-            if let Some(post_reply_email) = parent_user.email {
-              let subject = &format!(
-                "{} - Reply from {}",
-                Settings::get().hostname,
-                claims.username
-              );
-              let html = &format!(
-                "<h1>Post Reply</h1><br><div>{} - {}</div><br><a href={}/inbox>inbox</a>",
-                claims.username, comment_form.content, hostname
-              );
-              match send_email(subject, &post_reply_email, &parent_user.name, html) {
-                Ok(_o) => _o,
-                Err(e) => error!("{}", e),
-              };
-            }
-          }
-        }
-      }
-    };
+    // Scan the comment for user mentions, add those rows
+    let mentions = scrape_text_for_mentions(&comment_form.content);
+    let recipient_ids = send_local_notifs(&conn, &mentions, &updated_comment, &user, &post);
 
     // You like your own comment by default
     let like_form = CommentLikeForm {
@@ -224,6 +173,8 @@ impl Perform for Oper<CreateComment> {
       Err(_e) => return Err(APIError::err("couldnt_like_comment").into()),
     };
 
+    updated_comment.send_like(&user, &conn)?;
+
     let comment_view = CommentView::read(&conn, inserted_comment.id, Some(user_id))?;
 
     let mut res = CommentResponse {
@@ -266,6 +217,8 @@ impl Perform for Oper<EditComment> {
 
     let conn = pool.get()?;
 
+    let user = User_::read(&conn, user_id)?;
+
     let orig_comment = CommentView::read(&conn, data.edit_id, None)?;
 
     // You are allowed to mark the comment as read even if you're banned.
@@ -290,13 +243,15 @@ impl Perform for Oper<EditComment> {
       }
 
       // Check for a site ban
-      if UserView::read(&conn, user_id)?.banned {
+      if user.banned {
         return Err(APIError::err("site_ban").into());
       }
     }
 
     let content_slurs_removed = remove_slurs(&data.content.to_owned());
 
+    let read_comment = Comment::read(&conn, data.edit_id)?;
+
     let comment_form = CommentForm {
       content: content_slurs_removed,
       parent_id: data.parent_id,
@@ -305,66 +260,42 @@ impl Perform for Oper<EditComment> {
       removed: data.removed.to_owned(),
       deleted: data.deleted.to_owned(),
       read: data.read.to_owned(),
+      published: None,
       updated: if data.read.is_some() {
         orig_comment.updated
       } else {
         Some(naive_now())
       },
+      ap_id: read_comment.ap_id,
+      local: read_comment.local,
     };
 
-    let _updated_comment = match Comment::update(&conn, data.edit_id, &comment_form) {
+    let updated_comment = match Comment::update(&conn, data.edit_id, &comment_form) {
       Ok(comment) => comment,
       Err(_e) => return Err(APIError::err("couldnt_update_comment").into()),
     };
 
-    let mut recipient_ids = Vec::new();
-
-    // Scan the comment for user mentions, add those rows
-    let extracted_usernames = extract_usernames(&comment_form.content);
-
-    for username_mention in &extracted_usernames {
-      let mention_user = User_::read_from_name(&conn, (*username_mention).to_string());
-
-      if mention_user.is_ok() {
-        let mention_user_id = mention_user?.id;
-
-        // You can't mention yourself
-        // At some point, make it so you can't tag the parent creator either
-        // This can cause two notifications, one for reply and the other for mention
-        if mention_user_id != user_id {
-          recipient_ids.push(mention_user_id);
-
-          let user_mention_form = UserMentionForm {
-            recipient_id: mention_user_id,
-            comment_id: data.edit_id,
-            read: None,
-          };
-
-          // Allow this to fail softly, since comment edits might re-update or replace it
-          // Let the uniqueness handle this fail
-          match UserMention::create(&conn, &user_mention_form) {
-            Ok(_mention) => (),
-            Err(_e) => error!("{}", &_e),
-          }
-        }
-      }
-    }
-
-    // Add to recipient ids
-    match data.parent_id {
-      Some(parent_id) => {
-        let parent_comment = Comment::read(&conn, parent_id)?;
-        if parent_comment.creator_id != user_id {
-          let parent_user = User_::read(&conn, parent_comment.creator_id)?;
-          recipient_ids.push(parent_user.id);
-        }
+    if let Some(deleted) = data.deleted.to_owned() {
+      if deleted {
+        updated_comment.send_delete(&user, &conn)?;
+      } else {
+        updated_comment.send_undo_delete(&user, &conn)?;
       }
-      None => {
-        let post = Post::read(&conn, data.post_id)?;
-        recipient_ids.push(post.creator_id);
+    } else if let Some(removed) = data.removed.to_owned() {
+      if removed {
+        updated_comment.send_remove(&user, &conn)?;
+      } else {
+        updated_comment.send_undo_remove(&user, &conn)?;
       }
+    } else {
+      updated_comment.send_update(&user, &conn)?;
     }
 
+    let post = Post::read(&conn, data.post_id)?;
+
+    let mentions = scrape_text_for_mentions(&comment_form.content);
+    let recipient_ids = send_local_notifs(&conn, &mentions, &updated_comment, &user, &post);
+
     // Mod tables
     if let Some(removed) = data.removed.to_owned() {
       let form = ModRemoveCommentForm {
@@ -480,7 +411,8 @@ impl Perform for Oper<CreateCommentLike> {
     }
 
     // Check for a site ban
-    if UserView::read(&conn, user_id)?.banned {
+    let user = User_::read(&conn, user_id)?;
+    if user.banned {
       return Err(APIError::err("site_ban").into());
     }
 
@@ -517,6 +449,14 @@ impl Perform for Oper<CreateCommentLike> {
         Ok(like) => like,
         Err(_e) => return Err(APIError::err("couldnt_like_comment").into()),
       };
+
+      if like_form.score == 1 {
+        comment.send_like(&user, &conn)?;
+      } else if like_form.score == -1 {
+        comment.send_dislike(&user, &conn)?;
+      }
+    } else {
+      comment.send_undo_like(&user, &conn)?;
     }
 
     // Have to refetch the comment to get the current state
@@ -601,3 +541,106 @@ impl Perform for Oper<GetComments> {
     Ok(GetCommentsResponse { comments })
   }
 }
+
+pub fn send_local_notifs(
+  conn: &PgConnection,
+  mentions: &[MentionData],
+  comment: &Comment,
+  user: &User_,
+  post: &Post,
+) -> Vec<i32> {
+  let mut recipient_ids = Vec::new();
+  let hostname = &format!("https://{}", Settings::get().hostname);
+
+  // Send the local mentions
+  for mention in mentions
+    .iter()
+    .filter(|m| m.is_local() && m.name.ne(&user.name))
+    .collect::<Vec<&MentionData>>()
+  {
+    if let Ok(mention_user) = User_::read_from_name(&conn, &mention.name) {
+      // TODO
+      // At some point, make it so you can't tag the parent creator either
+      // This can cause two notifications, one for reply and the other for mention
+      recipient_ids.push(mention_user.id);
+
+      let user_mention_form = UserMentionForm {
+        recipient_id: mention_user.id,
+        comment_id: comment.id,
+        read: None,
+      };
+
+      // Allow this to fail softly, since comment edits might re-update or replace it
+      // Let the uniqueness handle this fail
+      match UserMention::create(&conn, &user_mention_form) {
+        Ok(_mention) => (),
+        Err(_e) => error!("{}", &_e),
+      };
+
+      // Send an email to those users that have notifications on
+      if 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!(
+            "<h1>User Mention</h1><br><div>{} - {}</div><br><a href={}/inbox>inbox</a>",
+            user.name, comment.content, hostname
+          );
+          match send_email(subject, &mention_email, &mention_user.name, html) {
+            Ok(_o) => _o,
+            Err(e) => error!("{}", e),
+          };
+        }
+      }
+    }
+  }
+
+  // Send notifs to the parent commenter / poster
+  match comment.parent_id {
+    Some(parent_id) => {
+      if let Ok(parent_comment) = Comment::read(&conn, parent_id) {
+        if parent_comment.creator_id != user.id {
+          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 let Some(comment_reply_email) = parent_user.email {
+                let subject = &format!("{} - Reply from {}", Settings::get().hostname, user.name,);
+                let html = &format!(
+                  "<h1>Comment Reply</h1><br><div>{} - {}</div><br><a href={}/inbox>inbox</a>",
+                  user.name, comment.content, hostname
+                );
+                match send_email(subject, &comment_reply_email, &parent_user.name, html) {
+                  Ok(_o) => _o,
+                  Err(e) => error!("{}", e),
+                };
+              }
+            }
+          }
+        }
+      }
+    }
+    // Its a post
+    None => {
+      if post.creator_id != user.id {
+        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 let Some(post_reply_email) = parent_user.email {
+              let subject = &format!("{} - Reply from {}", Settings::get().hostname, user.name,);
+              let html = &format!(
+                "<h1>Post Reply</h1><br><div>{} - {}</div><br><a href={}/inbox>inbox</a>",
+                user.name, comment.content, hostname
+              );
+              match send_email(subject, &post_reply_email, &parent_user.name, html) {
+                Ok(_o) => _o,
+                Err(e) => error!("{}", e),
+              };
+            }
+          }
+        }
+      }
+    }
+  };
+  recipient_ids
+}
index 618122b9868a1aab8c1a32ef16e455f974b732ae..3fc67eb348f766905ea14e0b701bed2e9009a50d 100644 (file)
@@ -1,18 +1,44 @@
 use super::*;
-use crate::is_valid_community_name;
+use crate::{
+  api::{APIError, Oper, Perform},
+  apub::{
+    extensions::signatures::generate_actor_keypair,
+    make_apub_endpoint,
+    ActorType,
+    EndpointType,
+  },
+  db::{Bannable, Crud, Followable, Joinable, SortType},
+  is_valid_community_name,
+  naive_from_unix,
+  naive_now,
+  slur_check,
+  slurs_vec_to_str,
+  websocket::{
+    server::{JoinCommunityRoom, SendCommunityRoomMessage},
+    UserOperation,
+    WebsocketInfo,
+  },
+};
+use diesel::{
+  r2d2::{ConnectionManager, Pool},
+  PgConnection,
+};
+use failure::Error;
+use serde::{Deserialize, Serialize};
+use std::str::FromStr;
 
 #[derive(Serialize, Deserialize)]
 pub struct GetCommunity {
   id: Option<i32>,
-  name: Option<String>,
+  pub name: Option<String>,
   auth: Option<String>,
 }
 
 #[derive(Serialize, Deserialize)]
 pub struct GetCommunityResponse {
   pub community: CommunityView,
-  moderators: Vec<CommunityModeratorView>,
-  admins: Vec<UserView>,
+  pub moderators: Vec<CommunityModeratorView>,
+  pub admins: Vec<UserView>,
   pub online: usize,
 }
 
@@ -31,17 +57,17 @@ pub struct CommunityResponse {
   pub community: CommunityView,
 }
 
-#[derive(Serialize, Deserialize)]
+#[derive(Serialize, Deserialize, Debug)]
 pub struct ListCommunities {
-  sort: String,
-  page: Option<i64>,
-  limit: Option<i64>,
-  auth: Option<String>,
+  pub sort: String,
+  pub page: Option<i64>,
+  pub limit: Option<i64>,
+  pub auth: Option<String>,
 }
 
-#[derive(Serialize, Deserialize)]
+#[derive(Serialize, Deserialize, Debug)]
 pub struct ListCommunitiesResponse {
-  communities: Vec<CommunityView>,
+  pub communities: Vec<CommunityView>,
 }
 
 #[derive(Serialize, Deserialize, Clone)]
@@ -135,25 +161,25 @@ impl Perform for Oper<GetCommunity> {
 
     let conn = pool.get()?;
 
-    let community_id = match data.id {
-      Some(id) => id,
+    let community = match data.id {
+      Some(id) => Community::read(&conn, id)?,
       None => {
         match Community::read_from_name(
           &conn,
-          data.name.to_owned().unwrap_or_else(|| "main".to_string()),
+          &data.name.to_owned().unwrap_or_else(|| "main".to_string()),
         ) {
-          Ok(community) => community.id,
+          Ok(community) => community,
           Err(_e) => return Err(APIError::err("couldnt_find_community").into()),
         }
       }
     };
 
-    let community_view = match CommunityView::read(&conn, community_id, user_id) {
+    let community_view = match CommunityView::read(&conn, community.id, user_id) {
       Ok(community) => community,
       Err(_e) => return Err(APIError::err("couldnt_find_community").into()),
     };
 
-    let moderators = match CommunityModeratorView::for_community(&conn, community_id) {
+    let moderators = match CommunityModeratorView::for_community(&conn, community.id) {
       Ok(moderators) => moderators,
       Err(_e) => return Err(APIError::err("couldnt_find_community").into()),
     };
@@ -166,8 +192,10 @@ impl Perform for Oper<GetCommunity> {
 
     let online = if let Some(ws) = websocket_info {
       if let Some(id) = ws.id {
-        ws.chatserver
-          .do_send(JoinCommunityRoom { community_id, id });
+        ws.chatserver.do_send(JoinCommunityRoom {
+          community_id: community.id,
+          id,
+        });
       }
 
       // TODO
@@ -235,6 +263,8 @@ impl Perform for Oper<CreateCommunity> {
     }
 
     // When you create a community, make sure the user becomes a moderator and a follower
+    let keypair = generate_actor_keypair()?;
+
     let community_form = CommunityForm {
       name: data.name.to_owned(),
       title: data.title.to_owned(),
@@ -245,6 +275,12 @@ impl Perform for Oper<CreateCommunity> {
       deleted: None,
       nsfw: data.nsfw,
       updated: None,
+      actor_id: make_apub_endpoint(EndpointType::Community, &data.name).to_string(),
+      local: true,
+      private_key: Some(keypair.private_key),
+      public_key: Some(keypair.public_key),
+      last_refreshed_at: None,
+      published: None,
     };
 
     let inserted_community = match Community::create(&conn, &community_form) {
@@ -320,7 +356,8 @@ impl Perform for Oper<EditCommunity> {
     let conn = pool.get()?;
 
     // Check for a site ban
-    if UserView::read(&conn, user_id)?.banned {
+    let user = User_::read(&conn, user_id)?;
+    if user.banned {
       return Err(APIError::err("site_ban").into());
     }
 
@@ -337,6 +374,8 @@ impl Perform for Oper<EditCommunity> {
       return Err(APIError::err("no_community_edit_allowed").into());
     }
 
+    let read_community = Community::read(&conn, data.edit_id)?;
+
     let community_form = CommunityForm {
       name: data.name.to_owned(),
       title: data.title.to_owned(),
@@ -347,9 +386,15 @@ impl Perform for Oper<EditCommunity> {
       deleted: data.deleted.to_owned(),
       nsfw: data.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 _updated_community = match Community::update(&conn, data.edit_id, &community_form) {
+    let updated_community = match Community::update(&conn, data.edit_id, &community_form) {
       Ok(community) => community,
       Err(_e) => return Err(APIError::err("couldnt_update_community").into()),
     };
@@ -370,6 +415,20 @@ impl Perform for Oper<EditCommunity> {
       ModRemoveCommunity::create(&conn, &form)?;
     }
 
+    if let Some(deleted) = data.deleted.to_owned() {
+      if deleted {
+        updated_community.send_delete(&user, &conn)?;
+      } else {
+        updated_community.send_undo_delete(&user, &conn)?;
+      }
+    } else if let Some(removed) = data.removed.to_owned() {
+      if removed {
+        updated_community.send_remove(&user, &conn)?;
+      } else {
+        updated_community.send_undo_remove(&user, &conn)?;
+      }
+    }
+
     let community_view = CommunityView::read(&conn, data.edit_id, Some(user_id))?;
 
     let res = CommunityResponse {
@@ -456,23 +515,41 @@ impl Perform for Oper<FollowCommunity> {
 
     let user_id = claims.id;
 
+    let conn = pool.get()?;
+
+    let community = Community::read(&conn, data.community_id)?;
     let community_follower_form = CommunityFollowerForm {
       community_id: data.community_id,
       user_id,
     };
 
-    let conn = pool.get()?;
-
-    if data.follow {
-      match CommunityFollower::follow(&conn, &community_follower_form) {
-        Ok(user) => user,
-        Err(_e) => return Err(APIError::err("community_follower_already_exists").into()),
-      };
+    if community.local {
+      if data.follow {
+        match CommunityFollower::follow(&conn, &community_follower_form) {
+          Ok(user) => user,
+          Err(_e) => return Err(APIError::err("community_follower_already_exists").into()),
+        };
+      } else {
+        match CommunityFollower::unfollow(&conn, &community_follower_form) {
+          Ok(user) => user,
+          Err(_e) => return Err(APIError::err("community_follower_already_exists").into()),
+        };
+      }
     } else {
-      match CommunityFollower::ignore(&conn, &community_follower_form) {
-        Ok(user) => user,
-        Err(_e) => return Err(APIError::err("community_follower_already_exists").into()),
-      };
+      let user = User_::read(&conn, user_id)?;
+
+      if data.follow {
+        // Dont actually add to the community followers here, because you need
+        // to wait for the accept
+        user.send_follow(&community.actor_id, &conn)?;
+      } else {
+        user.send_unfollow(&community.actor_id, &conn)?;
+        match CommunityFollower::unfollow(&conn, &community_follower_form) {
+          Ok(user) => user,
+          Err(_e) => return Err(APIError::err("community_follower_already_exists").into()),
+        };
+      }
+      // TODO: this needs to return a "pending" state, until Accept is received from the remote server
     }
 
     let community_view = CommunityView::read(&conn, data.community_id, Some(user_id))?;
@@ -684,11 +761,17 @@ impl Perform for Oper<TransferCommunity> {
       title: read_community.title,
       description: read_community.description,
       category_id: read_community.category_id,
-      creator_id: data.user_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 _updated_community = match Community::update(&conn, data.community_id, &community_form) {
index 4f11b3278e72bbbc8294b8bff49a9fc9710d557e..afd62aff8011a524cb45e0b748fb96c3af8c5c81 100644 (file)
@@ -1,42 +1,12 @@
-use crate::db::category::*;
-use crate::db::comment::*;
-use crate::db::comment_view::*;
-use crate::db::community::*;
-use crate::db::community_view::*;
-use crate::db::moderator::*;
-use crate::db::moderator_views::*;
-use crate::db::password_reset_request::*;
-use crate::db::post::*;
-use crate::db::post_view::*;
-use crate::db::private_message::*;
-use crate::db::private_message_view::*;
-use crate::db::site::*;
-use crate::db::site_view::*;
-use crate::db::user::*;
-use crate::db::user_mention::*;
-use crate::db::user_mention_view::*;
-use crate::db::user_view::*;
-use crate::db::*;
 use crate::{
-  extract_usernames, fetch_iframely_and_pictrs_data, generate_random_string, naive_from_unix,
-  naive_now, remove_slurs, send_email, slur_check, slurs_vec_to_str,
+  db::{community::*, community_view::*, moderator::*, site::*, user::*, user_view::*},
+  websocket::WebsocketInfo,
 };
-
-use crate::settings::Settings;
-use crate::websocket::UserOperation;
-use crate::websocket::{
-  server::{
-    JoinCommunityRoom, JoinPostRoom, JoinUserRoom, SendAllMessage, SendComment,
-    SendCommunityRoomMessage, SendPost, SendUserRoomMessage,
-  },
-  WebsocketInfo,
+use diesel::{
+  r2d2::{ConnectionManager, Pool},
+  PgConnection,
 };
-use diesel::r2d2::{ConnectionManager, Pool};
-use diesel::PgConnection;
 use failure::Error;
-use log::{error, info};
-use serde::{Deserialize, Serialize};
-use std::str::FromStr;
 
 pub mod comment;
 pub mod community;
@@ -62,8 +32,8 @@ pub struct Oper<T> {
   data: T,
 }
 
-impl<T> Oper<T> {
-  pub fn new(data: T) -> Oper<T> {
+impl<Data> Oper<Data> {
+  pub fn new(data: Data) -> Oper<Data> {
     Oper { data }
   }
 }
index 9eeb5158085a842b6a6901967eec66e91322052f..420bef1f6211e648fe449b1f876c7a8e94d22c27 100644 (file)
@@ -1,6 +1,41 @@
-use super::*;
-
-#[derive(Serialize, Deserialize)]
+use crate::{
+  api::{APIError, Oper, Perform},
+  apub::{ApubLikeableType, ApubObjectType},
+  db::{
+    comment_view::*,
+    community_view::*,
+    moderator::*,
+    post::*,
+    post_view::*,
+    site::*,
+    site_view::*,
+    user::*,
+    user_view::*,
+    Crud,
+    Likeable,
+    ListingType,
+    Saveable,
+    SortType,
+  },
+  fetch_iframely_and_pictrs_data,
+  naive_now,
+  slur_check,
+  slurs_vec_to_str,
+  websocket::{
+    server::{JoinCommunityRoom, JoinPostRoom, SendPost},
+    UserOperation,
+    WebsocketInfo,
+  },
+};
+use diesel::{
+  r2d2::{ConnectionManager, Pool},
+  PgConnection,
+};
+use failure::Error;
+use serde::{Deserialize, Serialize};
+use std::str::FromStr;
+
+#[derive(Serialize, Deserialize, Debug)]
 pub struct CreatePost {
   name: String,
   url: Option<String>,
@@ -31,7 +66,7 @@ pub struct GetPostResponse {
   pub online: usize,
 }
 
-#[derive(Serialize, Deserialize)]
+#[derive(Serialize, Deserialize, Debug)]
 pub struct GetPosts {
   type_: String,
   sort: String,
@@ -41,9 +76,9 @@ pub struct GetPosts {
   auth: Option<String>,
 }
 
-#[derive(Serialize, Deserialize)]
+#[derive(Serialize, Deserialize, Debug)]
 pub struct GetPostsResponse {
-  posts: Vec<PostView>,
+  pub posts: Vec<PostView>,
 }
 
 #[derive(Serialize, Deserialize)]
@@ -112,7 +147,8 @@ impl Perform for Oper<CreatePost> {
     }
 
     // Check for a site ban
-    if UserView::read(&conn, user_id)?.banned {
+    let user = User_::read(&conn, user_id)?;
+    if user.banned {
       return Err(APIError::err("site_ban").into());
     }
 
@@ -136,6 +172,9 @@ impl Perform for Oper<CreatePost> {
       embed_description: iframely_description,
       embed_html: iframely_html,
       thumbnail_url: pictrs_thumbnail,
+      ap_id: "changeme".into(),
+      local: true,
+      published: None,
     };
 
     let inserted_post = match Post::create(&conn, &post_form) {
@@ -151,6 +190,13 @@ impl Perform for Oper<CreatePost> {
       }
     };
 
+    let updated_post = match Post::update_ap_id(&conn, inserted_post.id) {
+      Ok(post) => post,
+      Err(_e) => return Err(APIError::err("couldnt_create_post").into()),
+    };
+
+    updated_post.send_create(&user, &conn)?;
+
     // They like their own post by default
     let like_form = PostLikeForm {
       post_id: inserted_post.id,
@@ -158,12 +204,13 @@ impl Perform for Oper<CreatePost> {
       score: 1,
     };
 
-    // Only add the like if the score isnt 0
     let _inserted_like = match PostLike::like(&conn, &like_form) {
       Ok(like) => like,
       Err(_e) => return Err(APIError::err("couldnt_like_post").into()),
     };
 
+    updated_post.send_like(&user, &conn)?;
+
     // Refetch the view
     let post_view = match PostView::read(&conn, inserted_post.id, Some(user_id)) {
       Ok(post) => post,
@@ -357,7 +404,8 @@ impl Perform for Oper<CreatePostLike> {
     }
 
     // Check for a site ban
-    if UserView::read(&conn, user_id)?.banned {
+    let user = User_::read(&conn, user_id)?;
+    if user.banned {
       return Err(APIError::err("site_ban").into());
     }
 
@@ -377,6 +425,14 @@ impl Perform for Oper<CreatePostLike> {
         Ok(like) => like,
         Err(_e) => return Err(APIError::err("couldnt_like_post").into()),
       };
+
+      if like_form.score == 1 {
+        post.send_like(&user, &conn)?;
+      } else if like_form.score == -1 {
+        post.send_dislike(&user, &conn)?;
+      }
+    } else {
+      post.send_undo_like(&user, &conn)?;
     }
 
     let post_view = match PostView::read(&conn, data.post_id, Some(user_id)) {
@@ -446,7 +502,8 @@ impl Perform for Oper<EditPost> {
     }
 
     // Check for a site ban
-    if UserView::read(&conn, user_id)?.banned {
+    let user = User_::read(&conn, user_id)?;
+    if user.banned {
       return Err(APIError::err("site_ban").into());
     }
 
@@ -454,6 +511,8 @@ impl Perform for Oper<EditPost> {
     let (iframely_title, iframely_description, iframely_html, pictrs_thumbnail) =
       fetch_iframely_and_pictrs_data(data.url.to_owned());
 
+    let read_post = Post::read(&conn, data.edit_id)?;
+
     let post_form = PostForm {
       name: data.name.to_owned(),
       url: data.url.to_owned(),
@@ -470,9 +529,12 @@ impl Perform for Oper<EditPost> {
       embed_description: iframely_description,
       embed_html: iframely_html,
       thumbnail_url: pictrs_thumbnail,
+      ap_id: read_post.ap_id,
+      local: read_post.local,
+      published: None,
     };
 
-    let _updated_post = match Post::update(&conn, data.edit_id, &post_form) {
+    let updated_post = match Post::update(&conn, data.edit_id, &post_form) {
       Ok(post) => post,
       Err(e) => {
         let err_type = if e.to_string() == "value too long for type character varying(200)" {
@@ -514,6 +576,22 @@ impl Perform for Oper<EditPost> {
       ModStickyPost::create(&conn, &form)?;
     }
 
+    if let Some(deleted) = data.deleted.to_owned() {
+      if deleted {
+        updated_post.send_delete(&user, &conn)?;
+      } else {
+        updated_post.send_undo_delete(&user, &conn)?;
+      }
+    } else if let Some(removed) = data.removed.to_owned() {
+      if removed {
+        updated_post.send_remove(&user, &conn)?;
+      } else {
+        updated_post.send_undo_remove(&user, &conn)?;
+      }
+    } else {
+      updated_post.send_update(&user, &conn)?;
+    }
+
     let post_view = PostView::read(&conn, data.edit_id, Some(user_id))?;
 
     let res = PostResponse { post: post_view };
index e05487dfb31f57c549330b9c5870a462cf90eff2..faee30cbb5fd5af1d0dc87e69212c5a075df7e1b 100644 (file)
@@ -1,5 +1,36 @@
 use super::user::Register;
-use super::*;
+use crate::{
+  api::{APIError, Oper, Perform},
+  apub::fetcher::search_by_apub_id,
+  db::{
+    category::*,
+    comment_view::*,
+    community_view::*,
+    moderator::*,
+    moderator_views::*,
+    post_view::*,
+    site::*,
+    site_view::*,
+    user::*,
+    user_view::*,
+    Crud,
+    SearchType,
+    SortType,
+  },
+  naive_now,
+  settings::Settings,
+  slur_check,
+  slurs_vec_to_str,
+  websocket::{server::SendAllMessage, UserOperation, WebsocketInfo},
+};
+use diesel::{
+  r2d2::{ConnectionManager, Pool},
+  PgConnection,
+};
+use failure::Error;
+use log::{debug, info};
+use serde::{Deserialize, Serialize};
+use std::str::FromStr;
 
 #[derive(Serialize, Deserialize)]
 pub struct ListCategories {}
@@ -9,7 +40,7 @@ pub struct ListCategoriesResponse {
   categories: Vec<Category>,
 }
 
-#[derive(Serialize, Deserialize)]
+#[derive(Serialize, Deserialize, Debug)]
 pub struct Search {
   q: String,
   type_: String,
@@ -20,13 +51,13 @@ pub struct Search {
   auth: Option<String>,
 }
 
-#[derive(Serialize, Deserialize)]
+#[derive(Serialize, Deserialize, Debug)]
 pub struct SearchResponse {
-  type_: String,
-  comments: Vec<CommentView>,
-  posts: Vec<PostView>,
-  communities: Vec<CommunityView>,
-  users: Vec<UserView>,
+  pub type_: String,
+  pub comments: Vec<CommentView>,
+  pub posts: Vec<PostView>,
+  pub communities: Vec<CommunityView>,
+  pub users: Vec<UserView>,
 }
 
 #[derive(Serialize, Deserialize)]
@@ -342,8 +373,7 @@ impl Perform for Oper<GetSite> {
     let conn = pool.get()?;
 
     // TODO refactor this a little
-    let site = Site::read(&conn, 1);
-    let site_view = if site.is_ok() {
+    let site_view = if let Ok(_site) = Site::read(&conn, 1) {
       Some(SiteView::read(&conn)?)
     } else if let Some(setup) = Settings::get().setup.as_ref() {
       let register = Register {
@@ -360,9 +390,9 @@ impl Perform for Oper<GetSite> {
       let create_site = CreateSite {
         name: setup.site_name.to_owned(),
         description: None,
-        enable_downvotes: false,
-        open_registration: false,
-        enable_nsfw: false,
+        enable_downvotes: true,
+        open_registration: true,
+        enable_nsfw: true,
         auth: login_response.jwt,
       };
       Oper::new(create_site).perform(pool, websocket_info.clone())?;
@@ -373,11 +403,16 @@ impl Perform for Oper<GetSite> {
     };
 
     let mut admins = UserView::admins(&conn)?;
-    if site_view.is_some() {
-      let site_creator_id = site_view.to_owned().unwrap().creator_id;
-      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);
+
+    // Make sure the site creator is the top admin
+    if let Some(site_view) = site_view.to_owned() {
+      let site_creator_id = site_view.creator_id;
+      // TODO investigate why this is sometimes coming back null
+      // Maybe user_.admin isn't being set to true?
+      if let Some(creator_index) = admins.iter().position(|r| r.id == site_creator_id) {
+        let creator_user = admins.remove(creator_index);
+        admins.insert(0, creator_user);
+      }
     }
 
     let banned = UserView::banned(&conn)?;
@@ -412,6 +447,15 @@ impl Perform for Oper<Search> {
   ) -> Result<SearchResponse, Error> {
     let data: &Search = &self.data;
 
+    dbg!(&data);
+
+    let conn = pool.get()?;
+
+    match search_by_apub_id(&data.q, &conn) {
+      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) => {
@@ -433,8 +477,6 @@ impl Perform for Oper<Search> {
 
     // TODO no clean / non-nsfw searching rn
 
-    let conn = pool.get()?;
-
     match type_ {
       SearchType::Posts => {
         posts = PostQueryBuilder::create(&conn)
index ee57723a1e6cb35888878b8c553d14343b39ea15..f68a1a82b7c1f48e642a2e7c96a5e075b589fec2 100644 (file)
@@ -1,6 +1,58 @@
-use super::*;
-use crate::is_valid_username;
+use crate::{
+  api::{APIError, Oper, Perform},
+  apub::{
+    extensions::signatures::generate_actor_keypair,
+    make_apub_endpoint,
+    ApubObjectType,
+    EndpointType,
+  },
+  db::{
+    comment::*,
+    comment_view::*,
+    community::*,
+    community_view::*,
+    moderator::*,
+    password_reset_request::*,
+    post::*,
+    post_view::*,
+    private_message::*,
+    private_message_view::*,
+    site::*,
+    site_view::*,
+    user::*,
+    user_mention::*,
+    user_mention_view::*,
+    user_view::*,
+    Crud,
+    Followable,
+    Joinable,
+    ListingType,
+    SortType,
+  },
+  generate_random_string,
+  is_valid_username,
+  naive_from_unix,
+  naive_now,
+  remove_slurs,
+  send_email,
+  settings::Settings,
+  slur_check,
+  slurs_vec_to_str,
+  websocket::{
+    server::{JoinUserRoom, SendAllMessage, SendUserRoomMessage},
+    UserOperation,
+    WebsocketInfo,
+  },
+};
 use bcrypt::verify;
+use diesel::{
+  r2d2::{ConnectionManager, Pool},
+  PgConnection,
+};
+use failure::Error;
+use log::error;
+use serde::{Deserialize, Serialize};
+use std::str::FromStr;
 
 #[derive(Serialize, Deserialize, Debug)]
 pub struct Login {
@@ -187,7 +239,7 @@ pub struct PrivateMessagesResponse {
 
 #[derive(Serialize, Deserialize, Clone)]
 pub struct PrivateMessageResponse {
-  message: PrivateMessageView,
+  pub message: PrivateMessageView,
 }
 
 #[derive(Serialize, Deserialize, Debug)]
@@ -262,6 +314,7 @@ impl Perform for Oper<Register> {
       return Err(APIError::err("admin_already_created").into());
     }
 
+    let user_keypair = generate_actor_keypair()?;
     if !is_valid_username(&data.username) {
       return Err(APIError::err("invalid_username").into());
     }
@@ -269,7 +322,6 @@ impl Perform for Oper<Register> {
     // Register the new user
     let user_form = UserForm {
       name: data.username.to_owned(),
-      fedi_name: Settings::get().hostname,
       email: data.email.to_owned(),
       matrix_user_id: None,
       avatar: None,
@@ -285,6 +337,12 @@ impl Perform for Oper<Register> {
       lang: "browser".into(),
       show_avatars: true,
       send_notifications_to_email: false,
+      actor_id: make_apub_endpoint(EndpointType::User, &data.username).to_string(),
+      bio: None,
+      local: true,
+      private_key: Some(user_keypair.private_key),
+      public_key: Some(user_keypair.public_key),
+      last_refreshed_at: None,
     };
 
     // Create the user
@@ -303,12 +361,15 @@ impl Perform for Oper<Register> {
       }
     };
 
+    let main_community_keypair = generate_actor_keypair()?;
+
     // Create the main community if it doesn't exist
     let main_community: Community = match Community::read(&conn, 2) {
       Ok(c) => c,
       Err(_e) => {
+        let default_community_name = "main";
         let community_form = CommunityForm {
-          name: "main".to_string(),
+          name: default_community_name.to_string(),
           title: "The Default Community".to_string(),
           description: Some("The Default Community".to_string()),
           category_id: 1,
@@ -317,6 +378,12 @@ impl Perform for Oper<Register> {
           removed: None,
           deleted: None,
           updated: None,
+          actor_id: make_apub_endpoint(EndpointType::Community, default_community_name).to_string(),
+          local: true,
+          private_key: Some(main_community_keypair.private_key),
+          public_key: Some(main_community_keypair.public_key),
+          last_refreshed_at: None,
+          published: None,
         };
         Community::create(&conn, &community_form).unwrap()
       }
@@ -411,7 +478,6 @@ impl Perform for Oper<SaveUserSettings> {
 
     let user_form = UserForm {
       name: read_user.name,
-      fedi_name: read_user.fedi_name,
       email,
       matrix_user_id: data.matrix_user_id.to_owned(),
       avatar: data.avatar.to_owned(),
@@ -427,6 +493,12 @@ impl Perform for Oper<SaveUserSettings> {
       lang: data.lang.to_owned(),
       show_avatars: data.show_avatars,
       send_notifications_to_email: data.send_notifications_to_email,
+      actor_id: read_user.actor_id,
+      bio: read_user.bio,
+      local: read_user.local,
+      private_key: read_user.private_key,
+      public_key: read_user.public_key,
+      last_refreshed_at: None,
     };
 
     let updated_user = match User_::update(&conn, user_id, &user_form) {
@@ -488,7 +560,7 @@ impl Perform for Oper<GetUserDetails> {
       None => {
         match User_::read_from_name(
           &conn,
-          data
+          &data
             .username
             .to_owned()
             .unwrap_or_else(|| "admin".to_string()),
@@ -580,30 +652,7 @@ impl Perform for Oper<AddAdmin> {
       return Err(APIError::err("not_an_admin").into());
     }
 
-    let read_user = User_::read(&conn, data.user_id)?;
-
-    // TODO make addadmin easier
-    let user_form = UserForm {
-      name: read_user.name,
-      fedi_name: read_user.fedi_name,
-      email: read_user.email,
-      matrix_user_id: read_user.matrix_user_id,
-      avatar: read_user.avatar,
-      password_encrypted: read_user.password_encrypted,
-      preferred_username: read_user.preferred_username,
-      updated: Some(naive_now()),
-      admin: data.added,
-      banned: read_user.banned,
-      show_nsfw: read_user.show_nsfw,
-      theme: read_user.theme,
-      default_sort_type: read_user.default_sort_type,
-      default_listing_type: read_user.default_listing_type,
-      lang: read_user.lang,
-      show_avatars: read_user.show_avatars,
-      send_notifications_to_email: read_user.send_notifications_to_email,
-    };
-
-    match User_::update(&conn, data.user_id, &user_form) {
+    match User_::add_admin(&conn, user_id, data.added) {
       Ok(user) => user,
       Err(_e) => return Err(APIError::err("couldnt_update_user").into()),
     };
@@ -661,30 +710,7 @@ impl Perform for Oper<BanUser> {
       return Err(APIError::err("not_an_admin").into());
     }
 
-    let read_user = User_::read(&conn, data.user_id)?;
-
-    // TODO make bans and addadmins easier
-    let user_form = UserForm {
-      name: read_user.name,
-      fedi_name: read_user.fedi_name,
-      email: read_user.email,
-      matrix_user_id: read_user.matrix_user_id,
-      avatar: read_user.avatar,
-      password_encrypted: read_user.password_encrypted,
-      preferred_username: read_user.preferred_username,
-      updated: Some(naive_now()),
-      admin: read_user.admin,
-      banned: data.ban,
-      show_nsfw: read_user.show_nsfw,
-      theme: read_user.theme,
-      default_sort_type: read_user.default_sort_type,
-      default_listing_type: read_user.default_listing_type,
-      lang: read_user.lang,
-      show_avatars: read_user.show_avatars,
-      send_notifications_to_email: read_user.send_notifications_to_email,
-    };
-
-    match User_::update(&conn, data.user_id, &user_form) {
+    match User_::ban_user(&conn, user_id, data.ban) {
       Ok(user) => user,
       Err(_e) => return Err(APIError::err("couldnt_update_user").into()),
     };
@@ -855,18 +881,7 @@ impl Perform for Oper<MarkAllAsRead> {
       .list()?;
 
     for reply in &replies {
-      let comment_form = CommentForm {
-        content: reply.to_owned().content,
-        parent_id: reply.to_owned().parent_id,
-        post_id: reply.to_owned().post_id,
-        creator_id: reply.to_owned().creator_id,
-        removed: None,
-        deleted: None,
-        read: Some(true),
-        updated: reply.to_owned().updated,
-      };
-
-      let _updated_comment = match Comment::update(&conn, reply.id, &comment_form) {
+      match Comment::mark_as_read(&conn, reply.id) {
         Ok(comment) => comment,
         Err(_e) => return Err(APIError::err("couldnt_update_comment").into()),
       };
@@ -902,12 +917,15 @@ impl Perform for Oper<MarkAllAsRead> {
 
     for message in &messages {
       let private_message_form = PrivateMessageForm {
-        content: None,
+        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 _updated_message = match PrivateMessage::update(&conn, message.id, &private_message_form)
@@ -955,18 +973,7 @@ impl Perform for Oper<DeleteAccount> {
       .list()?;
 
     for comment in &comments {
-      let comment_form = CommentForm {
-        content: "*Permananently Deleted*".to_string(),
-        parent_id: comment.to_owned().parent_id,
-        post_id: comment.to_owned().post_id,
-        creator_id: comment.to_owned().creator_id,
-        removed: None,
-        deleted: Some(true),
-        read: None,
-        updated: Some(naive_now()),
-      };
-
-      let _updated_comment = match Comment::update(&conn, comment.id, &comment_form) {
+      let _updated_comment = match Comment::permadelete(&conn, comment.id) {
         Ok(comment) => comment,
         Err(_e) => return Err(APIError::err("couldnt_update_comment").into()),
       };
@@ -980,25 +987,7 @@ impl Perform for Oper<DeleteAccount> {
       .list()?;
 
     for post in &posts {
-      let post_form = PostForm {
-        name: "*Permananently Deleted*".to_string(),
-        url: Some("https://deleted.com".to_string()),
-        body: Some("*Permananently Deleted*".to_string()),
-        creator_id: post.to_owned().creator_id,
-        community_id: post.to_owned().community_id,
-        removed: None,
-        deleted: Some(true),
-        nsfw: post.to_owned().nsfw,
-        locked: None,
-        stickied: None,
-        updated: Some(naive_now()),
-        embed_title: None,
-        embed_description: None,
-        embed_html: None,
-        thumbnail_url: None,
-      };
-
-      let _updated_post = match Post::update(&conn, post.id, &post_form) {
+      let _updated_post = match Post::permadelete(&conn, post.id) {
         Ok(post) => post,
         Err(_e) => return Err(APIError::err("couldnt_update_post").into()),
       };
@@ -1104,19 +1093,23 @@ impl Perform for Oper<CreatePrivateMessage> {
     let conn = pool.get()?;
 
     // Check for a site ban
-    if UserView::read(&conn, user_id)?.banned {
+    let user = User_::read(&conn, user_id)?;
+    if user.banned {
       return Err(APIError::err("site_ban").into());
     }
 
     let content_slurs_removed = remove_slurs(&data.content.to_owned());
 
     let private_message_form = PrivateMessageForm {
-      content: Some(content_slurs_removed.to_owned()),
+      content: content_slurs_removed.to_owned(),
       creator_id: user_id,
       recipient_id: data.recipient_id,
       deleted: None,
       read: None,
       updated: None,
+      ap_id: "changeme".into(),
+      local: true,
+      published: None,
     };
 
     let inserted_private_message = match PrivateMessage::create(&conn, &private_message_form) {
@@ -1126,6 +1119,14 @@ impl Perform for Oper<CreatePrivateMessage> {
       }
     };
 
+    let updated_private_message =
+      match PrivateMessage::update_ap_id(&conn, inserted_private_message.id) {
+        Ok(private_message) => private_message,
+        Err(_e) => return Err(APIError::err("couldnt_create_private_message").into()),
+      };
+
+    updated_private_message.send_create(&user, &conn)?;
+
     // Send notifications to the recipient
     let recipient_user = User_::read(&conn, data.recipient_id)?;
     if recipient_user.send_notifications_to_email {
@@ -1169,7 +1170,7 @@ impl Perform for Oper<EditPrivateMessage> {
   fn perform(
     &self,
     pool: Pool<ConnectionManager<PgConnection>>,
-    _websocket_info: Option<WebsocketInfo>,
+    websocket_info: Option<WebsocketInfo>,
   ) -> Result<PrivateMessageResponse, Error> {
     let data: &EditPrivateMessage = &self.data;
 
@@ -1185,7 +1186,8 @@ impl Perform for Oper<EditPrivateMessage> {
     let orig_private_message = PrivateMessage::read(&conn, data.edit_id)?;
 
     // Check for a site ban
-    if UserView::read(&conn, user_id)?.banned {
+    let user = User_::read(&conn, user_id)?;
+    if user.banned {
       return Err(APIError::err("site_ban").into());
     }
 
@@ -1197,8 +1199,8 @@ impl Perform for Oper<EditPrivateMessage> {
     }
 
     let content_slurs_removed = match &data.content {
-      Some(content) => Some(remove_slurs(content)),
-      None => None,
+      Some(content) => remove_slurs(content),
+      None => orig_private_message.content,
     };
 
     let private_message_form = PrivateMessageForm {
@@ -1212,17 +1214,41 @@ impl Perform for Oper<EditPrivateMessage> {
       } else {
         Some(naive_now())
       },
+      ap_id: orig_private_message.ap_id,
+      local: orig_private_message.local,
+      published: None,
     };
 
-    let _updated_private_message =
+    let updated_private_message =
       match PrivateMessage::update(&conn, data.edit_id, &private_message_form) {
         Ok(private_message) => private_message,
         Err(_e) => return Err(APIError::err("couldnt_update_private_message").into()),
       };
 
+    if let Some(deleted) = data.deleted.to_owned() {
+      if deleted {
+        updated_private_message.send_delete(&user, &conn)?;
+      } else {
+        updated_private_message.send_undo_delete(&user, &conn)?;
+      }
+    } else {
+      updated_private_message.send_update(&user, &conn)?;
+    }
+
     let message = PrivateMessageView::read(&conn, data.edit_id)?;
 
-    Ok(PrivateMessageResponse { message })
+    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,
+      });
+    }
+
+    Ok(res)
   }
 }
 
diff --git a/server/src/apub/activities.rs b/server/src/apub/activities.rs
new file mode 100644 (file)
index 0000000..3c4034c
--- /dev/null
@@ -0,0 +1,74 @@
+use crate::{
+  apub::{extensions::signatures::sign, is_apub_id_valid, ActorType},
+  db::{activity::insert_activity, community::Community, user::User_},
+};
+use activitystreams::{context, object::properties::ObjectProperties, public, Activity, Base};
+use diesel::PgConnection;
+use failure::{Error, _core::fmt::Debug};
+use log::debug;
+use serde::Serialize;
+use url::Url;
+
+pub fn populate_object_props(
+  props: &mut ObjectProperties,
+  addressed_ccs: Vec<String>,
+  object_id: &str,
+) -> Result<(), Error> {
+  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 fn send_activity_to_community<A>(
+  creator: &User_,
+  conn: &PgConnection,
+  community: &Community,
+  to: Vec<String>,
+  activity: A,
+) -> Result<(), Error>
+where
+  A: Activity + Base + Serialize + Debug,
+{
+  insert_activity(&conn, creator.id, &activity, true)?;
+
+  // if this is a local community, we need to do an announce from the community instead
+  if community.local {
+    Community::do_announce(activity, &community, creator, conn)?;
+  } else {
+    send_activity(&activity, creator, to)?;
+  }
+  Ok(())
+}
+
+/// Send an activity to a list of recipients, using the correct headers etc.
+pub fn send_activity<A>(activity: &A, actor: &dyn ActorType, to: Vec<String>) -> Result<(), Error>
+where
+  A: Serialize + Debug,
+{
+  let json = serde_json::to_string(&activity)?;
+  debug!("Sending activitypub activity {} to {:?}", json, 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 blacklisted)", t);
+      continue;
+    }
+    let mut request = attohttpc::post(t).header("Host", to_url.domain().unwrap());
+    let signature = sign(&mut request, actor)?;
+    let res = request
+      .header("Signature", signature)
+      .header("Content-Type", "application/json")
+      .text(json.to_owned())
+      .send()?
+      .text()?;
+
+    debug!("Result for activity send: {:?}", res);
+  }
+  Ok(())
+}
diff --git a/server/src/apub/comment.rs b/server/src/apub/comment.rs
new file mode 100644 (file)
index 0000000..0a513f3
--- /dev/null
@@ -0,0 +1,510 @@
+use crate::{
+  apub::{
+    activities::{populate_object_props, send_activity_to_community},
+    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,
+    },
+    ActorType,
+    ApubLikeableType,
+    ApubObjectType,
+    FromApub,
+    ToApub,
+  },
+  convert_datetime,
+  db::{
+    comment::{Comment, CommentForm},
+    community::Community,
+    post::Post,
+    user::User_,
+    Crud,
+  },
+  routes::DbPoolParam,
+  scrape_text_for_mentions,
+  MentionData,
+};
+use activitystreams::{
+  activity::{Create, Delete, Dislike, Like, Remove, Undo, Update},
+  context,
+  link::Mention,
+  object::{kind::NoteType, properties::ObjectProperties, Note},
+};
+use activitystreams_new::object::Tombstone;
+use actix_web::{body::Body, web::Path, HttpResponse, Result};
+use diesel::PgConnection;
+use failure::Error;
+use itertools::Itertools;
+use log::debug;
+use serde::Deserialize;
+
+#[derive(Deserialize)]
+pub struct CommentQuery {
+  comment_id: String,
+}
+
+/// Return the post json over HTTP.
+pub async fn get_apub_comment(
+  info: Path<CommentQuery>,
+  db: DbPoolParam,
+) -> Result<HttpResponse<Body>, Error> {
+  let id = info.comment_id.parse::<i32>()?;
+  let comment = Comment::read(&&db.get()?, id)?;
+  if !comment.deleted {
+    Ok(create_apub_response(&comment.to_apub(&db.get().unwrap())?))
+  } else {
+    Ok(create_apub_tombstone_response(&comment.to_tombstone()?))
+  }
+}
+
+impl ToApub for Comment {
+  type Response = Note;
+
+  fn to_apub(&self, conn: &PgConnection) -> Result<Note, Error> {
+    let mut comment = Note::default();
+    let oprops: &mut ObjectProperties = comment.as_mut();
+    let creator = User_::read(&conn, self.creator_id)?;
+    let post = Post::read(&conn, self.post_id)?;
+    let community = Community::read(&conn, post.community_id)?;
+
+    // Add a vector containing some important info to the "in_reply_to" field
+    // [post_ap_id, Option(parent_comment_ap_id)]
+    let mut in_reply_to_vec = vec![post.ap_id];
+
+    if let Some(parent_id) = self.parent_id {
+      let parent_comment = Comment::read(&conn, parent_id)?;
+      in_reply_to_vec.push(parent_comment.ap_id);
+    }
+
+    oprops
+      // 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)?;
+
+    if let Some(u) = self.updated {
+      oprops.set_updated(convert_datetime(u))?;
+    }
+
+    Ok(comment)
+  }
+
+  fn to_tombstone(&self) -> Result<Tombstone, Error> {
+    create_tombstone(
+      self.deleted,
+      &self.ap_id,
+      self.updated,
+      NoteType.to_string(),
+    )
+  }
+}
+
+impl FromApub for CommentForm {
+  type ApubType = Note;
+
+  /// Parse an ActivityPub note received from another instance into a Lemmy comment
+  fn from_apub(note: &Note, conn: &PgConnection) -> Result<CommentForm, Error> {
+    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, &conn)?;
+
+    let mut in_reply_tos = oprops.get_many_in_reply_to_xsd_any_uris().unwrap();
+    let post_ap_id = in_reply_tos.next().unwrap().to_string();
+
+    // This post, or the parent comment might not yet exist on this server yet, fetch them.
+    let post = get_or_fetch_and_insert_remote_post(&post_ap_id, &conn)?;
+
+    // The 2nd item, if it exists, is the parent comment apub_id
+    // For deeply nested comments, FromApub automatically gets called recursively
+    let parent_id: Option<i32> = match in_reply_tos.next() {
+      Some(parent_comment_uri) => {
+        let parent_comment_ap_id = &parent_comment_uri.to_string();
+        let parent_comment = get_or_fetch_and_insert_remote_comment(&parent_comment_ap_id, &conn)?;
+
+        Some(parent_comment.id)
+      }
+      None => None,
+    };
+
+    Ok(CommentForm {
+      creator_id: creator.id,
+      post_id: post.id,
+      parent_id,
+      content: oprops
+        .get_content_xsd_string()
+        .map(|c| c.to_string())
+        .unwrap(),
+      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()),
+      deleted: None,
+      ap_id: oprops.get_id().unwrap().to_string(),
+      local: false,
+    })
+  }
+}
+
+impl ApubObjectType for Comment {
+  /// Send out information about a newly created comment, to the followers of the community.
+  fn send_create(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error> {
+    let note = self.to_apub(conn)?;
+    let post = Post::read(&conn, self.post_id)?;
+    let community = Community::read(conn, post.community_id)?;
+    let id = format!("{}/create/{}", self.ap_id, uuid::Uuid::new_v4());
+
+    let maa: MentionsAndAddresses =
+      collect_non_local_mentions_and_addresses(&conn, &self.content, &community)?;
+
+    let mut create = Create::new();
+    populate_object_props(&mut create.object_props, maa.addressed_ccs, &id)?;
+
+    // Set the mention tags
+    create.object_props.set_many_tag_base_boxes(maa.tags)?;
+
+    create
+      .create_props
+      .set_actor_xsd_any_uri(creator.actor_id.to_owned())?
+      .set_object_base_box(note)?;
+
+    send_activity_to_community(&creator, &conn, &community, maa.inboxes, create)?;
+    Ok(())
+  }
+
+  /// Send out information about an edited post, to the followers of the community.
+  fn send_update(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error> {
+    let note = self.to_apub(&conn)?;
+    let post = Post::read(&conn, self.post_id)?;
+    let community = Community::read(&conn, post.community_id)?;
+    let id = format!("{}/update/{}", self.ap_id, uuid::Uuid::new_v4());
+
+    let maa: MentionsAndAddresses =
+      collect_non_local_mentions_and_addresses(&conn, &self.content, &community)?;
+
+    let mut update = Update::new();
+    populate_object_props(&mut update.object_props, maa.addressed_ccs, &id)?;
+
+    // Set the mention tags
+    update.object_props.set_many_tag_base_boxes(maa.tags)?;
+
+    update
+      .update_props
+      .set_actor_xsd_any_uri(creator.actor_id.to_owned())?
+      .set_object_base_box(note)?;
+
+    send_activity_to_community(&creator, &conn, &community, maa.inboxes, update)?;
+    Ok(())
+  }
+
+  fn send_delete(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error> {
+    let note = self.to_apub(&conn)?;
+    let post = Post::read(&conn, self.post_id)?;
+    let community = Community::read(&conn, post.community_id)?;
+    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,
+    )?;
+
+    delete
+      .delete_props
+      .set_actor_xsd_any_uri(creator.actor_id.to_owned())?
+      .set_object_base_box(note)?;
+
+    send_activity_to_community(
+      &creator,
+      &conn,
+      &community,
+      vec![community.get_shared_inbox_url()],
+      delete,
+    )?;
+    Ok(())
+  }
+
+  fn send_undo_delete(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error> {
+    let note = self.to_apub(&conn)?;
+    let post = Post::read(&conn, self.post_id)?;
+    let community = Community::read(&conn, post.community_id)?;
+
+    // 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,
+    )?;
+
+    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();
+
+    populate_object_props(
+      &mut undo.object_props,
+      vec![community.get_followers_url()],
+      &undo_id,
+    )?;
+
+    undo
+      .undo_props
+      .set_actor_xsd_any_uri(creator.actor_id.to_owned())?
+      .set_object_base_box(delete)?;
+
+    send_activity_to_community(
+      &creator,
+      &conn,
+      &community,
+      vec![community.get_shared_inbox_url()],
+      undo,
+    )?;
+    Ok(())
+  }
+
+  fn send_remove(&self, mod_: &User_, conn: &PgConnection) -> Result<(), Error> {
+    let note = self.to_apub(&conn)?;
+    let post = Post::read(&conn, self.post_id)?;
+    let community = Community::read(&conn, post.community_id)?;
+    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,
+    )?;
+
+    remove
+      .remove_props
+      .set_actor_xsd_any_uri(mod_.actor_id.to_owned())?
+      .set_object_base_box(note)?;
+
+    send_activity_to_community(
+      &mod_,
+      &conn,
+      &community,
+      vec![community.get_shared_inbox_url()],
+      remove,
+    )?;
+    Ok(())
+  }
+
+  fn send_undo_remove(&self, mod_: &User_, conn: &PgConnection) -> Result<(), Error> {
+    let note = self.to_apub(&conn)?;
+    let post = Post::read(&conn, self.post_id)?;
+    let community = Community::read(&conn, post.community_id)?;
+
+    // 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,
+    )?;
+
+    remove
+      .remove_props
+      .set_actor_xsd_any_uri(mod_.actor_id.to_owned())?
+      .set_object_base_box(note)?;
+
+    // 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,
+    )?;
+
+    undo
+      .undo_props
+      .set_actor_xsd_any_uri(mod_.actor_id.to_owned())?
+      .set_object_base_box(remove)?;
+
+    send_activity_to_community(
+      &mod_,
+      &conn,
+      &community,
+      vec![community.get_shared_inbox_url()],
+      undo,
+    )?;
+    Ok(())
+  }
+}
+
+impl ApubLikeableType for Comment {
+  fn send_like(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error> {
+    let note = self.to_apub(&conn)?;
+    let post = Post::read(&conn, self.post_id)?;
+    let community = Community::read(&conn, post.community_id)?;
+    let id = format!("{}/like/{}", self.ap_id, uuid::Uuid::new_v4());
+
+    let mut like = Like::new();
+    populate_object_props(
+      &mut like.object_props,
+      vec![community.get_followers_url()],
+      &id,
+    )?;
+    like
+      .like_props
+      .set_actor_xsd_any_uri(creator.actor_id.to_owned())?
+      .set_object_base_box(note)?;
+
+    send_activity_to_community(
+      &creator,
+      &conn,
+      &community,
+      vec![community.get_shared_inbox_url()],
+      like,
+    )?;
+    Ok(())
+  }
+
+  fn send_dislike(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error> {
+    let note = self.to_apub(&conn)?;
+    let post = Post::read(&conn, self.post_id)?;
+    let community = Community::read(&conn, post.community_id)?;
+    let id = format!("{}/dislike/{}", self.ap_id, uuid::Uuid::new_v4());
+
+    let mut dislike = Dislike::new();
+    populate_object_props(
+      &mut dislike.object_props,
+      vec![community.get_followers_url()],
+      &id,
+    )?;
+    dislike
+      .dislike_props
+      .set_actor_xsd_any_uri(creator.actor_id.to_owned())?
+      .set_object_base_box(note)?;
+
+    send_activity_to_community(
+      &creator,
+      &conn,
+      &community,
+      vec![community.get_shared_inbox_url()],
+      dislike,
+    )?;
+    Ok(())
+  }
+
+  fn send_undo_like(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error> {
+    let note = self.to_apub(&conn)?;
+    let post = Post::read(&conn, self.post_id)?;
+    let community = Community::read(&conn, post.community_id)?;
+    let id = format!("{}/dislike/{}", self.ap_id, uuid::Uuid::new_v4());
+
+    let mut like = Like::new();
+    populate_object_props(
+      &mut like.object_props,
+      vec![community.get_followers_url()],
+      &id,
+    )?;
+    like
+      .like_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/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,
+    )?;
+
+    undo
+      .undo_props
+      .set_actor_xsd_any_uri(creator.actor_id.to_owned())?
+      .set_object_base_box(like)?;
+
+    send_activity_to_community(
+      &creator,
+      &conn,
+      &community,
+      vec![community.get_shared_inbox_url()],
+      undo,
+    )?;
+    Ok(())
+  }
+}
+
+struct MentionsAndAddresses {
+  addressed_ccs: Vec<String>,
+  inboxes: Vec<String>,
+  tags: Vec<Mention>,
+}
+
+/// This takes a comment, and builds a list of to_addresses, inboxes,
+/// and mention tags, so they know where to be sent to.
+/// Addresses are the users / addresses that go in the cc field.
+fn collect_non_local_mentions_and_addresses(
+  conn: &PgConnection,
+  content: &str,
+  community: &Community,
+) -> Result<MentionsAndAddresses, Error> {
+  let mut addressed_ccs = vec![community.get_followers_url()];
+
+  // Add the mention tag
+  let mut tags = Vec::new();
+
+  // Get the inboxes for any mentions
+  let mentions = scrape_text_for_mentions(&content)
+    .into_iter()
+    // Filter only the non-local ones
+    .filter(|m| !m.is_local())
+    .collect::<Vec<MentionData>>();
+  let mut mention_inboxes = Vec::new();
+  for mention in &mentions {
+    // TODO should it be fetching it every time?
+    if let Ok(actor_id) = fetch_webfinger_url(mention) {
+      debug!("mention actor_id: {}", actor_id);
+      addressed_ccs.push(actor_id.to_owned());
+      let mention_user = get_or_fetch_and_upsert_remote_user(&actor_id, &conn)?;
+      let 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())?;
+      tags.push(mention_tag);
+    }
+  }
+
+  let mut inboxes = vec![community.get_shared_inbox_url()];
+  inboxes.extend(mention_inboxes);
+  inboxes = inboxes.into_iter().unique().collect();
+
+  Ok(MentionsAndAddresses {
+    addressed_ccs,
+    inboxes,
+    tags,
+  })
+}
index 32f14eeb28f7309b6832c9b51555f4aa7027505c..8c8c3b2809379d2b3337b0bdbd8f020400e34f1d 100644 (file)
-use crate::apub::make_apub_endpoint;
-use crate::db::community::Community;
-use crate::db::community_view::CommunityFollowerView;
-use crate::db::establish_unpooled_connection;
-use crate::to_datetime_utc;
-use activitypub::{actor::Group, collection::UnorderedCollection, context};
-use actix_web::body::Body;
-use actix_web::web::Path;
-use actix_web::HttpResponse;
-use serde::Deserialize;
+use crate::{
+  apub::{
+    activities::{populate_object_props, send_activity},
+    create_apub_response,
+    create_apub_tombstone_response,
+    create_tombstone,
+    extensions::{group_extensions::GroupExtension, signatures::PublicKey},
+    fetcher::get_or_fetch_and_upsert_remote_user,
+    get_shared_inbox,
+    ActorType,
+    FromApub,
+    GroupExt,
+    ToApub,
+  },
+  convert_datetime,
+  db::{
+    activity::insert_activity,
+    community::{Community, CommunityForm},
+    community_view::{CommunityFollowerView, CommunityModeratorView},
+    user::User_,
+  },
+  naive_now,
+  routes::DbPoolParam,
+};
+use activitystreams::{
+  activity::{Accept, Announce, Delete, Remove, Undo},
+  actor::{kind::GroupType, properties::ApActorProperties, Group},
+  collection::UnorderedCollection,
+  context,
+  endpoint::EndpointProperties,
+  object::properties::ObjectProperties,
+  Activity,
+  Base,
+  BaseBox,
+};
+use activitystreams_ext::Ext3;
+use activitystreams_new::{activity::Follow, object::Tombstone};
+use actix_web::{body::Body, web::Path, HttpResponse, Result};
+use diesel::PgConnection;
+use failure::{Error, _core::fmt::Debug};
+use itertools::Itertools;
+use serde::{Deserialize, Serialize};
 
-impl Community {
-  pub fn as_group(&self) -> Group {
-    let base_url = make_apub_endpoint("c", &self.name);
+#[derive(Deserialize)]
+pub struct CommunityQuery {
+  community_name: String,
+}
+
+impl ToApub for Community {
+  type Response = GroupExt;
 
+  // Turn a Lemmy Community into an ActivityPub group that can be sent out over the network.
+  fn to_apub(&self, conn: &PgConnection) -> Result<GroupExt, Error> {
     let mut group = Group::default();
+    let oprops: &mut ObjectProperties = group.as_mut();
 
-    group.object_props.set_context_object(context()).ok();
-    group.object_props.set_id_string(base_url.to_string()).ok();
-    group
-      .object_props
-      .set_name_string(self.name.to_owned())
-      .ok();
-    group
-      .object_props
-      .set_published_utctime(to_datetime_utc(self.published))
-      .ok();
-    if let Some(updated) = self.updated {
-      group
-        .object_props
-        .set_updated_utctime(to_datetime_utc(updated))
-        .ok();
-    }
+    // The attributed to, is an ordered vector with the creator actor_ids first,
+    // then the rest of the moderators
+    // TODO Technically the instance admins can mod the community, but lets
+    // ignore that for now
+    let moderators = CommunityModeratorView::for_community(&conn, self.id)?
+      .into_iter()
+      .map(|m| m.user_actor_id)
+      .collect();
+
+    oprops
+      .set_context_xsd_any_uri(context())?
+      .set_id(self.actor_id.to_owned())?
+      .set_name_xsd_string(self.name.to_owned())?
+      .set_published(convert_datetime(self.published))?
+      .set_many_attributed_to_xsd_any_uris(moderators)?;
 
-    if let Some(description) = &self.description {
-      group
-        .object_props
-        .set_summary_string(description.to_string())
-        .ok();
+    if let Some(u) = self.updated.to_owned() {
+      oprops.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
+      //       -> same for post.content and others
+      oprops.set_content_xsd_string(d)?;
     }
 
-    group
-      .ap_actor_props
-      .set_inbox_string(format!("{}/inbox", &base_url))
-      .ok();
-    group
-      .ap_actor_props
-      .set_outbox_string(format!("{}/outbox", &base_url))
-      .ok();
-    group
-      .ap_actor_props
-      .set_followers_string(format!("{}/followers", &base_url))
-      .ok();
+    let mut endpoint_props = EndpointProperties::default();
 
-    group
-  }
+    endpoint_props.set_shared_inbox(self.get_shared_inbox_url())?;
 
-  pub fn followers_as_collection(&self) -> UnorderedCollection {
-    let base_url = make_apub_endpoint("c", &self.name);
+    let mut actor_props = ApActorProperties::default();
 
-    let mut collection = UnorderedCollection::default();
-    collection.object_props.set_context_object(context()).ok();
-    collection.object_props.set_id_string(base_url).ok();
+    actor_props
+      .set_preferred_username(self.title.to_owned())?
+      .set_inbox(self.get_inbox_url())?
+      .set_outbox(self.get_outbox_url())?
+      .set_endpoints(endpoint_props)?
+      .set_followers(self.get_followers_url())?;
 
-    let connection = establish_unpooled_connection();
-    //As we are an object, we validated that the community id was valid
-    let community_followers = CommunityFollowerView::for_community(&connection, self.id).unwrap();
+    let group_extension = GroupExtension::new(conn, self.category_id, self.nsfw)?;
 
-    let ap_followers = community_followers
-      .iter()
-      .map(|follower| make_apub_endpoint("u", &follower.user_name))
-      .collect();
+    Ok(Ext3::new(
+      group,
+      group_extension,
+      actor_props,
+      self.get_public_key_ext(),
+    ))
+  }
 
-    collection
-      .collection_props
-      .set_items_string_vec(ap_followers)
-      .unwrap();
-    collection
+  fn to_tombstone(&self) -> Result<Tombstone, Error> {
+    create_tombstone(
+      self.deleted,
+      &self.actor_id,
+      self.updated,
+      GroupType.to_string(),
+    )
   }
 }
 
-#[derive(Deserialize)]
-pub struct CommunityQuery {
-  community_name: String,
+impl ActorType for Community {
+  fn actor_id(&self) -> String {
+    self.actor_id.to_owned()
+  }
+
+  fn public_key(&self) -> String {
+    self.public_key.to_owned().unwrap()
+  }
+  fn private_key(&self) -> String {
+    self.private_key.to_owned().unwrap()
+  }
+
+  /// As a local community, accept the follow request from a remote user.
+  fn send_accept_follow(&self, follow: &Follow, conn: &PgConnection) -> Result<(), Error> {
+    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)?;
+    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);
+
+    insert_activity(&conn, self.creator_id, &accept, true)?;
+
+    send_activity(&accept, self, vec![to])?;
+    Ok(())
+  }
+
+  fn send_delete(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error> {
+    let group = self.to_apub(conn)?;
+    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,
+    )?;
+
+    delete
+      .delete_props
+      .set_actor_xsd_any_uri(creator.actor_id.to_owned())?
+      .set_object_base_box(BaseBox::from_concrete(group)?)?;
+
+    insert_activity(&conn, self.creator_id, &delete, true)?;
+
+    // Note: For an accept, since it was automatic, no one pushed a button,
+    // the community was the actor.
+    // But for delete, the creator is the actor, and does the signing
+    send_activity(&delete, creator, self.get_follower_inboxes(&conn)?)?;
+    Ok(())
+  }
+
+  fn send_undo_delete(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error> {
+    let group = self.to_apub(conn)?;
+    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,
+    )?;
+
+    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,
+    )?;
+
+    undo
+      .undo_props
+      .set_actor_xsd_any_uri(creator.actor_id.to_owned())?
+      .set_object_base_box(delete)?;
+
+    insert_activity(&conn, self.creator_id, &undo, true)?;
+
+    // Note: For an accept, since it was automatic, no one pushed a button,
+    // the community was the actor.
+    // But for delete, the creator is the actor, and does the signing
+    send_activity(&undo, creator, self.get_follower_inboxes(&conn)?)?;
+    Ok(())
+  }
+
+  fn send_remove(&self, mod_: &User_, conn: &PgConnection) -> Result<(), Error> {
+    let group = self.to_apub(conn)?;
+    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,
+    )?;
+
+    remove
+      .remove_props
+      .set_actor_xsd_any_uri(mod_.actor_id.to_owned())?
+      .set_object_base_box(BaseBox::from_concrete(group)?)?;
+
+    insert_activity(&conn, mod_.id, &remove, true)?;
+
+    // Note: For an accept, since it was automatic, no one pushed a button,
+    // the community was the actor.
+    // But for delete, the creator is the actor, and does the signing
+    send_activity(&remove, mod_, self.get_follower_inboxes(&conn)?)?;
+    Ok(())
+  }
+
+  fn send_undo_remove(&self, mod_: &User_, conn: &PgConnection) -> Result<(), Error> {
+    let group = self.to_apub(conn)?;
+    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,
+    )?;
+
+    remove
+      .remove_props
+      .set_actor_xsd_any_uri(mod_.actor_id.to_owned())?
+      .set_object_base_box(BaseBox::from_concrete(group)?)?;
+
+    // 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,
+    )?;
+
+    undo
+      .undo_props
+      .set_actor_xsd_any_uri(mod_.actor_id.to_owned())?
+      .set_object_base_box(remove)?;
+
+    insert_activity(&conn, mod_.id, &undo, true)?;
+
+    // Note: For an accept, since it was automatic, no one pushed a button,
+    // the community was the actor.
+    // But for remove , the creator is the actor, and does the signing
+    send_activity(&undo, mod_, self.get_follower_inboxes(&conn)?)?;
+    Ok(())
+  }
+
+  /// For a given community, returns the inboxes of all followers.
+  fn get_follower_inboxes(&self, conn: &PgConnection) -> Result<Vec<String>, Error> {
+    Ok(
+      CommunityFollowerView::for_community(conn, self.id)?
+        .into_iter()
+        .map(|c| get_shared_inbox(&c.user_actor_id))
+        .filter(|s| !s.is_empty())
+        .unique()
+        .collect(),
+    )
+  }
+
+  fn send_follow(&self, _follow_actor_id: &str, _conn: &PgConnection) -> Result<(), Error> {
+    unimplemented!()
+  }
+
+  fn send_unfollow(&self, _follow_actor_id: &str, _conn: &PgConnection) -> Result<(), Error> {
+    unimplemented!()
+  }
 }
 
-pub async fn get_apub_community(info: Path<CommunityQuery>) -> HttpResponse<Body> {
-  let connection = establish_unpooled_connection();
+impl FromApub for CommunityForm {
+  type ApubType = GroupExt;
+
+  /// Parse an ActivityPub group received from another instance into a Lemmy community.
+  fn from_apub(group: &GroupExt, conn: &PgConnection) -> Result<Self, Error> {
+    let group_extensions: &GroupExtension = &group.ext_one;
+    let oprops = &group.inner.object_props;
+    let aprops = &group.ext_two;
+    let public_key: &PublicKey = &group.ext_three.public_key;
+
+    let mut creator_and_moderator_uris = oprops.get_many_attributed_to_xsd_any_uris().unwrap();
+    let creator = creator_and_moderator_uris
+      .next()
+      .map(|c| get_or_fetch_and_upsert_remote_user(&c.to_string(), &conn).unwrap())
+      .unwrap();
+
+    Ok(CommunityForm {
+      name: oprops.get_name_xsd_string().unwrap().to_string(),
+      title: aprops.get_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: oprops.get_content_xsd_string().map(|s| s.to_string()),
+      category_id: group_extensions.category.identifier.parse::<i32>()?,
+      creator_id: creator.id,
+      removed: 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()),
+      deleted: None,
+      nsfw: group_extensions.sensitive,
+      actor_id: oprops.get_id().unwrap().to_string(),
+      local: false,
+      private_key: None,
+      public_key: Some(public_key.to_owned().public_key_pem),
+      last_refreshed_at: Some(naive_now()),
+    })
+  }
+}
 
-  if let Ok(community) = Community::read_from_name(&connection, info.community_name.to_owned()) {
-    HttpResponse::Ok()
-      .content_type("application/activity+json")
-      .body(serde_json::to_string(&community.as_group()).unwrap())
+/// Return the community json over HTTP.
+pub async fn get_apub_community_http(
+  info: Path<CommunityQuery>,
+  db: DbPoolParam,
+) -> Result<HttpResponse<Body>, Error> {
+  let community = Community::read_from_name(&&db.get()?, &info.community_name)?;
+  if !community.deleted {
+    Ok(create_apub_response(
+      &community.to_apub(&db.get().unwrap())?,
+    ))
   } else {
-    HttpResponse::NotFound().finish()
+    Ok(create_apub_tombstone_response(&community.to_tombstone()?))
   }
 }
 
-pub async fn get_apub_community_followers(info: Path<CommunityQuery>) -> HttpResponse<Body> {
-  let connection = establish_unpooled_connection();
+/// Returns an empty followers collection, only populating the size (for privacy).
+pub async fn get_apub_community_followers(
+  info: Path<CommunityQuery>,
+  db: DbPoolParam,
+) -> Result<HttpResponse<Body>, Error> {
+  let community = Community::read_from_name(&&db.get()?, &info.community_name)?;
 
-  if let Ok(community) = Community::read_from_name(&connection, info.community_name.to_owned()) {
-    HttpResponse::Ok()
-      .content_type("application/activity+json")
-      .body(serde_json::to_string(&community.followers_as_collection()).unwrap())
-  } else {
-    HttpResponse::NotFound().finish()
+  let conn = db.get()?;
+
+  //As we are an object, we validated that the community id was valid
+  let community_followers = CommunityFollowerView::for_community(&conn, community.id).unwrap();
+
+  let mut collection = UnorderedCollection::default();
+  let oprops: &mut ObjectProperties = collection.as_mut();
+  oprops
+    .set_context_xsd_any_uri(context())?
+    .set_id(community.actor_id)?;
+  collection
+    .collection_props
+    .set_total_items(community_followers.len() as u64)?;
+  Ok(create_apub_response(&collection))
+}
+
+impl Community {
+  pub fn do_announce<A>(
+    activity: A,
+    community: &Community,
+    sender: &dyn ActorType,
+    conn: &PgConnection,
+  ) -> Result<HttpResponse, Error>
+  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()),
+    )?;
+    announce
+      .announce_props
+      .set_actor_xsd_any_uri(community.actor_id.to_owned())?
+      .set_object_base_box(BaseBox::from_concrete(activity)?)?;
+
+    insert_activity(&conn, community.creator_id, &announce, true)?;
+
+    // dont send to the instance where the activity originally came from, because that would result
+    // in a database error (same data inserted twice)
+    let mut to = community.get_follower_inboxes(&conn)?;
+    // this seems to be the "easiest" stable alternative for remove_item()
+    to.retain(|x| *x != sender.get_shared_inbox_url());
+
+    send_activity(&announce, community, to)?;
+
+    Ok(HttpResponse::Ok().finish())
   }
 }
diff --git a/server/src/apub/community_inbox.rs b/server/src/apub/community_inbox.rs
new file mode 100644 (file)
index 0000000..975f268
--- /dev/null
@@ -0,0 +1,123 @@
+use crate::{
+  apub::{
+    extensions::signatures::verify,
+    fetcher::{get_or_fetch_and_upsert_remote_community, get_or_fetch_and_upsert_remote_user},
+    ActorType,
+  },
+  db::{
+    activity::insert_activity,
+    community::{Community, CommunityFollower, CommunityFollowerForm},
+    user::User_,
+    Followable,
+  },
+  routes::{ChatServerParam, DbPoolParam},
+};
+use activitystreams::activity::Undo;
+use activitystreams_new::activity::Follow;
+use actix_web::{web, HttpRequest, HttpResponse, Result};
+use diesel::PgConnection;
+use failure::{Error, _core::fmt::Debug};
+use log::debug;
+use serde::Deserialize;
+
+#[serde(untagged)]
+#[derive(Deserialize, Debug)]
+pub enum CommunityAcceptedObjects {
+  Follow(Follow),
+  Undo(Undo),
+}
+
+impl CommunityAcceptedObjects {
+  fn follow(&self) -> Result<Follow, Error> {
+    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,
+  _chat_server: ChatServerParam,
+) -> Result<HttpResponse, Error> {
+  let input = input.into_inner();
+  let conn = db.get()?;
+  let community = Community::read_from_name(&conn, &path.into_inner())?;
+  if !community.local {
+    return Err(format_err!(
+      "Received activity is addressed to remote community {}",
+      &community.actor_id
+    ));
+  }
+  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 conn = db.get()?;
+
+  let user = get_or_fetch_and_upsert_remote_user(&user_uri, &conn)?;
+  let community = get_or_fetch_and_upsert_remote_community(&community_uri, &conn)?;
+
+  verify(&request, &user)?;
+
+  match input {
+    CommunityAcceptedObjects::Follow(f) => handle_follow(&f, &user, &community, &conn),
+    CommunityAcceptedObjects::Undo(u) => handle_undo_follow(&u, &user, &community, &conn),
+  }
+}
+
+/// Handle a follow request from a remote user, adding it to the local database and returning an
+/// Accept activity.
+fn handle_follow(
+  follow: &Follow,
+  user: &User_,
+  community: &Community,
+  conn: &PgConnection,
+) -> Result<HttpResponse, Error> {
+  insert_activity(&conn, user.id, &follow, false)?;
+
+  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.
+  CommunityFollower::follow(&conn, &community_follower_form).ok();
+
+  community.send_accept_follow(&follow, &conn)?;
+
+  Ok(HttpResponse::Ok().finish())
+}
+
+fn handle_undo_follow(
+  undo: &Undo,
+  user: &User_,
+  community: &Community,
+  conn: &PgConnection,
+) -> Result<HttpResponse, Error> {
+  insert_activity(&conn, user.id, &undo, false)?;
+
+  let community_follower_form = CommunityFollowerForm {
+    community_id: community.id,
+    user_id: user.id,
+  };
+
+  CommunityFollower::unfollow(&conn, &community_follower_form).ok();
+
+  Ok(HttpResponse::Ok().finish())
+}
diff --git a/server/src/apub/extensions/group_extensions.rs b/server/src/apub/extensions/group_extensions.rs
new file mode 100644 (file)
index 0000000..ece9770
--- /dev/null
@@ -0,0 +1,40 @@
+use crate::db::{category::Category, Crud};
+use activitystreams::{ext::Extension, Actor};
+use diesel::PgConnection;
+use failure::Error;
+use serde::{Deserialize, Serialize};
+
+#[derive(Clone, Debug, Default, Deserialize, Serialize)]
+#[serde(rename_all = "camelCase")]
+pub struct GroupExtension {
+  pub category: GroupCategory,
+  pub sensitive: bool,
+}
+
+#[derive(Clone, Debug, Default, Deserialize, Serialize)]
+#[serde(rename_all = "camelCase")]
+pub struct GroupCategory {
+  // Using a string because that's how Peertube does it.
+  pub identifier: String,
+  pub name: String,
+}
+
+impl GroupExtension {
+  pub fn new(
+    conn: &PgConnection,
+    category_id: i32,
+    sensitive: bool,
+  ) -> Result<GroupExtension, Error> {
+    let category = Category::read(conn, category_id)?;
+    let group_category = GroupCategory {
+      identifier: category_id.to_string(),
+      name: category.name,
+    };
+    Ok(GroupExtension {
+      category: group_category,
+      sensitive,
+    })
+  }
+}
+
+impl<T> Extension<T> for GroupExtension where T: Actor {}
diff --git a/server/src/apub/extensions/mod.rs b/server/src/apub/extensions/mod.rs
new file mode 100644 (file)
index 0000000..fdd06e5
--- /dev/null
@@ -0,0 +1,3 @@
+pub mod group_extensions;
+pub mod page_extension;
+pub mod signatures;
diff --git a/server/src/apub/extensions/page_extension.rs b/server/src/apub/extensions/page_extension.rs
new file mode 100644 (file)
index 0000000..484807e
--- /dev/null
@@ -0,0 +1,11 @@
+use activitystreams::{ext::Extension, Base};
+use serde::{Deserialize, Serialize};
+
+#[derive(Clone, Debug, Default, Deserialize, Serialize)]
+#[serde(rename_all = "camelCase")]
+pub struct PageExtension {
+  pub comments_enabled: bool,
+  pub sensitive: bool,
+}
+
+impl<T> Extension<T> for PageExtension where T: Base {}
diff --git a/server/src/apub/extensions/signatures.rs b/server/src/apub/extensions/signatures.rs
new file mode 100644 (file)
index 0000000..7aa9489
--- /dev/null
@@ -0,0 +1,137 @@
+use crate::apub::ActorType;
+use activitystreams::ext::Extension;
+use actix_web::HttpRequest;
+use attohttpc::RequestBuilder;
+use failure::Error;
+use http_signature_normalization::Config;
+use log::debug;
+use openssl::{
+  hash::MessageDigest,
+  pkey::PKey,
+  rsa::Rsa,
+  sign::{Signer, Verifier},
+};
+use serde::{Deserialize, Serialize};
+use std::collections::BTreeMap;
+
+lazy_static! {
+  static ref HTTP_SIG_CONFIG: Config = Config::new();
+}
+
+pub struct Keypair {
+  pub private_key: String,
+  pub public_key: String,
+}
+
+/// Generate the asymmetric keypair for ActivityPub HTTP signatures.
+pub fn generate_actor_keypair() -> Result<Keypair, Error> {
+  let rsa = Rsa::generate(2048)?;
+  let pkey = PKey::from_rsa(rsa)?;
+  let public_key = pkey.public_key_to_pem()?;
+  let private_key = pkey.private_key_to_pem_pkcs8()?;
+  Ok(Keypair {
+    private_key: String::from_utf8(private_key)?,
+    public_key: String::from_utf8(public_key)?,
+  })
+}
+
+// TODO is it possible to create this signature, with just the url and actor?
+/// Signs request headers with the given keypair.
+pub fn sign(request: &mut RequestBuilder, actor: &dyn ActorType) -> Result<String, Error> {
+  let signing_key_id = format!("{}#main-key", actor.actor_id());
+
+  let headers = request
+    .inspect()
+    .headers()
+    .iter()
+    .map(|h| -> Result<(String, String), Error> {
+      Ok((h.0.as_str().to_owned(), h.1.to_str()?.to_owned()))
+    })
+    .collect::<Result<BTreeMap<String, String>, Error>>()?;
+
+  let mut path_and_query = request.inspect().url().path().to_owned();
+  if let Some(query) = request.inspect().url().query() {
+    path_and_query.push_str(query);
+  }
+
+  let signature_header_value = HTTP_SIG_CONFIG
+    .begin_sign(
+      request.inspect().method().as_str(),
+      &path_and_query,
+      headers,
+    )?
+    .sign(signing_key_id, |signing_string| {
+      let private_key = PKey::private_key_from_pem(actor.private_key().as_bytes())?;
+      let mut signer = Signer::new(MessageDigest::sha256(), &private_key).unwrap();
+      signer.update(signing_string.as_bytes()).unwrap();
+      Ok(base64::encode(signer.sign_to_vec()?)) as Result<_, Error>
+    })?
+    .signature_header();
+
+  Ok(signature_header_value)
+}
+
+pub fn verify(request: &HttpRequest, actor: &dyn ActorType) -> Result<(), Error> {
+  let headers = request
+    .headers()
+    .iter()
+    .map(|h| -> Result<(String, String), Error> {
+      Ok((h.0.as_str().to_owned(), h.1.to_str()?.to_owned()))
+    })
+    .collect::<Result<BTreeMap<String, String>, Error>>()?;
+
+  let verified = HTTP_SIG_CONFIG
+    .begin_verify(
+      request.method().as_str(),
+      request.uri().path_and_query().unwrap().as_str(),
+      headers,
+    )?
+    .verify(|signature, signing_string| -> Result<bool, Error> {
+      debug!(
+        "Verifying with key {}, message {}",
+        &actor.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();
+      Ok(verifier.verify(&base64::decode(signature)?)?)
+    })?;
+
+  if verified {
+    debug!("verified signature for {}", &request.uri());
+    Ok(())
+  } else {
+    Err(format_err!(
+      "Invalid signature on request: {}",
+      &request.uri()
+    ))
+  }
+}
+
+// The following is taken from here:
+// https://docs.rs/activitystreams/0.5.0-alpha.17/activitystreams/ext/index.html
+
+#[derive(Clone, Debug, Default, Deserialize, Serialize)]
+#[serde(rename_all = "camelCase")]
+pub struct PublicKey {
+  pub id: String,
+  pub owner: String,
+  pub public_key_pem: String,
+}
+
+#[derive(Clone, Debug, Default, Deserialize, Serialize)]
+#[serde(rename_all = "camelCase")]
+pub struct PublicKeyExtension {
+  pub public_key: PublicKey,
+}
+
+impl PublicKey {
+  pub fn to_ext(&self) -> PublicKeyExtension {
+    PublicKeyExtension {
+      public_key: self.to_owned(),
+    }
+  }
+}
+
+impl<T> Extension<T> for PublicKeyExtension where T: activitystreams::Actor {}
diff --git a/server/src/apub/fetcher.rs b/server/src/apub/fetcher.rs
new file mode 100644 (file)
index 0000000..20bab4e
--- /dev/null
@@ -0,0 +1,326 @@
+use activitystreams::object::Note;
+use actix_web::Result;
+use diesel::{result::Error::NotFound, PgConnection};
+use failure::{Error, _core::fmt::Debug};
+use log::debug;
+use serde::Deserialize;
+use std::time::Duration;
+use url::Url;
+
+use crate::{
+  api::site::SearchResponse,
+  db::{
+    comment::{Comment, CommentForm},
+    comment_view::CommentView,
+    community::{Community, CommunityForm, CommunityModerator, CommunityModeratorForm},
+    community_view::CommunityView,
+    post::{Post, PostForm},
+    post_view::PostView,
+    user::{UserForm, User_},
+    Crud,
+    Joinable,
+    SearchType,
+  },
+  naive_now,
+  routes::nodeinfo::{NodeInfo, NodeInfoWellKnown},
+};
+
+use crate::{
+  apub::{
+    get_apub_protocol_string,
+    is_apub_id_valid,
+    FromApub,
+    GroupExt,
+    PageExt,
+    PersonExt,
+    APUB_JSON_CONTENT_TYPE,
+  },
+  db::user_view::UserView,
+};
+use chrono::NaiveDateTime;
+
+static ACTOR_REFETCH_INTERVAL_SECONDS: i64 = 24 * 60 * 60;
+
+// Fetch nodeinfo metadata from a remote instance.
+fn _fetch_node_info(domain: &str) -> Result<NodeInfo, Error> {
+  let well_known_uri = Url::parse(&format!(
+    "{}://{}/.well-known/nodeinfo",
+    get_apub_protocol_string(),
+    domain
+  ))?;
+  let well_known = fetch_remote_object::<NodeInfoWellKnown>(&well_known_uri)?;
+  Ok(fetch_remote_object::<NodeInfo>(&well_known.links.href)?)
+}
+
+/// Fetch any type of ActivityPub object, handling things like HTTP headers, deserialisation,
+/// timeouts etc.
+pub fn fetch_remote_object<Response>(url: &Url) -> Result<Response, Error>
+where
+  Response: for<'de> Deserialize<'de>,
+{
+  if !is_apub_id_valid(&url) {
+    return Err(format_err!("Activitypub uri invalid or blocked: {}", url));
+  }
+  // TODO: this function should return a future
+  let timeout = Duration::from_secs(60);
+  let text: String = attohttpc::get(url.as_str())
+    .header("Accept", APUB_JSON_CONTENT_TYPE)
+    .connect_timeout(timeout)
+    .timeout(timeout)
+    // .body(())
+    .send()?
+    .text()?;
+  let res: Response = serde_json::from_str(&text)?;
+  Ok(res)
+}
+
+/// The types of ActivityPub objects that can be fetched directly by searching for their ID.
+#[serde(untagged)]
+#[derive(serde::Deserialize, Debug)]
+pub enum SearchAcceptedObjects {
+  Person(Box<PersonExt>),
+  Group(Box<GroupExt>),
+  Page(Box<PageExt>),
+  Comment(Box<Note>),
+}
+
+/// Attempt to parse the query as URL, and fetch an ActivityPub object from it.
+///
+/// Some working examples for use with the docker/federation/ setup:
+/// http://lemmy_alpha:8540/c/main, or !main@lemmy_alpha:8540
+/// http://lemmy_alpha:8540/u/lemmy_alpha, or @lemmy_alpha@lemmy_alpha:8540
+/// http://lemmy_alpha:8540/post/3
+/// http://lemmy_alpha:8540/comment/2
+pub fn search_by_apub_id(query: &str, conn: &PgConnection) -> Result<SearchResponse, Error> {
+  // Parse the shorthand query url
+  let query_url = if query.contains('@') {
+    debug!("{}", query);
+    let split = query.split('@').collect::<Vec<&str>>();
+
+    // User type will look like ['', username, instance]
+    // Community will look like [!community, instance]
+    let (name, instance) = if split.len() == 3 {
+      (format!("/u/{}", split[1]), split[2])
+    } else if split.len() == 2 {
+      if split[0].contains('!') {
+        let split2 = split[0].split('!').collect::<Vec<&str>>();
+        (format!("/c/{}", split2[1]), split[1])
+      } else {
+        return Err(format_err!("Invalid search query: {}", query));
+      }
+    } else {
+      return Err(format_err!("Invalid search query: {}", query));
+    };
+
+    let url = format!("{}://{}{}", get_apub_protocol_string(), instance, name);
+    Url::parse(&url)?
+  } else {
+    Url::parse(&query)?
+  };
+
+  let mut response = SearchResponse {
+    type_: SearchType::All.to_string(),
+    comments: vec![],
+    posts: vec![],
+    communities: vec![],
+    users: vec![],
+  };
+  match fetch_remote_object::<SearchAcceptedObjects>(&query_url)? {
+    SearchAcceptedObjects::Person(p) => {
+      let user_uri = p.inner.object_props.get_id().unwrap().to_string();
+      let user = get_or_fetch_and_upsert_remote_user(&user_uri, &conn)?;
+      response.users = vec![UserView::read(conn, user.id)?];
+    }
+    SearchAcceptedObjects::Group(g) => {
+      let community_uri = g.inner.object_props.get_id().unwrap().to_string();
+      let community = get_or_fetch_and_upsert_remote_community(&community_uri, &conn)?;
+      // TODO Maybe at some point in the future, fetch all the history of a community
+      // fetch_community_outbox(&c, conn)?;
+      response.communities = vec![CommunityView::read(conn, community.id, None)?];
+    }
+    SearchAcceptedObjects::Page(p) => {
+      let p = upsert_post(&PostForm::from_apub(&p, conn)?, conn)?;
+      response.posts = vec![PostView::read(conn, p.id, None)?];
+    }
+    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(&Url::parse(&post_url)?)?;
+      upsert_post(&PostForm::from_apub(&post, conn)?, conn)?;
+      let c = upsert_comment(&CommentForm::from_apub(&c, conn)?, conn)?;
+      response.comments = vec![CommentView::read(conn, c.id, None)?];
+    }
+  }
+  Ok(response)
+}
+
+/// Check if a remote user exists, create if not found, if its too old update it.Fetch a user, insert/update it in the database and return the user.
+pub fn get_or_fetch_and_upsert_remote_user(
+  apub_id: &str,
+  conn: &PgConnection,
+) -> Result<User_, Error> {
+  match User_::read_from_actor_id(&conn, &apub_id) {
+    Ok(u) => {
+      // If its older than a day, re-fetch it
+      if !u.local && should_refetch_actor(u.last_refreshed_at) {
+        debug!("Fetching and updating from remote user: {}", apub_id);
+        let person = fetch_remote_object::<PersonExt>(&Url::parse(apub_id)?)?;
+        let mut uf = UserForm::from_apub(&person, &conn)?;
+        uf.last_refreshed_at = Some(naive_now());
+        Ok(User_::update(&conn, u.id, &uf)?)
+      } else {
+        Ok(u)
+      }
+    }
+    Err(NotFound {}) => {
+      debug!("Fetching and creating remote user: {}", apub_id);
+      let person = fetch_remote_object::<PersonExt>(&Url::parse(apub_id)?)?;
+      let uf = UserForm::from_apub(&person, &conn)?;
+      Ok(User_::create(conn, &uf)?)
+    }
+    Err(e) => Err(Error::from(e)),
+  }
+}
+
+/// Determines when a remote actor should be refetched from its instance. In release builds, this is
+/// ACTOR_REFETCH_INTERVAL_SECONDS after the last refetch, in debug builds always.
+///
+/// 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
+  } else {
+    let 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 fn get_or_fetch_and_upsert_remote_community(
+  apub_id: &str,
+  conn: &PgConnection,
+) -> Result<Community, Error> {
+  match Community::read_from_actor_id(&conn, &apub_id) {
+    Ok(c) => {
+      if !c.local && should_refetch_actor(c.last_refreshed_at) {
+        debug!("Fetching and updating from remote community: {}", apub_id);
+        let group = fetch_remote_object::<GroupExt>(&Url::parse(apub_id)?)?;
+        let mut cf = CommunityForm::from_apub(&group, conn)?;
+        cf.last_refreshed_at = Some(naive_now());
+        Ok(Community::update(&conn, c.id, &cf)?)
+      } else {
+        Ok(c)
+      }
+    }
+    Err(NotFound {}) => {
+      debug!("Fetching and creating remote community: {}", apub_id);
+      let group = fetch_remote_object::<GroupExt>(&Url::parse(apub_id)?)?;
+      let cf = CommunityForm::from_apub(&group, conn)?;
+      let community = Community::create(conn, &cf)?;
+
+      // Also add the community moderators too
+      let creator_and_moderator_uris = group
+        .inner
+        .object_props
+        .get_many_attributed_to_xsd_any_uris()
+        .unwrap();
+      let creator_and_moderators = creator_and_moderator_uris
+        .map(|c| get_or_fetch_and_upsert_remote_user(&c.to_string(), &conn).unwrap())
+        .collect::<Vec<User_>>();
+
+      for mod_ in creator_and_moderators {
+        let community_moderator_form = CommunityModeratorForm {
+          community_id: community.id,
+          user_id: mod_.id,
+        };
+        CommunityModerator::join(&conn, &community_moderator_form)?;
+      }
+
+      Ok(community)
+    }
+    Err(e) => Err(Error::from(e)),
+  }
+}
+
+fn upsert_post(post_form: &PostForm, conn: &PgConnection) -> Result<Post, Error> {
+  let existing = Post::read_from_apub_id(conn, &post_form.ap_id);
+  match existing {
+    Err(NotFound {}) => Ok(Post::create(conn, &post_form)?),
+    Ok(p) => Ok(Post::update(conn, p.id, &post_form)?),
+    Err(e) => Err(Error::from(e)),
+  }
+}
+
+pub fn get_or_fetch_and_insert_remote_post(
+  post_ap_id: &str,
+  conn: &PgConnection,
+) -> Result<Post, Error> {
+  match Post::read_from_apub_id(conn, post_ap_id) {
+    Ok(p) => Ok(p),
+    Err(NotFound {}) => {
+      debug!("Fetching and creating remote post: {}", post_ap_id);
+      let post = fetch_remote_object::<PageExt>(&Url::parse(post_ap_id)?)?;
+      let post_form = PostForm::from_apub(&post, conn)?;
+      Ok(Post::create(conn, &post_form)?)
+    }
+    Err(e) => Err(Error::from(e)),
+  }
+}
+
+fn upsert_comment(comment_form: &CommentForm, conn: &PgConnection) -> Result<Comment, Error> {
+  let existing = Comment::read_from_apub_id(conn, &comment_form.ap_id);
+  match existing {
+    Err(NotFound {}) => Ok(Comment::create(conn, &comment_form)?),
+    Ok(p) => Ok(Comment::update(conn, p.id, &comment_form)?),
+    Err(e) => Err(Error::from(e)),
+  }
+}
+
+pub fn get_or_fetch_and_insert_remote_comment(
+  comment_ap_id: &str,
+  conn: &PgConnection,
+) -> Result<Comment, Error> {
+  match Comment::read_from_apub_id(conn, comment_ap_id) {
+    Ok(p) => Ok(p),
+    Err(NotFound {}) => {
+      debug!(
+        "Fetching and creating remote comment and its parents: {}",
+        comment_ap_id
+      );
+      let comment = fetch_remote_object::<Note>(&Url::parse(comment_ap_id)?)?;
+      let comment_form = CommentForm::from_apub(&comment, conn)?;
+      Ok(Comment::create(conn, &comment_form)?)
+    }
+    Err(e) => Err(Error::from(e)),
+  }
+}
+
+// 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>, Error> {
+//   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, Error> {
+//         let page = obox.clone().to_concrete::<Page>()?;
+//         PostForm::from_page(&page, conn)
+//       })
+//       .map(|pf| upsert_post(&pf?, conn))
+//       .collect::<Result<Vec<Post>, Error>>()?,
+//   )
+// }
index e224e2591636c033da7a4f65d0c02554b8cf2543..7d2aee65c9101afc113ed27487489ac5218a2910 100644 (file)
+pub mod activities;
+pub mod comment;
 pub mod community;
+pub mod community_inbox;
+pub mod extensions;
+pub mod fetcher;
 pub mod post;
+pub mod private_message;
+pub mod shared_inbox;
 pub mod user;
-use crate::Settings;
-
-use std::fmt::Display;
-
-#[cfg(test)]
-mod tests {
-  use crate::db::community::Community;
-  use crate::db::post::Post;
-  use crate::db::user::User_;
-  use crate::db::{ListingType, SortType};
-  use crate::{naive_now, Settings};
-
-  #[test]
-  fn test_person() {
-    let user = User_ {
-      id: 52,
-      name: "thom".into(),
-      fedi_name: "rrf".into(),
-      preferred_username: None,
-      password_encrypted: "here".into(),
-      email: None,
-      matrix_user_id: None,
-      avatar: None,
-      published: naive_now(),
-      admin: false,
-      banned: false,
-      updated: None,
-      show_nsfw: false,
-      theme: "darkly".into(),
-      default_sort_type: SortType::Hot as i16,
-      default_listing_type: ListingType::Subscribed as i16,
-      lang: "browser".into(),
-      show_avatars: true,
-      send_notifications_to_email: false,
-    };
-
-    let person = user.as_person();
-    assert_eq!(
-      format!("https://{}/federation/u/thom", Settings::get().hostname),
-      person.object_props.id_string().unwrap()
-    );
+pub mod user_inbox;
+
+use crate::{
+  apub::extensions::{
+    group_extensions::GroupExtension,
+    page_extension::PageExtension,
+    signatures::{PublicKey, PublicKeyExtension},
+  },
+  convert_datetime,
+  db::user::User_,
+  routes::webfinger::WebFingerResponse,
+  MentionData,
+  Settings,
+};
+use activitystreams::{
+  actor::{properties::ApActorProperties, Group, Person},
+  object::Page,
+};
+use activitystreams_ext::{Ext1, Ext2, Ext3};
+use activitystreams_new::{activity::Follow, object::Tombstone, prelude::*};
+use actix_web::{body::Body, HttpResponse, Result};
+use chrono::NaiveDateTime;
+use diesel::PgConnection;
+use failure::Error;
+use log::debug;
+use serde::Serialize;
+use url::Url;
+
+type GroupExt = Ext3<Group, GroupExtension, ApActorProperties, PublicKeyExtension>;
+type PersonExt = Ext2<Person, ApActorProperties, PublicKeyExtension>;
+type PageExt = Ext1<Page, PageExtension>;
+
+pub static APUB_JSON_CONTENT_TYPE: &str = "application/activity+json";
+
+pub enum EndpointType {
+  Community,
+  User,
+  Post,
+  Comment,
+  PrivateMessage,
+}
+
+/// Convert the data to json and turn it into an HTTP Response with the correct ActivityPub
+/// headers.
+fn create_apub_response<T>(data: &T) -> HttpResponse<Body>
+where
+  T: Serialize,
+{
+  HttpResponse::Ok()
+    .content_type(APUB_JSON_CONTENT_TYPE)
+    .json(data)
+}
+
+fn create_apub_tombstone_response<T>(data: &T) -> HttpResponse<Body>
+where
+  T: Serialize,
+{
+  HttpResponse::Gone()
+    .content_type(APUB_JSON_CONTENT_TYPE)
+    .json(data)
+}
+
+/// Generates the ActivityPub ID for a given object type and ID.
+pub fn make_apub_endpoint(endpoint_type: EndpointType, name: &str) -> Url {
+  let point = match endpoint_type {
+    EndpointType::Community => "c",
+    EndpointType::User => "u",
+    EndpointType::Post => "post",
+    EndpointType::Comment => "comment",
+    EndpointType::PrivateMessage => "private_message",
+  };
+
+  Url::parse(&format!(
+    "{}://{}/{}/{}",
+    get_apub_protocol_string(),
+    Settings::get().hostname,
+    point,
+    name
+  ))
+  .unwrap()
+}
+
+pub fn get_apub_protocol_string() -> &'static str {
+  if Settings::get().federation.tls_enabled {
+    "https"
+  } else {
+    "http"
+  }
+}
+
+// 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 {
+  if apub_id.scheme() != get_apub_protocol_string() {
+    return false;
   }
 
-  #[test]
-  fn test_community() {
-    let community = Community {
-      id: 42,
-      name: "Test".into(),
-      title: "Test Title".into(),
-      description: Some("Test community".into()),
-      category_id: 32,
-      creator_id: 52,
-      removed: false,
-      published: naive_now(),
-      updated: Some(naive_now()),
-      deleted: false,
-      nsfw: false,
-    };
-
-    let group = community.as_group();
-    assert_eq!(
-      format!("https://{}/federation/c/Test", Settings::get().hostname),
-      group.object_props.id_string().unwrap()
-    );
+  let allowed_instances: Vec<String> = Settings::get()
+    .federation
+    .allowed_instances
+    .split(',')
+    .map(|d| d.to_string())
+    .collect();
+  match apub_id.domain() {
+    Some(d) => allowed_instances.contains(&d.to_owned()),
+    None => false,
   }
+}
+
+pub trait ToApub {
+  type Response;
+  fn to_apub(&self, conn: &PgConnection) -> Result<Self::Response, Error>;
+  fn to_tombstone(&self) -> Result<Tombstone, Error>;
+}
 
-  #[test]
-  fn test_post() {
-    let post = Post {
-      id: 62,
-      name: "A test post".into(),
-      url: None,
-      body: None,
-      creator_id: 52,
-      community_id: 42,
-      published: naive_now(),
-      removed: false,
-      locked: false,
-      stickied: false,
-      nsfw: false,
-      deleted: false,
-      updated: None,
-      embed_title: None,
-      embed_description: None,
-      embed_html: None,
-      thumbnail_url: None,
-    };
-
-    let page = post.as_page();
-    assert_eq!(
-      format!("https://{}/federation/post/62", Settings::get().hostname),
-      page.object_props.id_string().unwrap()
-    );
+/// Updated is actually the deletion time
+fn create_tombstone(
+  deleted: bool,
+  object_id: &str,
+  updated: Option<NaiveDateTime>,
+  former_type: String,
+) -> Result<Tombstone, Error> {
+  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());
+      Ok(tombstone)
+    } else {
+      Err(format_err!(
+        "Cant convert to tombstone because updated time was None."
+      ))
+    }
+  } else {
+    Err(format_err!(
+      "Cant convert object to tombstone if it wasnt deleted"
+    ))
   }
 }
 
-pub fn make_apub_endpoint<S: Display, T: Display>(point: S, value: T) -> String {
+pub trait FromApub {
+  type ApubType;
+  fn from_apub(apub: &Self::ApubType, conn: &PgConnection) -> Result<Self, Error>
+  where
+    Self: Sized;
+}
+
+pub trait ApubObjectType {
+  fn send_create(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error>;
+  fn send_update(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error>;
+  fn send_delete(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error>;
+  fn send_undo_delete(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error>;
+  fn send_remove(&self, mod_: &User_, conn: &PgConnection) -> Result<(), Error>;
+  fn send_undo_remove(&self, mod_: &User_, conn: &PgConnection) -> Result<(), Error>;
+}
+
+pub trait ApubLikeableType {
+  fn send_like(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error>;
+  fn send_dislike(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error>;
+  fn send_undo_like(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error>;
+}
+
+pub fn get_shared_inbox(actor_id: &str) -> String {
+  let url = Url::parse(actor_id).unwrap();
   format!(
-    "https://{}/federation/{}/{}",
-    Settings::get().hostname,
-    point,
-    value
+    "{}://{}{}/inbox",
+    &url.scheme(),
+    &url.host_str().unwrap(),
+    if let Some(port) = url.port() {
+      format!(":{}", port)
+    } else {
+      "".to_string()
+    },
   )
 }
+
+pub trait ActorType {
+  fn actor_id(&self) -> String;
+
+  fn public_key(&self) -> String;
+  fn private_key(&self) -> String;
+
+  // These two have default impls, since currently a community can't follow anything,
+  // and a user can't be followed (yet)
+  #[allow(unused_variables)]
+  fn send_follow(&self, follow_actor_id: &str, conn: &PgConnection) -> Result<(), Error>;
+  fn send_unfollow(&self, follow_actor_id: &str, conn: &PgConnection) -> Result<(), Error>;
+
+  #[allow(unused_variables)]
+  fn send_accept_follow(&self, follow: &Follow, conn: &PgConnection) -> Result<(), Error>;
+
+  fn send_delete(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error>;
+  fn send_undo_delete(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error>;
+
+  fn send_remove(&self, mod_: &User_, conn: &PgConnection) -> Result<(), Error>;
+  fn send_undo_remove(&self, mod_: &User_, conn: &PgConnection) -> Result<(), Error>;
+
+  /// For a given community, returns the inboxes of all followers.
+  fn get_follower_inboxes(&self, conn: &PgConnection) -> Result<Vec<String>, Error>;
+
+  // TODO move these to the db rows
+  fn get_inbox_url(&self) -> String {
+    format!("{}/inbox", &self.actor_id())
+  }
+
+  fn get_shared_inbox_url(&self) -> String {
+    get_shared_inbox(&self.actor_id())
+  }
+
+  fn get_outbox_url(&self) -> String {
+    format!("{}/outbox", &self.actor_id())
+  }
+
+  fn get_followers_url(&self) -> String {
+    format!("{}/followers", &self.actor_id())
+  }
+
+  fn get_following_url(&self) -> String {
+    format!("{}/following", &self.actor_id())
+  }
+
+  fn get_liked_url(&self) -> String {
+    format!("{}/liked", &self.actor_id())
+  }
+
+  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()
+  }
+}
+
+pub fn fetch_webfinger_url(mention: &MentionData) -> Result<String, Error> {
+  let fetch_url = format!(
+    "{}://{}/.well-known/webfinger?resource=acct:{}@{}",
+    get_apub_protocol_string(),
+    mention.domain,
+    mention.name,
+    mention.domain
+  );
+  debug!("Fetching webfinger url: {}", &fetch_url);
+  let text: String = attohttpc::get(&fetch_url).send()?.text()?;
+  let res: WebFingerResponse = serde_json::from_str(&text)?;
+  let link = res
+    .links
+    .iter()
+    .find(|l| l.type_.eq(&Some("application/activity+json".to_string())))
+    .ok_or_else(|| format_err!("No application/activity+json link found."))?;
+  link
+    .href
+    .to_owned()
+    .ok_or_else(|| format_err!("No href found."))
+}
index ebb17129037eb0b6f30e1b383f46e9baa2d1a83b..3f86d34d1132ae21ace8a9f8ee835ed55c8503e9 100644 (file)
-use crate::apub::make_apub_endpoint;
-use crate::db::post::Post;
-use crate::to_datetime_utc;
-use activitypub::{context, object::Page};
-
-impl Post {
-  pub fn as_page(&self) -> Page {
-    let base_url = make_apub_endpoint("post", self.id);
+use crate::{
+  apub::{
+    activities::{populate_object_props, send_activity_to_community},
+    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},
+    get_apub_protocol_string,
+    ActorType,
+    ApubLikeableType,
+    ApubObjectType,
+    FromApub,
+    PageExt,
+    ToApub,
+  },
+  convert_datetime,
+  db::{
+    community::Community,
+    post::{Post, PostForm},
+    user::User_,
+    Crud,
+  },
+  routes::DbPoolParam,
+  Settings,
+};
+use activitystreams::{
+  activity::{Create, Delete, Dislike, Like, Remove, Undo, Update},
+  context,
+  object::{kind::PageType, properties::ObjectProperties, AnyImage, Image, Page},
+  BaseBox,
+};
+use activitystreams_ext::Ext1;
+use activitystreams_new::object::Tombstone;
+use actix_web::{body::Body, web::Path, HttpResponse, Result};
+use diesel::PgConnection;
+use failure::Error;
+use serde::Deserialize;
+
+#[derive(Deserialize)]
+pub struct PostQuery {
+  post_id: String,
+}
+
+/// Return the post json over HTTP.
+pub async fn get_apub_post(
+  info: Path<PostQuery>,
+  db: DbPoolParam,
+) -> Result<HttpResponse<Body>, Error> {
+  let id = info.post_id.parse::<i32>()?;
+  let post = Post::read(&&db.get()?, id)?;
+  if !post.deleted {
+    Ok(create_apub_response(&post.to_apub(&db.get().unwrap())?))
+  } else {
+    Ok(create_apub_tombstone_response(&post.to_tombstone()?))
+  }
+}
+
+impl ToApub for Post {
+  type Response = PageExt;
+
+  // Turn a Lemmy post into an ActivityPub page that can be sent out over the network.
+  fn to_apub(&self, conn: &PgConnection) -> Result<PageExt, Error> {
     let mut page = Page::default();
+    let oprops: &mut ObjectProperties = page.as_mut();
+    let creator = User_::read(conn, self.creator_id)?;
+    let community = Community::read(conn, self.community_id)?;
 
-    page.object_props.set_context_object(context()).ok();
-    page.object_props.set_id_string(base_url).ok();
-    page.object_props.set_name_string(self.name.to_owned()).ok();
+    oprops
+      // 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())?
+      // 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)?;
 
     if let Some(body) = &self.body {
-      page.object_props.set_content_string(body.to_owned()).ok();
+      oprops.set_content_xsd_string(body.to_owned())?;
     }
 
-    if let Some(url) = &self.url {
-      page.object_props.set_url_string(url.to_owned()).ok();
+    // 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())?;
+
+      // Embeds
+      let mut page_preview = Page::new();
+      page_preview
+        .object_props
+        .set_url_xsd_any_uri(u.to_owned())?;
+
+      if let Some(embed_title) = &self.embed_title {
+        page_preview
+          .object_props
+          .set_name_xsd_string(embed_title.to_owned())?;
+      }
+
+      if let Some(embed_description) = &self.embed_description {
+        page_preview
+          .object_props
+          .set_summary_xsd_string(embed_description.to_owned())?;
+      }
+
+      if let Some(embed_html) = &self.embed_html {
+        page_preview
+          .object_props
+          .set_content_xsd_string(embed_html.to_owned())?;
+      }
+
+      oprops.set_preview_base_box(page_preview)?;
     }
 
-    //page.object_props.set_attributed_to_string
+    if let Some(thumbnail_url) = &self.thumbnail_url {
+      let full_url = format!(
+        "{}://{}/pictshare/{}",
+        get_apub_protocol_string(),
+        Settings::get().hostname,
+        thumbnail_url
+      );
 
-    page
-      .object_props
-      .set_published_utctime(to_datetime_utc(self.published))
-      .ok();
-    if let Some(updated) = self.updated {
-      page
-        .object_props
-        .set_updated_utctime(to_datetime_utc(updated))
-        .ok();
+      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)?;
+    }
+
+    if let Some(u) = self.updated {
+      oprops.set_updated(convert_datetime(u))?;
     }
 
-    page
+    let ext = PageExtension {
+      comments_enabled: !self.locked,
+      sensitive: self.nsfw,
+    };
+    Ok(Ext1::new(page, ext))
+  }
+
+  fn to_tombstone(&self) -> Result<Tombstone, Error> {
+    create_tombstone(
+      self.deleted,
+      &self.ap_id,
+      self.updated,
+      PageType.to_string(),
+    )
+  }
+}
+
+impl FromApub for PostForm {
+  type ApubType = PageExt;
+
+  /// Parse an ActivityPub page received from another instance into a Lemmy post.
+  fn from_apub(page: &PageExt, conn: &PgConnection) -> Result<PostForm, Error> {
+    let ext = &page.ext_one;
+    let oprops = &page.inner.object_props;
+    let creator_actor_id = &oprops.get_attributed_to_xsd_any_uri().unwrap().to_string();
+    let creator = get_or_fetch_and_upsert_remote_user(&creator_actor_id, &conn)?;
+    let community_actor_id = &oprops.get_to_xsd_any_uri().unwrap().to_string();
+    let community = get_or_fetch_and_upsert_remote_community(&community_actor_id, &conn)?;
+
+    let 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()),
+      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),
+    };
+
+    Ok(PostForm {
+      name: oprops.get_summary_xsd_string().unwrap().to_string(),
+      url,
+      body: oprops.get_content_xsd_string().map(|c| c.to_string()),
+      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()),
+      deleted: None,
+      nsfw: ext.sensitive,
+      stickied: None, // -> put it in "featured" collection of the community
+      embed_title,
+      embed_description,
+      embed_html,
+      thumbnail_url,
+      ap_id: oprops.get_id().unwrap().to_string(),
+      local: false,
+    })
+  }
+}
+
+impl ApubObjectType for Post {
+  /// Send out information about a newly created post, to the followers of the community.
+  fn send_create(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error> {
+    let page = self.to_apub(conn)?;
+    let community = Community::read(conn, self.community_id)?;
+    let id = format!("{}/create/{}", self.ap_id, uuid::Uuid::new_v4());
+
+    let mut create = Create::new();
+    populate_object_props(
+      &mut create.object_props,
+      vec![community.get_followers_url()],
+      &id,
+    )?;
+    create
+      .create_props
+      .set_actor_xsd_any_uri(creator.actor_id.to_owned())?
+      .set_object_base_box(BaseBox::from_concrete(page)?)?;
+
+    send_activity_to_community(
+      creator,
+      conn,
+      &community,
+      vec![community.get_shared_inbox_url()],
+      create,
+    )?;
+    Ok(())
+  }
+
+  /// Send out information about an edited post, to the followers of the community.
+  fn send_update(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error> {
+    let page = self.to_apub(conn)?;
+    let community = Community::read(conn, self.community_id)?;
+    let id = format!("{}/update/{}", self.ap_id, uuid::Uuid::new_v4());
+
+    let mut update = Update::new();
+    populate_object_props(
+      &mut update.object_props,
+      vec![community.get_followers_url()],
+      &id,
+    )?;
+    update
+      .update_props
+      .set_actor_xsd_any_uri(creator.actor_id.to_owned())?
+      .set_object_base_box(BaseBox::from_concrete(page)?)?;
+
+    send_activity_to_community(
+      creator,
+      conn,
+      &community,
+      vec![community.get_shared_inbox_url()],
+      update,
+    )?;
+    Ok(())
+  }
+
+  fn send_delete(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error> {
+    let page = self.to_apub(conn)?;
+    let community = Community::read(conn, self.community_id)?;
+    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,
+    )?;
+
+    delete
+      .delete_props
+      .set_actor_xsd_any_uri(creator.actor_id.to_owned())?
+      .set_object_base_box(BaseBox::from_concrete(page)?)?;
+
+    let community = Community::read(conn, self.community_id)?;
+
+    send_activity_to_community(
+      creator,
+      conn,
+      &community,
+      vec![community.get_shared_inbox_url()],
+      delete,
+    )?;
+    Ok(())
+  }
+
+  fn send_undo_delete(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error> {
+    let page = self.to_apub(conn)?;
+    let community = Community::read(conn, self.community_id)?;
+    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,
+    )?;
+
+    delete
+      .delete_props
+      .set_actor_xsd_any_uri(creator.actor_id.to_owned())?
+      .set_object_base_box(BaseBox::from_concrete(page)?)?;
+
+    // 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,
+    )?;
+
+    undo
+      .undo_props
+      .set_actor_xsd_any_uri(creator.actor_id.to_owned())?
+      .set_object_base_box(delete)?;
+
+    let community = Community::read(conn, self.community_id)?;
+    send_activity_to_community(
+      creator,
+      conn,
+      &community,
+      vec![community.get_shared_inbox_url()],
+      undo,
+    )?;
+    Ok(())
+  }
+
+  fn send_remove(&self, mod_: &User_, conn: &PgConnection) -> Result<(), Error> {
+    let page = self.to_apub(conn)?;
+    let community = Community::read(conn, self.community_id)?;
+    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,
+    )?;
+
+    remove
+      .remove_props
+      .set_actor_xsd_any_uri(mod_.actor_id.to_owned())?
+      .set_object_base_box(BaseBox::from_concrete(page)?)?;
+
+    let community = Community::read(conn, self.community_id)?;
+
+    send_activity_to_community(
+      mod_,
+      conn,
+      &community,
+      vec![community.get_shared_inbox_url()],
+      remove,
+    )?;
+    Ok(())
+  }
+  fn send_undo_remove(&self, mod_: &User_, conn: &PgConnection) -> Result<(), Error> {
+    let page = self.to_apub(conn)?;
+    let community = Community::read(conn, self.community_id)?;
+    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,
+    )?;
+
+    remove
+      .remove_props
+      .set_actor_xsd_any_uri(mod_.actor_id.to_owned())?
+      .set_object_base_box(BaseBox::from_concrete(page)?)?;
+
+    // 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,
+    )?;
+
+    undo
+      .undo_props
+      .set_actor_xsd_any_uri(mod_.actor_id.to_owned())?
+      .set_object_base_box(remove)?;
+
+    let community = Community::read(conn, self.community_id)?;
+    send_activity_to_community(
+      mod_,
+      conn,
+      &community,
+      vec![community.get_shared_inbox_url()],
+      undo,
+    )?;
+    Ok(())
+  }
+}
+
+impl ApubLikeableType for Post {
+  fn send_like(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error> {
+    let page = self.to_apub(conn)?;
+    let community = Community::read(conn, self.community_id)?;
+    let id = format!("{}/like/{}", self.ap_id, uuid::Uuid::new_v4());
+
+    let mut like = Like::new();
+    populate_object_props(
+      &mut like.object_props,
+      vec![community.get_followers_url()],
+      &id,
+    )?;
+    like
+      .like_props
+      .set_actor_xsd_any_uri(creator.actor_id.to_owned())?
+      .set_object_base_box(BaseBox::from_concrete(page)?)?;
+
+    send_activity_to_community(
+      &creator,
+      &conn,
+      &community,
+      vec![community.get_shared_inbox_url()],
+      like,
+    )?;
+    Ok(())
+  }
+
+  fn send_dislike(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error> {
+    let page = self.to_apub(conn)?;
+    let community = Community::read(conn, self.community_id)?;
+    let id = format!("{}/dislike/{}", self.ap_id, uuid::Uuid::new_v4());
+
+    let mut dislike = Dislike::new();
+    populate_object_props(
+      &mut dislike.object_props,
+      vec![community.get_followers_url()],
+      &id,
+    )?;
+    dislike
+      .dislike_props
+      .set_actor_xsd_any_uri(creator.actor_id.to_owned())?
+      .set_object_base_box(BaseBox::from_concrete(page)?)?;
+
+    send_activity_to_community(
+      &creator,
+      &conn,
+      &community,
+      vec![community.get_shared_inbox_url()],
+      dislike,
+    )?;
+    Ok(())
+  }
+
+  fn send_undo_like(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error> {
+    let page = self.to_apub(conn)?;
+    let community = Community::read(conn, self.community_id)?;
+    let id = format!("{}/like/{}", self.ap_id, uuid::Uuid::new_v4());
+
+    let mut like = Like::new();
+    populate_object_props(
+      &mut like.object_props,
+      vec![community.get_followers_url()],
+      &id,
+    )?;
+    like
+      .like_props
+      .set_actor_xsd_any_uri(creator.actor_id.to_owned())?
+      .set_object_base_box(BaseBox::from_concrete(page)?)?;
+
+    // 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,
+    )?;
+
+    undo
+      .undo_props
+      .set_actor_xsd_any_uri(creator.actor_id.to_owned())?
+      .set_object_base_box(like)?;
+
+    send_activity_to_community(
+      &creator,
+      &conn,
+      &community,
+      vec![community.get_shared_inbox_url()],
+      undo,
+    )?;
+    Ok(())
   }
 }
diff --git a/server/src/apub/private_message.rs b/server/src/apub/private_message.rs
new file mode 100644 (file)
index 0000000..a700043
--- /dev/null
@@ -0,0 +1,211 @@
+use crate::{
+  apub::{
+    activities::send_activity,
+    create_tombstone,
+    fetcher::get_or_fetch_and_upsert_remote_user,
+    ApubObjectType,
+    FromApub,
+    ToApub,
+  },
+  convert_datetime,
+  db::{
+    activity::insert_activity,
+    private_message::{PrivateMessage, PrivateMessageForm},
+    user::User_,
+    Crud,
+  },
+};
+use activitystreams::{
+  activity::{Create, Delete, Undo, Update},
+  context,
+  object::{kind::NoteType, properties::ObjectProperties, Note},
+};
+use activitystreams_new::object::Tombstone;
+use actix_web::Result;
+use diesel::PgConnection;
+use failure::Error;
+
+impl ToApub for PrivateMessage {
+  type Response = Note;
+
+  fn to_apub(&self, conn: &PgConnection) -> Result<Note, Error> {
+    let mut private_message = Note::default();
+    let oprops: &mut ObjectProperties = private_message.as_mut();
+    let creator = User_::read(&conn, self.creator_id)?;
+    let recipient = User_::read(&conn, self.recipient_id)?;
+
+    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)?;
+
+    if let Some(u) = self.updated {
+      oprops.set_updated(convert_datetime(u))?;
+    }
+
+    Ok(private_message)
+  }
+
+  fn to_tombstone(&self) -> Result<Tombstone, Error> {
+    create_tombstone(
+      self.deleted,
+      &self.ap_id,
+      self.updated,
+      NoteType.to_string(),
+    )
+  }
+}
+
+impl FromApub for PrivateMessageForm {
+  type ApubType = Note;
+
+  /// Parse an ActivityPub note received from another instance into a Lemmy Private message
+  fn from_apub(note: &Note, conn: &PgConnection) -> Result<PrivateMessageForm, Error> {
+    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, &conn)?;
+    let recipient_actor_id = &oprops.get_to_xsd_any_uri().unwrap().to_string();
+    let recipient = get_or_fetch_and_upsert_remote_user(&recipient_actor_id, &conn)?;
+
+    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()),
+      deleted: None,
+      read: None,
+      ap_id: oprops.get_id().unwrap().to_string(),
+      local: false,
+    })
+  }
+}
+
+impl ApubObjectType for PrivateMessage {
+  /// Send out information about a newly created private message
+  fn send_create(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error> {
+    let note = self.to_apub(conn)?;
+    let id = format!("{}/create/{}", self.ap_id, uuid::Uuid::new_v4());
+    let recipient = User_::read(&conn, self.recipient_id)?;
+
+    let mut create = Create::new();
+    create
+      .object_props
+      .set_context_xsd_any_uri(context())?
+      .set_id(id)?;
+    let to = format!("{}/inbox", recipient.actor_id);
+
+    create
+      .create_props
+      .set_actor_xsd_any_uri(creator.actor_id.to_owned())?
+      .set_object_base_box(note)?;
+
+    insert_activity(&conn, creator.id, &create, true)?;
+
+    send_activity(&create, creator, vec![to])?;
+    Ok(())
+  }
+
+  /// Send out information about an edited post, to the followers of the community.
+  fn send_update(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error> {
+    let note = self.to_apub(conn)?;
+    let id = format!("{}/update/{}", self.ap_id, uuid::Uuid::new_v4());
+    let recipient = User_::read(&conn, self.recipient_id)?;
+
+    let mut update = Update::new();
+    update
+      .object_props
+      .set_context_xsd_any_uri(context())?
+      .set_id(id)?;
+    let to = format!("{}/inbox", recipient.actor_id);
+
+    update
+      .update_props
+      .set_actor_xsd_any_uri(creator.actor_id.to_owned())?
+      .set_object_base_box(note)?;
+
+    insert_activity(&conn, creator.id, &update, true)?;
+
+    send_activity(&update, creator, vec![to])?;
+    Ok(())
+  }
+
+  fn send_delete(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error> {
+    let note = self.to_apub(conn)?;
+    let id = format!("{}/delete/{}", self.ap_id, uuid::Uuid::new_v4());
+    let recipient = User_::read(&conn, self.recipient_id)?;
+
+    let mut delete = Delete::new();
+    delete
+      .object_props
+      .set_context_xsd_any_uri(context())?
+      .set_id(id)?;
+    let to = format!("{}/inbox", recipient.actor_id);
+
+    delete
+      .delete_props
+      .set_actor_xsd_any_uri(creator.actor_id.to_owned())?
+      .set_object_base_box(note)?;
+
+    insert_activity(&conn, creator.id, &delete, true)?;
+
+    send_activity(&delete, creator, vec![to])?;
+    Ok(())
+  }
+
+  fn send_undo_delete(&self, creator: &User_, conn: &PgConnection) -> Result<(), Error> {
+    let note = self.to_apub(conn)?;
+    let id = format!("{}/delete/{}", self.ap_id, uuid::Uuid::new_v4());
+    let recipient = User_::read(&conn, self.recipient_id)?;
+
+    let mut delete = Delete::new();
+    delete
+      .object_props
+      .set_context_xsd_any_uri(context())?
+      .set_id(id)?;
+    let to = format!("{}/inbox", recipient.actor_id);
+
+    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)?;
+
+    undo
+      .undo_props
+      .set_actor_xsd_any_uri(creator.actor_id.to_owned())?
+      .set_object_base_box(delete)?;
+
+    insert_activity(&conn, creator.id, &undo, true)?;
+
+    send_activity(&undo, creator, vec![to])?;
+    Ok(())
+  }
+
+  fn send_remove(&self, _mod_: &User_, _conn: &PgConnection) -> Result<(), Error> {
+    unimplemented!()
+  }
+
+  fn send_undo_remove(&self, _mod_: &User_, _conn: &PgConnection) -> Result<(), Error> {
+    unimplemented!()
+  }
+}
diff --git a/server/src/apub/shared_inbox.rs b/server/src/apub/shared_inbox.rs
new file mode 100644 (file)
index 0000000..1ada6ad
--- /dev/null
@@ -0,0 +1,1615 @@
+use crate::{
+  api::{
+    comment::{send_local_notifs, CommentResponse},
+    community::CommunityResponse,
+    post::PostResponse,
+  },
+  apub::{
+    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,
+    },
+    FromApub,
+    GroupExt,
+    PageExt,
+  },
+  db::{
+    activity::insert_activity,
+    comment::{Comment, CommentForm, CommentLike, CommentLikeForm},
+    comment_view::CommentView,
+    community::{Community, CommunityForm},
+    community_view::CommunityView,
+    post::{Post, PostForm, PostLike, PostLikeForm},
+    post_view::PostView,
+    Crud,
+    Likeable,
+  },
+  naive_now,
+  routes::{ChatServerParam, DbPoolParam},
+  scrape_text_for_mentions,
+  websocket::{
+    server::{SendComment, SendCommunityRoomMessage, SendPost},
+    UserOperation,
+  },
+};
+use activitystreams::{
+  activity::{Announce, Create, Delete, Dislike, Like, Remove, Undo, Update},
+  object::Note,
+  Activity,
+  Base,
+  BaseBox,
+};
+use actix_web::{web, HttpRequest, HttpResponse, Result};
+use diesel::PgConnection;
+use failure::{Error, _core::fmt::Debug};
+use log::debug;
+use serde::{Deserialize, Serialize};
+
+#[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>,
+  db: DbPoolParam,
+  chat_server: ChatServerParam,
+) -> Result<HttpResponse, Error> {
+  let activity = input.into_inner();
+  let conn = &db.get().unwrap();
+
+  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(), &conn) {
+    Ok(u) => verify(&request, &u),
+    Err(_) => {
+      let c = get_or_fetch_and_upsert_remote_community(&sender.to_string(), &conn)?;
+      verify(&request, &c)
+    }
+  }?;
+
+  match (activity, object.kind()) {
+    (SharedAcceptedObjects::Create(c), Some("Page")) => {
+      receive_create_post(&c, &conn, chat_server)?;
+      announce_activity_if_valid::<Create>(*c, &to, sender, conn)
+    }
+    (SharedAcceptedObjects::Update(u), Some("Page")) => {
+      receive_update_post(&u, &conn, chat_server)?;
+      announce_activity_if_valid::<Update>(*u, &to, sender, conn)
+    }
+    (SharedAcceptedObjects::Like(l), Some("Page")) => {
+      receive_like_post(&l, &conn, chat_server)?;
+      announce_activity_if_valid::<Like>(*l, &to, sender, conn)
+    }
+    (SharedAcceptedObjects::Dislike(d), Some("Page")) => {
+      receive_dislike_post(&d, &conn, chat_server)?;
+      announce_activity_if_valid::<Dislike>(*d, &to, sender, conn)
+    }
+    (SharedAcceptedObjects::Delete(d), Some("Page")) => {
+      receive_delete_post(&d, &conn, chat_server)?;
+      announce_activity_if_valid::<Delete>(*d, &to, sender, conn)
+    }
+    (SharedAcceptedObjects::Remove(r), Some("Page")) => {
+      receive_remove_post(&r, &conn, chat_server)?;
+      announce_activity_if_valid::<Remove>(*r, &to, sender, conn)
+    }
+    (SharedAcceptedObjects::Create(c), Some("Note")) => {
+      receive_create_comment(&c, &conn, chat_server)?;
+      announce_activity_if_valid::<Create>(*c, &to, sender, conn)
+    }
+    (SharedAcceptedObjects::Update(u), Some("Note")) => {
+      receive_update_comment(&u, &conn, chat_server)?;
+      announce_activity_if_valid::<Update>(*u, &to, sender, conn)
+    }
+    (SharedAcceptedObjects::Like(l), Some("Note")) => {
+      receive_like_comment(&l, &conn, chat_server)?;
+      announce_activity_if_valid::<Like>(*l, &to, sender, conn)
+    }
+    (SharedAcceptedObjects::Dislike(d), Some("Note")) => {
+      receive_dislike_comment(&d, &conn, chat_server)?;
+      announce_activity_if_valid::<Dislike>(*d, &to, sender, conn)
+    }
+    (SharedAcceptedObjects::Delete(d), Some("Note")) => {
+      receive_delete_comment(&d, &conn, chat_server)?;
+      announce_activity_if_valid::<Delete>(*d, &to, sender, conn)
+    }
+    (SharedAcceptedObjects::Remove(r), Some("Note")) => {
+      receive_remove_comment(&r, &conn, chat_server)?;
+      announce_activity_if_valid::<Remove>(*r, &to, sender, conn)
+    }
+    (SharedAcceptedObjects::Delete(d), Some("Group")) => {
+      receive_delete_community(&d, &conn, chat_server)?;
+      announce_activity_if_valid::<Delete>(*d, &to, sender, conn)
+    }
+    (SharedAcceptedObjects::Remove(r), Some("Group")) => {
+      receive_remove_community(&r, &conn, chat_server)?;
+      announce_activity_if_valid::<Remove>(*r, &to, sender, conn)
+    }
+    (SharedAcceptedObjects::Undo(u), Some("Delete")) => {
+      receive_undo_delete(&u, &conn, chat_server)?;
+      announce_activity_if_valid::<Undo>(*u, &to, sender, conn)
+    }
+    (SharedAcceptedObjects::Undo(u), Some("Remove")) => {
+      receive_undo_remove(&u, &conn, chat_server)?;
+      announce_activity_if_valid::<Undo>(*u, &to, sender, conn)
+    }
+    (SharedAcceptedObjects::Undo(u), Some("Like")) => {
+      receive_undo_like(&u, &conn, chat_server)?;
+      announce_activity_if_valid::<Undo>(*u, &to, sender, conn)
+    }
+    (SharedAcceptedObjects::Announce(a), _) => receive_announce(a, &conn, chat_server),
+    (a, _) => receive_unhandled_activity(a),
+  }
+}
+
+// TODO: should pass in sender as ActorType, but thats a bit tricky in shared_inbox()
+fn announce_activity_if_valid<A>(
+  activity: A,
+  community_uri: &str,
+  sender: &str,
+  conn: &PgConnection,
+) -> Result<HttpResponse, Error>
+where
+  A: Activity + Base + Serialize + Debug,
+{
+  let community = Community::read_from_actor_id(conn, &community_uri)?;
+  if community.local {
+    let sending_user = get_or_fetch_and_upsert_remote_user(&sender.to_string(), &conn)?;
+    Community::do_announce(activity, &community, &sending_user, conn)
+  } else {
+    Ok(HttpResponse::NotFound().finish())
+  }
+}
+
+fn receive_announce(
+  announce: Box<Announce>,
+  conn: &PgConnection,
+  chat_server: ChatServerParam,
+) -> Result<HttpResponse, Error> {
+  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, &conn, chat_server),
+        Some("Note") => receive_create_comment(&create, &conn, chat_server),
+        _ => 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, &conn, chat_server),
+        Some("Note") => receive_update_comment(&update, &conn, chat_server),
+        _ => 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, &conn, chat_server),
+        Some("Note") => receive_like_comment(&like, &conn, chat_server),
+        _ => 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, &conn, chat_server),
+        Some("Note") => receive_dislike_comment(&dislike, &conn, chat_server),
+        _ => 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, &conn, chat_server),
+        Some("Note") => receive_delete_comment(&delete, &conn, chat_server),
+        _ => 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, &conn, chat_server),
+        Some("Note") => receive_remove_comment(&remove, &conn, chat_server),
+        _ => 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, &conn, chat_server),
+        Some("Remove") => receive_undo_remove(&undo, &conn, chat_server),
+        Some("Like") => receive_undo_like(&undo, &conn, chat_server),
+        _ => receive_unhandled_activity(announce),
+      }
+    }
+    _ => receive_unhandled_activity(announce),
+  }
+}
+
+fn receive_unhandled_activity<A>(activity: A) -> Result<HttpResponse, Error>
+where
+  A: Debug,
+{
+  debug!("received unhandled activity type: {:?}", activity);
+  Ok(HttpResponse::NotImplemented().finish())
+}
+
+fn receive_create_post(
+  create: &Create,
+  conn: &PgConnection,
+  chat_server: ChatServerParam,
+) -> Result<HttpResponse, Error> {
+  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, &conn)?;
+
+  insert_activity(&conn, user.id, &create, false)?;
+
+  let post = PostForm::from_apub(&page, &conn)?;
+  let inserted_post = Post::create(conn, &post)?;
+
+  // Refetch the view
+  let post_view = PostView::read(&conn, inserted_post.id, None)?;
+
+  let res = PostResponse { post: post_view };
+
+  chat_server.do_send(SendPost {
+    op: UserOperation::CreatePost,
+    post: res,
+    my_id: None,
+  });
+
+  Ok(HttpResponse::Ok().finish())
+}
+
+fn receive_create_comment(
+  create: &Create,
+  conn: &PgConnection,
+  chat_server: ChatServerParam,
+) -> Result<HttpResponse, Error> {
+  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, &conn)?;
+
+  insert_activity(&conn, user.id, &create, false)?;
+
+  let comment = CommentForm::from_apub(&note, &conn)?;
+  let inserted_comment = Comment::create(conn, &comment)?;
+  let post = Post::read(&conn, inserted_comment.post_id)?;
+
+  // Note:
+  // Although mentions could be gotten from the post tags (they are included there), or the ccs,
+  // Its much easier to scrape them from the comment body, since the API has to do that
+  // anyway.
+  let mentions = scrape_text_for_mentions(&inserted_comment.content);
+  let recipient_ids = send_local_notifs(&conn, &mentions, &inserted_comment, &user, &post);
+
+  // Refetch the view
+  let comment_view = CommentView::read(&conn, inserted_comment.id, None)?;
+
+  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())
+}
+
+fn receive_update_post(
+  update: &Update,
+  conn: &PgConnection,
+  chat_server: ChatServerParam,
+) -> Result<HttpResponse, Error> {
+  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, &conn)?;
+
+  insert_activity(&conn, user.id, &update, false)?;
+
+  let post = PostForm::from_apub(&page, conn)?;
+  let post_id = get_or_fetch_and_insert_remote_post(&post.ap_id, &conn)?.id;
+  Post::update(conn, post_id, &post)?;
+
+  // Refetch the view
+  let post_view = PostView::read(&conn, post_id, None)?;
+
+  let res = PostResponse { post: post_view };
+
+  chat_server.do_send(SendPost {
+    op: UserOperation::EditPost,
+    post: res,
+    my_id: None,
+  });
+
+  Ok(HttpResponse::Ok().finish())
+}
+
+fn receive_like_post(
+  like: &Like,
+
+  conn: &PgConnection,
+  chat_server: ChatServerParam,
+) -> Result<HttpResponse, Error> {
+  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, &conn)?;
+
+  insert_activity(&conn, user.id, &like, false)?;
+
+  let post = PostForm::from_apub(&page, conn)?;
+  let post_id = get_or_fetch_and_insert_remote_post(&post.ap_id, &conn)?.id;
+
+  let like_form = PostLikeForm {
+    post_id,
+    user_id: user.id,
+    score: 1,
+  };
+  PostLike::remove(&conn, &like_form)?;
+  PostLike::like(&conn, &like_form)?;
+
+  // Refetch the view
+  let post_view = PostView::read(&conn, post_id, None)?;
+
+  let res = PostResponse { post: post_view };
+
+  chat_server.do_send(SendPost {
+    op: UserOperation::CreatePostLike,
+    post: res,
+    my_id: None,
+  });
+
+  Ok(HttpResponse::Ok().finish())
+}
+
+fn receive_dislike_post(
+  dislike: &Dislike,
+
+  conn: &PgConnection,
+  chat_server: ChatServerParam,
+) -> Result<HttpResponse, Error> {
+  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, &conn)?;
+
+  insert_activity(&conn, user.id, &dislike, false)?;
+
+  let post = PostForm::from_apub(&page, conn)?;
+  let post_id = get_or_fetch_and_insert_remote_post(&post.ap_id, &conn)?.id;
+
+  let like_form = PostLikeForm {
+    post_id,
+    user_id: user.id,
+    score: -1,
+  };
+  PostLike::remove(&conn, &like_form)?;
+  PostLike::like(&conn, &like_form)?;
+
+  // Refetch the view
+  let post_view = PostView::read(&conn, post_id, None)?;
+
+  let res = PostResponse { post: post_view };
+
+  chat_server.do_send(SendPost {
+    op: UserOperation::CreatePostLike,
+    post: res,
+    my_id: None,
+  });
+
+  Ok(HttpResponse::Ok().finish())
+}
+
+fn receive_update_comment(
+  update: &Update,
+
+  conn: &PgConnection,
+  chat_server: ChatServerParam,
+) -> Result<HttpResponse, Error> {
+  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, &conn)?;
+
+  insert_activity(&conn, user.id, &update, false)?;
+
+  let comment = CommentForm::from_apub(&note, &conn)?;
+  let comment_id = get_or_fetch_and_insert_remote_comment(&comment.ap_id, &conn)?.id;
+  let updated_comment = Comment::update(conn, comment_id, &comment)?;
+  let post = Post::read(&conn, updated_comment.post_id)?;
+
+  let mentions = scrape_text_for_mentions(&updated_comment.content);
+  let recipient_ids = send_local_notifs(&conn, &mentions, &updated_comment, &user, &post);
+
+  // Refetch the view
+  let comment_view = CommentView::read(&conn, comment_id, None)?;
+
+  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())
+}
+
+fn receive_like_comment(
+  like: &Like,
+
+  conn: &PgConnection,
+  chat_server: ChatServerParam,
+) -> Result<HttpResponse, Error> {
+  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, &conn)?;
+
+  insert_activity(&conn, user.id, &like, false)?;
+
+  let comment = CommentForm::from_apub(&note, &conn)?;
+  let comment_id = get_or_fetch_and_insert_remote_comment(&comment.ap_id, &conn)?.id;
+  let like_form = CommentLikeForm {
+    comment_id,
+    post_id: comment.post_id,
+    user_id: user.id,
+    score: 1,
+  };
+  CommentLike::remove(&conn, &like_form)?;
+  CommentLike::like(&conn, &like_form)?;
+
+  // Refetch the view
+  let comment_view = CommentView::read(&conn, comment_id, None)?;
+
+  // 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())
+}
+
+fn receive_dislike_comment(
+  dislike: &Dislike,
+
+  conn: &PgConnection,
+  chat_server: ChatServerParam,
+) -> Result<HttpResponse, Error> {
+  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, &conn)?;
+
+  insert_activity(&conn, user.id, &dislike, false)?;
+
+  let comment = CommentForm::from_apub(&note, &conn)?;
+  let comment_id = get_or_fetch_and_insert_remote_comment(&comment.ap_id, &conn)?.id;
+  let like_form = CommentLikeForm {
+    comment_id,
+    post_id: comment.post_id,
+    user_id: user.id,
+    score: -1,
+  };
+  CommentLike::remove(&conn, &like_form)?;
+  CommentLike::like(&conn, &like_form)?;
+
+  // Refetch the view
+  let comment_view = CommentView::read(&conn, comment_id, None)?;
+
+  // 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())
+}
+
+fn receive_delete_community(
+  delete: &Delete,
+
+  conn: &PgConnection,
+  chat_server: ChatServerParam,
+) -> Result<HttpResponse, Error> {
+  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, &conn)?;
+
+  insert_activity(&conn, user.id, &delete, false)?;
+
+  let community_actor_id = CommunityForm::from_apub(&group, &conn)?.actor_id;
+  let community = Community::read_from_actor_id(conn, &community_actor_id)?;
+
+  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,
+  };
+
+  Community::update(&conn, community.id, &community_form)?;
+
+  let res = CommunityResponse {
+    community: CommunityView::read(&conn, community.id, None)?,
+  };
+
+  chat_server.do_send(SendCommunityRoomMessage {
+    op: UserOperation::EditCommunity,
+    response: res,
+    community_id: community.id,
+    my_id: None,
+  });
+
+  Ok(HttpResponse::Ok().finish())
+}
+
+fn receive_remove_community(
+  remove: &Remove,
+
+  conn: &PgConnection,
+  chat_server: ChatServerParam,
+) -> Result<HttpResponse, Error> {
+  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, &conn)?;
+
+  insert_activity(&conn, mod_.id, &remove, false)?;
+
+  let community_actor_id = CommunityForm::from_apub(&group, &conn)?.actor_id;
+  let community = Community::read_from_actor_id(conn, &community_actor_id)?;
+
+  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,
+  };
+
+  Community::update(&conn, community.id, &community_form)?;
+
+  let res = CommunityResponse {
+    community: CommunityView::read(&conn, community.id, None)?,
+  };
+
+  chat_server.do_send(SendCommunityRoomMessage {
+    op: UserOperation::EditCommunity,
+    response: res,
+    community_id: community.id,
+    my_id: None,
+  });
+
+  Ok(HttpResponse::Ok().finish())
+}
+
+fn receive_delete_post(
+  delete: &Delete,
+
+  conn: &PgConnection,
+  chat_server: ChatServerParam,
+) -> Result<HttpResponse, Error> {
+  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, &conn)?;
+
+  insert_activity(&conn, user.id, &delete, false)?;
+
+  let post_ap_id = PostForm::from_apub(&page, conn)?.ap_id;
+  let post = get_or_fetch_and_insert_remote_post(&post_ap_id, &conn)?;
+
+  let post_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,
+  };
+  Post::update(&conn, post.id, &post_form)?;
+
+  // Refetch the view
+  let post_view = PostView::read(&conn, post.id, None)?;
+
+  let res = PostResponse { post: post_view };
+
+  chat_server.do_send(SendPost {
+    op: UserOperation::EditPost,
+    post: res,
+    my_id: None,
+  });
+
+  Ok(HttpResponse::Ok().finish())
+}
+
+fn receive_remove_post(
+  remove: &Remove,
+
+  conn: &PgConnection,
+  chat_server: ChatServerParam,
+) -> Result<HttpResponse, Error> {
+  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, &conn)?;
+
+  insert_activity(&conn, mod_.id, &remove, false)?;
+
+  let post_ap_id = PostForm::from_apub(&page, conn)?.ap_id;
+  let post = get_or_fetch_and_insert_remote_post(&post_ap_id, &conn)?;
+
+  let post_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,
+  };
+  Post::update(&conn, post.id, &post_form)?;
+
+  // Refetch the view
+  let post_view = PostView::read(&conn, post.id, None)?;
+
+  let res = PostResponse { post: post_view };
+
+  chat_server.do_send(SendPost {
+    op: UserOperation::EditPost,
+    post: res,
+    my_id: None,
+  });
+
+  Ok(HttpResponse::Ok().finish())
+}
+
+fn receive_delete_comment(
+  delete: &Delete,
+
+  conn: &PgConnection,
+  chat_server: ChatServerParam,
+) -> Result<HttpResponse, Error> {
+  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, &conn)?;
+
+  insert_activity(&conn, user.id, &delete, false)?;
+
+  let comment_ap_id = CommentForm::from_apub(&note, &conn)?.ap_id;
+  let comment = get_or_fetch_and_insert_remote_comment(&comment_ap_id, &conn)?;
+  let comment_form = CommentForm {
+    content: comment.content.to_owned(),
+    parent_id: comment.parent_id,
+    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,
+  };
+  Comment::update(&conn, comment.id, &comment_form)?;
+
+  // Refetch the view
+  let comment_view = CommentView::read(&conn, comment.id, None)?;
+
+  // 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())
+}
+
+fn receive_remove_comment(
+  remove: &Remove,
+
+  conn: &PgConnection,
+  chat_server: ChatServerParam,
+) -> Result<HttpResponse, Error> {
+  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, &conn)?;
+
+  insert_activity(&conn, mod_.id, &remove, false)?;
+
+  let comment_ap_id = CommentForm::from_apub(&note, &conn)?.ap_id;
+  let comment = get_or_fetch_and_insert_remote_comment(&comment_ap_id, &conn)?;
+  let comment_form = CommentForm {
+    content: comment.content.to_owned(),
+    parent_id: comment.parent_id,
+    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,
+  };
+  Comment::update(&conn, comment.id, &comment_form)?;
+
+  // Refetch the view
+  let comment_view = CommentView::read(&conn, comment.id, None)?;
+
+  // 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())
+}
+
+fn receive_undo_delete(
+  undo: &Undo,
+
+  conn: &PgConnection,
+  chat_server: ChatServerParam,
+) -> Result<HttpResponse, Error> {
+  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, &conn, chat_server),
+    "Page" => receive_undo_delete_post(&delete, &conn, chat_server),
+    "Group" => receive_undo_delete_community(&delete, &conn, chat_server),
+    d => Err(format_err!("Undo Delete type {} not supported", d)),
+  }
+}
+
+fn receive_undo_remove(
+  undo: &Undo,
+
+  conn: &PgConnection,
+  chat_server: ChatServerParam,
+) -> Result<HttpResponse, Error> {
+  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, &conn, chat_server),
+    "Page" => receive_undo_remove_post(&remove, &conn, chat_server),
+    "Group" => receive_undo_remove_community(&remove, &conn, chat_server),
+    d => Err(format_err!("Undo Delete type {} not supported", d)),
+  }
+}
+
+fn receive_undo_delete_comment(
+  delete: &Delete,
+
+  conn: &PgConnection,
+  chat_server: ChatServerParam,
+) -> Result<HttpResponse, Error> {
+  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, &conn)?;
+
+  insert_activity(&conn, user.id, &delete, false)?;
+
+  let comment_ap_id = CommentForm::from_apub(&note, &conn)?.ap_id;
+  let comment = get_or_fetch_and_insert_remote_comment(&comment_ap_id, &conn)?;
+  let comment_form = CommentForm {
+    content: comment.content.to_owned(),
+    parent_id: comment.parent_id,
+    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,
+  };
+  Comment::update(&conn, comment.id, &comment_form)?;
+
+  // Refetch the view
+  let comment_view = CommentView::read(&conn, comment.id, None)?;
+
+  // 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())
+}
+
+fn receive_undo_remove_comment(
+  remove: &Remove,
+
+  conn: &PgConnection,
+  chat_server: ChatServerParam,
+) -> Result<HttpResponse, Error> {
+  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, &conn)?;
+
+  insert_activity(&conn, mod_.id, &remove, false)?;
+
+  let comment_ap_id = CommentForm::from_apub(&note, &conn)?.ap_id;
+  let comment = get_or_fetch_and_insert_remote_comment(&comment_ap_id, &conn)?;
+  let comment_form = CommentForm {
+    content: comment.content.to_owned(),
+    parent_id: comment.parent_id,
+    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,
+  };
+  Comment::update(&conn, comment.id, &comment_form)?;
+
+  // Refetch the view
+  let comment_view = CommentView::read(&conn, comment.id, None)?;
+
+  // 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())
+}
+
+fn receive_undo_delete_post(
+  delete: &Delete,
+
+  conn: &PgConnection,
+  chat_server: ChatServerParam,
+) -> Result<HttpResponse, Error> {
+  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, &conn)?;
+
+  insert_activity(&conn, user.id, &delete, false)?;
+
+  let post_ap_id = PostForm::from_apub(&page, conn)?.ap_id;
+  let post = get_or_fetch_and_insert_remote_post(&post_ap_id, &conn)?;
+
+  let post_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,
+  };
+  Post::update(&conn, post.id, &post_form)?;
+
+  // Refetch the view
+  let post_view = PostView::read(&conn, post.id, None)?;
+
+  let res = PostResponse { post: post_view };
+
+  chat_server.do_send(SendPost {
+    op: UserOperation::EditPost,
+    post: res,
+    my_id: None,
+  });
+
+  Ok(HttpResponse::Ok().finish())
+}
+
+fn receive_undo_remove_post(
+  remove: &Remove,
+
+  conn: &PgConnection,
+  chat_server: ChatServerParam,
+) -> Result<HttpResponse, Error> {
+  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, &conn)?;
+
+  insert_activity(&conn, mod_.id, &remove, false)?;
+
+  let post_ap_id = PostForm::from_apub(&page, conn)?.ap_id;
+  let post = get_or_fetch_and_insert_remote_post(&post_ap_id, &conn)?;
+
+  let post_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,
+  };
+  Post::update(&conn, post.id, &post_form)?;
+
+  // Refetch the view
+  let post_view = PostView::read(&conn, post.id, None)?;
+
+  let res = PostResponse { post: post_view };
+
+  chat_server.do_send(SendPost {
+    op: UserOperation::EditPost,
+    post: res,
+    my_id: None,
+  });
+
+  Ok(HttpResponse::Ok().finish())
+}
+
+fn receive_undo_delete_community(
+  delete: &Delete,
+
+  conn: &PgConnection,
+  chat_server: ChatServerParam,
+) -> Result<HttpResponse, Error> {
+  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, &conn)?;
+
+  insert_activity(&conn, user.id, &delete, false)?;
+
+  let community_actor_id = CommunityForm::from_apub(&group, &conn)?.actor_id;
+  let community = Community::read_from_actor_id(conn, &community_actor_id)?;
+
+  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,
+  };
+
+  Community::update(&conn, community.id, &community_form)?;
+
+  let res = CommunityResponse {
+    community: CommunityView::read(&conn, community.id, None)?,
+  };
+
+  chat_server.do_send(SendCommunityRoomMessage {
+    op: UserOperation::EditCommunity,
+    response: res,
+    community_id: community.id,
+    my_id: None,
+  });
+
+  Ok(HttpResponse::Ok().finish())
+}
+
+fn receive_undo_remove_community(
+  remove: &Remove,
+
+  conn: &PgConnection,
+  chat_server: ChatServerParam,
+) -> Result<HttpResponse, Error> {
+  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, &conn)?;
+
+  insert_activity(&conn, mod_.id, &remove, false)?;
+
+  let community_actor_id = CommunityForm::from_apub(&group, &conn)?.actor_id;
+  let community = Community::read_from_actor_id(conn, &community_actor_id)?;
+
+  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,
+  };
+
+  Community::update(&conn, community.id, &community_form)?;
+
+  let res = CommunityResponse {
+    community: CommunityView::read(&conn, community.id, None)?,
+  };
+
+  chat_server.do_send(SendCommunityRoomMessage {
+    op: UserOperation::EditCommunity,
+    response: res,
+    community_id: community.id,
+    my_id: None,
+  });
+
+  Ok(HttpResponse::Ok().finish())
+}
+
+fn receive_undo_like(
+  undo: &Undo,
+
+  conn: &PgConnection,
+  chat_server: ChatServerParam,
+) -> Result<HttpResponse, Error> {
+  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, &conn, chat_server),
+    "Page" => receive_undo_like_post(&like, &conn, chat_server),
+    d => Err(format_err!("Undo Delete type {} not supported", d)),
+  }
+}
+
+fn receive_undo_like_comment(
+  like: &Like,
+
+  conn: &PgConnection,
+  chat_server: ChatServerParam,
+) -> Result<HttpResponse, Error> {
+  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, &conn)?;
+
+  insert_activity(&conn, user.id, &like, false)?;
+
+  let comment = CommentForm::from_apub(&note, &conn)?;
+  let comment_id = get_or_fetch_and_insert_remote_comment(&comment.ap_id, &conn)?.id;
+  let like_form = CommentLikeForm {
+    comment_id,
+    post_id: comment.post_id,
+    user_id: user.id,
+    score: 0,
+  };
+  CommentLike::remove(&conn, &like_form)?;
+
+  // Refetch the view
+  let comment_view = CommentView::read(&conn, comment_id, None)?;
+
+  // 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())
+}
+
+fn receive_undo_like_post(
+  like: &Like,
+
+  conn: &PgConnection,
+  chat_server: ChatServerParam,
+) -> Result<HttpResponse, Error> {
+  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, &conn)?;
+
+  insert_activity(&conn, user.id, &like, false)?;
+
+  let post = PostForm::from_apub(&page, conn)?;
+  let post_id = get_or_fetch_and_insert_remote_post(&post.ap_id, &conn)?.id;
+
+  let like_form = PostLikeForm {
+    post_id,
+    user_id: user.id,
+    score: 1,
+  };
+  PostLike::remove(&conn, &like_form)?;
+
+  // Refetch the view
+  let post_view = PostView::read(&conn, post_id, None)?;
+
+  let res = PostResponse { post: post_view };
+
+  chat_server.do_send(SendPost {
+    op: UserOperation::CreatePostLike,
+    post: res,
+    my_id: None,
+  });
+
+  Ok(HttpResponse::Ok().finish())
+}
index 5f2421f11cdb57c9ae6b7997a6f993859b3c8111..c840cc22d88b149c220f9b4c2f13f11b7183bf00 100644 (file)
-use crate::apub::make_apub_endpoint;
-use crate::db::establish_unpooled_connection;
-use crate::db::user::User_;
-use crate::to_datetime_utc;
-use activitypub::{actor::Person, context};
-use actix_web::body::Body;
-use actix_web::web::Path;
-use actix_web::HttpResponse;
+use crate::{
+  apub::{
+    activities::send_activity,
+    create_apub_response,
+    extensions::signatures::PublicKey,
+    ActorType,
+    FromApub,
+    PersonExt,
+    ToApub,
+  },
+  convert_datetime,
+  db::{
+    activity::insert_activity,
+    user::{UserForm, User_},
+  },
+  naive_now,
+  routes::DbPoolParam,
+};
+use activitystreams::{
+  actor::{properties::ApActorProperties, Person},
+  context,
+  endpoint::EndpointProperties,
+  object::{properties::ObjectProperties, AnyImage, Image},
+  primitives::XsdAnyUri,
+};
+use activitystreams_ext::Ext2;
+use activitystreams_new::{
+  activity::{Follow, Undo},
+  object::Tombstone,
+  prelude::*,
+};
+use actix_web::{body::Body, web::Path, HttpResponse, Result};
+use diesel::PgConnection;
+use failure::Error;
 use serde::Deserialize;
 
-impl User_ {
-  pub fn as_person(&self) -> Person {
-    let base_url = make_apub_endpoint("u", &self.name);
+#[derive(Deserialize)]
+pub struct UserQuery {
+  user_name: String,
+}
+
+impl ToApub for User_ {
+  type Response = PersonExt;
+
+  // Turn a Lemmy Community into an ActivityPub group that can be sent out over the network.
+  fn to_apub(&self, _conn: &PgConnection) -> Result<PersonExt, Error> {
+    // TODO go through all these to_string and to_owned()
     let mut person = Person::default();
-    person.object_props.set_context_object(context()).ok();
-    person.object_props.set_id_string(base_url.to_string()).ok();
-    person
-      .object_props
-      .set_name_string(self.name.to_owned())
-      .ok();
-    person
-      .object_props
-      .set_published_utctime(to_datetime_utc(self.published))
-      .ok();
-    if let Some(updated) = self.updated {
-      person
-        .object_props
-        .set_updated_utctime(to_datetime_utc(updated))
-        .ok();
+    let oprops: &mut ObjectProperties = person.as_mut();
+    oprops
+      .set_context_xsd_any_uri(context())?
+      .set_id(self.actor_id.to_string())?
+      .set_name_xsd_string(self.name.to_owned())?
+      .set_published(convert_datetime(self.published))?;
+
+    if let Some(u) = self.updated {
+      oprops.set_updated(convert_datetime(u))?;
     }
 
-    person
-      .ap_actor_props
-      .set_inbox_string(format!("{}/inbox", &base_url))
-      .ok();
-    person
-      .ap_actor_props
-      .set_outbox_string(format!("{}/outbox", &base_url))
-      .ok();
-    person
-      .ap_actor_props
-      .set_following_string(format!("{}/following", &base_url))
-      .ok();
-    person
-      .ap_actor_props
-      .set_liked_string(format!("{}/liked", &base_url))
-      .ok();
     if let Some(i) = &self.preferred_username {
-      person
-        .ap_actor_props
-        .set_preferred_username_string(i.to_string())
-        .ok();
+      oprops.set_name_xsd_string(i.to_owned())?;
+    }
+
+    if let Some(avatar_url) = &self.avatar {
+      let mut image = Image::new();
+      image
+        .object_props
+        .set_url_xsd_any_uri(avatar_url.to_owned())?;
+      let any_image = AnyImage::from_concrete(image)?;
+      oprops.set_icon_any_image(any_image)?;
     }
 
-    person
+    let mut endpoint_props = EndpointProperties::default();
+
+    endpoint_props.set_shared_inbox(self.get_shared_inbox_url())?;
+
+    let mut actor_props = ApActorProperties::default();
+
+    actor_props
+      .set_inbox(self.get_inbox_url())?
+      .set_outbox(self.get_outbox_url())?
+      .set_endpoints(endpoint_props)?
+      .set_followers(self.get_followers_url())?
+      .set_following(self.get_following_url())?
+      .set_liked(self.get_liked_url())?;
+
+    Ok(Ext2::new(person, actor_props, self.get_public_key_ext()))
+  }
+  fn to_tombstone(&self) -> Result<Tombstone, Error> {
+    unimplemented!()
   }
 }
 
-#[derive(Deserialize)]
-pub struct UserQuery {
-  user_name: String,
+impl ActorType for User_ {
+  fn actor_id(&self) -> String {
+    self.actor_id.to_owned()
+  }
+
+  fn public_key(&self) -> String {
+    self.public_key.to_owned().unwrap()
+  }
+
+  fn private_key(&self) -> String {
+    self.private_key.to_owned().unwrap()
+  }
+
+  /// As a given local user, send out a follow request to a remote community.
+  fn send_follow(&self, follow_actor_id: &str, conn: &PgConnection) -> Result<(), Error> {
+    let id = format!("{}/follow/{}", self.actor_id, uuid::Uuid::new_v4());
+    let mut follow = Follow::new(self.actor_id.to_owned(), follow_actor_id);
+    follow.set_context(context()).set_id(id.parse()?);
+    let to = format!("{}/inbox", follow_actor_id);
+
+    insert_activity(&conn, self.id, &follow, true)?;
+
+    send_activity(&follow, self, vec![to])?;
+    Ok(())
+  }
+
+  fn send_unfollow(&self, follow_actor_id: &str, conn: &PgConnection) -> Result<(), Error> {
+    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);
+
+    // 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()?);
+
+    insert_activity(&conn, self.id, &undo, true)?;
+
+    send_activity(&undo, self, vec![to])?;
+    Ok(())
+  }
+
+  fn send_delete(&self, _creator: &User_, _conn: &PgConnection) -> Result<(), Error> {
+    unimplemented!()
+  }
+
+  fn send_undo_delete(&self, _creator: &User_, _conn: &PgConnection) -> Result<(), Error> {
+    unimplemented!()
+  }
+
+  fn send_remove(&self, _creator: &User_, _conn: &PgConnection) -> Result<(), Error> {
+    unimplemented!()
+  }
+
+  fn send_undo_remove(&self, _creator: &User_, _conn: &PgConnection) -> Result<(), Error> {
+    unimplemented!()
+  }
+
+  fn send_accept_follow(&self, _follow: &Follow, _conn: &PgConnection) -> Result<(), Error> {
+    unimplemented!()
+  }
+
+  fn get_follower_inboxes(&self, _conn: &PgConnection) -> Result<Vec<String>, Error> {
+    unimplemented!()
+  }
 }
 
-pub async fn get_apub_user(info: Path<UserQuery>) -> HttpResponse<Body> {
-  let connection = establish_unpooled_connection();
+impl FromApub for UserForm {
+  type ApubType = PersonExt;
+  /// Parse an ActivityPub person received from another instance into a Lemmy user.
+  fn from_apub(person: &PersonExt, _conn: &PgConnection) -> Result<Self, Error> {
+    let oprops = &person.inner.object_props;
+    let aprops = &person.ext_one;
+    let public_key: &PublicKey = &person.ext_two.public_key;
+
+    let avatar = match oprops.get_icon_any_image() {
+      Some(any_image) => any_image
+        .to_owned()
+        .into_concrete::<Image>()?
+        .object_props
+        .get_url_xsd_any_uri()
+        .map(|u| u.to_string()),
+      None => None,
+    };
 
-  if let Ok(user) = User_::find_by_email_or_username(&connection, &info.user_name) {
-    HttpResponse::Ok()
-      .content_type("application/activity+json")
-      .body(serde_json::to_string(&user.as_person()).unwrap())
-  } else {
-    HttpResponse::NotFound().finish()
+    Ok(UserForm {
+      name: oprops.get_name_xsd_string().unwrap().to_string(),
+      preferred_username: aprops.get_preferred_username().map(|u| u.to_string()),
+      password_encrypted: "".to_string(),
+      admin: false,
+      banned: false,
+      email: None,
+      avatar,
+      updated: oprops
+        .get_updated()
+        .map(|u| u.as_ref().to_owned().naive_local()),
+      show_nsfw: false,
+      theme: "".to_string(),
+      default_sort_type: 0,
+      default_listing_type: 0,
+      lang: "".to_string(),
+      show_avatars: false,
+      send_notifications_to_email: false,
+      matrix_user_id: None,
+      actor_id: oprops.get_id().unwrap().to_string(),
+      bio: oprops.get_summary_xsd_string().map(|s| s.to_string()),
+      local: false,
+      private_key: None,
+      public_key: Some(public_key.to_owned().public_key_pem),
+      last_refreshed_at: Some(naive_now()),
+    })
   }
 }
+
+/// Return the user json over HTTP.
+pub async fn get_apub_user_http(
+  info: Path<UserQuery>,
+  db: DbPoolParam,
+) -> Result<HttpResponse<Body>, Error> {
+  let user = User_::find_by_email_or_username(&&db.get()?, &info.user_name)?;
+  let u = user.to_apub(&db.get().unwrap())?;
+  Ok(create_apub_response(&u))
+}
diff --git a/server/src/apub/user_inbox.rs b/server/src/apub/user_inbox.rs
new file mode 100644 (file)
index 0000000..f60a2ba
--- /dev/null
@@ -0,0 +1,312 @@
+use crate::{
+  api::user::PrivateMessageResponse,
+  apub::{
+    extensions::signatures::verify,
+    fetcher::{get_or_fetch_and_upsert_remote_community, get_or_fetch_and_upsert_remote_user},
+    FromApub,
+  },
+  db::{
+    activity::insert_activity,
+    community::{CommunityFollower, CommunityFollowerForm},
+    private_message::{PrivateMessage, PrivateMessageForm},
+    private_message_view::PrivateMessageView,
+    user::User_,
+    Crud,
+    Followable,
+  },
+  naive_now,
+  routes::{ChatServerParam, DbPoolParam},
+  websocket::{server::SendUserRoomMessage, UserOperation},
+};
+use activitystreams::{
+  activity::{Accept, Create, Delete, Undo, Update},
+  object::Note,
+};
+use actix_web::{web, HttpRequest, HttpResponse, Result};
+use diesel::PgConnection;
+use failure::{Error, _core::fmt::Debug};
+use log::debug;
+use serde::Deserialize;
+
+#[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>,
+  db: DbPoolParam,
+  chat_server: ChatServerParam,
+) -> Result<HttpResponse, Error> {
+  // TODO: would be nice if we could do the signature check here, but we cant access the actor property
+  let input = input.into_inner();
+  let conn = &db.get().unwrap();
+  let username = path.into_inner();
+  debug!("User {} received activity: {:?}", &username, &input);
+
+  match input {
+    UserAcceptedObjects::Accept(a) => receive_accept(&a, &request, &username, &conn),
+    UserAcceptedObjects::Create(c) => {
+      receive_create_private_message(&c, &request, &conn, chat_server)
+    }
+    UserAcceptedObjects::Update(u) => {
+      receive_update_private_message(&u, &request, &conn, chat_server)
+    }
+    UserAcceptedObjects::Delete(d) => {
+      receive_delete_private_message(&d, &request, &conn, chat_server)
+    }
+    UserAcceptedObjects::Undo(u) => {
+      receive_undo_delete_private_message(&u, &request, &conn, chat_server)
+    }
+  }
+}
+
+/// Handle accepted follows.
+fn receive_accept(
+  accept: &Accept,
+  request: &HttpRequest,
+  username: &str,
+  conn: &PgConnection,
+) -> Result<HttpResponse, Error> {
+  let community_uri = accept
+    .accept_props
+    .get_actor_xsd_any_uri()
+    .unwrap()
+    .to_string();
+
+  let community = get_or_fetch_and_upsert_remote_community(&community_uri, conn)?;
+  verify(request, &community)?;
+
+  let user = User_::read_from_name(&conn, username)?;
+
+  insert_activity(&conn, community.creator_id, &accept, false)?;
+
+  // 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
+  CommunityFollower::follow(&conn, &community_follower_form)?;
+
+  // TODO: make sure that we actually requested a follow
+  Ok(HttpResponse::Ok().finish())
+}
+
+fn receive_create_private_message(
+  create: &Create,
+  request: &HttpRequest,
+  conn: &PgConnection,
+  chat_server: ChatServerParam,
+) -> Result<HttpResponse, Error> {
+  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, &conn)?;
+  verify(request, &user)?;
+
+  insert_activity(&conn, user.id, &create, false)?;
+
+  let private_message = PrivateMessageForm::from_apub(&note, &conn)?;
+  let inserted_private_message = PrivateMessage::create(&conn, &private_message)?;
+
+  let message = PrivateMessageView::read(&conn, inserted_private_message.id)?;
+
+  let res = PrivateMessageResponse {
+    message: message.to_owned(),
+  };
+
+  chat_server.do_send(SendUserRoomMessage {
+    op: UserOperation::CreatePrivateMessage,
+    response: res,
+    recipient_id: message.recipient_id,
+    my_id: None,
+  });
+
+  Ok(HttpResponse::Ok().finish())
+}
+
+fn receive_update_private_message(
+  update: &Update,
+  request: &HttpRequest,
+  conn: &PgConnection,
+  chat_server: ChatServerParam,
+) -> Result<HttpResponse, Error> {
+  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, &conn)?;
+  verify(request, &user)?;
+
+  insert_activity(&conn, user.id, &update, false)?;
+
+  let private_message = PrivateMessageForm::from_apub(&note, &conn)?;
+  let private_message_id = PrivateMessage::read_from_apub_id(&conn, &private_message.ap_id)?.id;
+  PrivateMessage::update(conn, private_message_id, &private_message)?;
+
+  let message = PrivateMessageView::read(&conn, private_message_id)?;
+
+  let res = PrivateMessageResponse {
+    message: message.to_owned(),
+  };
+
+  chat_server.do_send(SendUserRoomMessage {
+    op: UserOperation::EditPrivateMessage,
+    response: res,
+    recipient_id: message.recipient_id,
+    my_id: None,
+  });
+
+  Ok(HttpResponse::Ok().finish())
+}
+
+fn receive_delete_private_message(
+  delete: &Delete,
+  request: &HttpRequest,
+  conn: &PgConnection,
+  chat_server: ChatServerParam,
+) -> Result<HttpResponse, Error> {
+  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, &conn)?;
+  verify(request, &user)?;
+
+  insert_activity(&conn, user.id, &delete, false)?;
+
+  let private_message = PrivateMessageForm::from_apub(&note, &conn)?;
+  let private_message_id = PrivateMessage::read_from_apub_id(&conn, &private_message.ap_id)?.id;
+  let private_message_form = PrivateMessageForm {
+    content: private_message.content,
+    recipient_id: private_message.recipient_id,
+    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()),
+  };
+  PrivateMessage::update(conn, private_message_id, &private_message_form)?;
+
+  let message = PrivateMessageView::read(&conn, private_message_id)?;
+
+  let res = PrivateMessageResponse {
+    message: message.to_owned(),
+  };
+
+  chat_server.do_send(SendUserRoomMessage {
+    op: UserOperation::EditPrivateMessage,
+    response: res,
+    recipient_id: message.recipient_id,
+    my_id: None,
+  });
+
+  Ok(HttpResponse::Ok().finish())
+}
+
+fn receive_undo_delete_private_message(
+  undo: &Undo,
+  request: &HttpRequest,
+  conn: &PgConnection,
+  chat_server: ChatServerParam,
+) -> Result<HttpResponse, Error> {
+  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, &conn)?;
+  verify(request, &user)?;
+
+  insert_activity(&conn, user.id, &delete, false)?;
+
+  let private_message = PrivateMessageForm::from_apub(&note, &conn)?;
+  let private_message_id = PrivateMessage::read_from_apub_id(&conn, &private_message.ap_id)?.id;
+  let private_message_form = PrivateMessageForm {
+    content: private_message.content,
+    recipient_id: private_message.recipient_id,
+    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()),
+  };
+  PrivateMessage::update(conn, private_message_id, &private_message_form)?;
+
+  let message = PrivateMessageView::read(&conn, private_message_id)?;
+
+  let res = PrivateMessageResponse {
+    message: message.to_owned(),
+  };
+
+  chat_server.do_send(SendUserRoomMessage {
+    op: UserOperation::EditPrivateMessage,
+    response: res,
+    recipient_id: message.recipient_id,
+    my_id: None,
+  });
+
+  Ok(HttpResponse::Ok().finish())
+}
diff --git a/server/src/db/activity.rs b/server/src/db/activity.rs
new file mode 100644 (file)
index 0000000..714c820
--- /dev/null
@@ -0,0 +1,149 @@
+use crate::{db::Crud, schema::activity};
+use diesel::{dsl::*, result::Error, *};
+use failure::_core::fmt::Debug;
+use log::debug;
+use serde::{Deserialize, Serialize};
+use serde_json::Value;
+
+#[derive(Queryable, Identifiable, PartialEq, Debug, Serialize, Deserialize)]
+#[table_name = "activity"]
+pub struct Activity {
+  pub id: i32,
+  pub user_id: i32,
+  pub data: Value,
+  pub local: bool,
+  pub published: chrono::NaiveDateTime,
+  pub updated: Option<chrono::NaiveDateTime>,
+}
+
+#[derive(Insertable, AsChangeset, Clone, Serialize, Deserialize)]
+#[table_name = "activity"]
+pub struct ActivityForm {
+  pub user_id: i32,
+  pub data: Value,
+  pub local: bool,
+  pub updated: Option<chrono::NaiveDateTime>,
+}
+
+impl Crud<ActivityForm> for Activity {
+  fn read(conn: &PgConnection, activity_id: i32) -> Result<Self, Error> {
+    use crate::schema::activity::dsl::*;
+    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)
+      .values(new_activity)
+      .get_result::<Self>(conn)
+  }
+
+  fn update(
+    conn: &PgConnection,
+    activity_id: i32,
+    new_activity: &ActivityForm,
+  ) -> Result<Self, Error> {
+    use crate::schema::activity::dsl::*;
+    diesel::update(activity.find(activity_id))
+      .set(new_activity)
+      .get_result::<Self>(conn)
+  }
+}
+
+pub fn insert_activity<T>(
+  conn: &PgConnection,
+  user_id: i32,
+  data: &T,
+  local: bool,
+) -> Result<(), failure::Error>
+where
+  T: Serialize + Debug,
+{
+  let activity_form = ActivityForm {
+    user_id,
+    data: serde_json::to_value(&data)?,
+    local,
+    updated: None,
+  };
+  debug!("inserting activity for user {}, data {:?}", user_id, data);
+  Activity::create(&conn, &activity_form)?;
+  Ok(())
+}
+
+#[cfg(test)]
+mod tests {
+  use super::{super::user::*, *};
+  use crate::db::{establish_unpooled_connection, Crud, ListingType, SortType};
+
+  #[test]
+  fn test_crud() {
+    let conn = establish_unpooled_connection();
+
+    let creator_form = UserForm {
+      name: "activity_creator_pm".into(),
+      preferred_username: None,
+      password_encrypted: "nope".into(),
+      email: None,
+      matrix_user_id: None,
+      avatar: None,
+      admin: false,
+      banned: false,
+      updated: None,
+      show_nsfw: false,
+      theme: "darkly".into(),
+      default_sort_type: SortType::Hot as i16,
+      default_listing_type: ListingType::Subscribed as i16,
+      lang: "browser".into(),
+      show_avatars: true,
+      send_notifications_to_email: false,
+      actor_id: "changeme".into(),
+      bio: None,
+      local: true,
+      private_key: None,
+      public_key: None,
+      last_refreshed_at: None,
+    };
+
+    let inserted_creator = User_::create(&conn, &creator_form).unwrap();
+
+    let test_json: Value = serde_json::from_str(
+      r#"{
+    "street": "Article Circle Expressway 1",
+    "city": "North Pole",
+    "postcode": "99705",
+    "state": "Alaska"
+}"#,
+    )
+    .unwrap();
+    let activity_form = ActivityForm {
+      user_id: inserted_creator.id,
+      data: test_json.to_owned(),
+      local: true,
+      updated: None,
+    };
+
+    let inserted_activity = Activity::create(&conn, &activity_form).unwrap();
+
+    let expected_activity = Activity {
+      id: inserted_activity.id,
+      user_id: inserted_creator.id,
+      data: test_json,
+      local: true,
+      published: inserted_activity.published,
+      updated: None,
+    };
+
+    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 408c8231bfb215e43319cb4aac62dd86baeb48ea..ff49bbbee3057194d284ef349c2e104902a50a54 100644 (file)
@@ -1,6 +1,9 @@
-use super::*;
-use crate::schema::category;
-use crate::schema::category::dsl::*;
+use crate::{
+  db::Crud,
+  schema::{category, category::dsl::*},
+};
+use diesel::{dsl::*, result::Error, *};
+use serde::{Deserialize, Serialize};
 
 #[derive(Queryable, Identifiable, PartialEq, Debug, Serialize, Deserialize)]
 #[table_name = "category"]
@@ -50,6 +53,8 @@ impl Category {
 #[cfg(test)]
 mod tests {
   use super::*;
+  use crate::db::establish_unpooled_connection;
+
   #[test]
   fn test_crud() {
     let conn = establish_unpooled_connection();
diff --git a/server/src/db/code_migrations.rs b/server/src/db/code_migrations.rs
new file mode 100644 (file)
index 0000000..204bfe7
--- /dev/null
@@ -0,0 +1,173 @@
+// This is for db migrations that require code
+use super::{
+  comment::Comment,
+  community::{Community, CommunityForm},
+  post::Post,
+  private_message::PrivateMessage,
+  user::{UserForm, User_},
+};
+use crate::{
+  apub::{extensions::signatures::generate_actor_keypair, make_apub_endpoint, EndpointType},
+  db::Crud,
+  naive_now,
+};
+use diesel::*;
+use failure::Error;
+use log::info;
+
+pub fn run_advanced_migrations(conn: &PgConnection) -> Result<(), Error> {
+  user_updates_2020_04_02(conn)?;
+  community_updates_2020_04_02(conn)?;
+  post_updates_2020_04_03(conn)?;
+  comment_updates_2020_04_03(conn)?;
+  private_message_updates_2020_05_05(conn)?;
+
+  Ok(())
+}
+
+fn user_updates_2020_04_02(conn: &PgConnection) -> Result<(), Error> {
+  use crate::schema::user_::dsl::*;
+
+  info!("Running user_updates_2020_04_02");
+
+  // Update the actor_id, private_key, and public_key, last_refreshed_at
+  let incorrect_users = user_
+    .filter(actor_id.eq("changeme"))
+    .filter(local.eq(true))
+    .load::<User_>(conn)?;
+
+  for cuser in &incorrect_users {
+    let keypair = generate_actor_keypair()?;
+
+    let form = UserForm {
+      name: cuser.name.to_owned(),
+      email: cuser.email.to_owned(),
+      matrix_user_id: cuser.matrix_user_id.to_owned(),
+      avatar: cuser.avatar.to_owned(),
+      password_encrypted: cuser.password_encrypted.to_owned(),
+      preferred_username: cuser.preferred_username.to_owned(),
+      updated: None,
+      admin: cuser.admin,
+      banned: cuser.banned,
+      show_nsfw: cuser.show_nsfw,
+      theme: cuser.theme.to_owned(),
+      default_sort_type: cuser.default_sort_type,
+      default_listing_type: cuser.default_listing_type,
+      lang: cuser.lang.to_owned(),
+      show_avatars: cuser.show_avatars,
+      send_notifications_to_email: cuser.send_notifications_to_email,
+      actor_id: make_apub_endpoint(EndpointType::User, &cuser.name).to_string(),
+      bio: cuser.bio.to_owned(),
+      local: cuser.local,
+      private_key: Some(keypair.private_key),
+      public_key: Some(keypair.public_key),
+      last_refreshed_at: Some(naive_now()),
+    };
+
+    User_::update(&conn, cuser.id, &form)?;
+  }
+
+  info!("{} user rows updated.", incorrect_users.len());
+
+  Ok(())
+}
+
+fn community_updates_2020_04_02(conn: &PgConnection) -> Result<(), Error> {
+  use crate::schema::community::dsl::*;
+
+  info!("Running community_updates_2020_04_02");
+
+  // Update the actor_id, private_key, and public_key, last_refreshed_at
+  let incorrect_communities = community
+    .filter(actor_id.eq("changeme"))
+    .filter(local.eq(true))
+    .load::<Community>(conn)?;
+
+  for ccommunity in &incorrect_communities {
+    let keypair = generate_actor_keypair()?;
+
+    let form = CommunityForm {
+      name: ccommunity.name.to_owned(),
+      title: ccommunity.title.to_owned(),
+      description: ccommunity.description.to_owned(),
+      category_id: ccommunity.category_id,
+      creator_id: ccommunity.creator_id,
+      removed: None,
+      deleted: None,
+      nsfw: ccommunity.nsfw,
+      updated: None,
+      actor_id: make_apub_endpoint(EndpointType::Community, &ccommunity.name).to_string(),
+      local: ccommunity.local,
+      private_key: Some(keypair.private_key),
+      public_key: Some(keypair.public_key),
+      last_refreshed_at: Some(naive_now()),
+      published: None,
+    };
+
+    Community::update(&conn, ccommunity.id, &form)?;
+  }
+
+  info!("{} community rows updated.", incorrect_communities.len());
+
+  Ok(())
+}
+
+fn post_updates_2020_04_03(conn: &PgConnection) -> Result<(), Error> {
+  use crate::schema::post::dsl::*;
+
+  info!("Running post_updates_2020_04_03");
+
+  // Update the ap_id
+  let incorrect_posts = post
+    .filter(ap_id.eq("changeme"))
+    .filter(local.eq(true))
+    .load::<Post>(conn)?;
+
+  for cpost in &incorrect_posts {
+    Post::update_ap_id(&conn, cpost.id)?;
+  }
+
+  info!("{} post rows updated.", incorrect_posts.len());
+
+  Ok(())
+}
+
+fn comment_updates_2020_04_03(conn: &PgConnection) -> Result<(), Error> {
+  use crate::schema::comment::dsl::*;
+
+  info!("Running comment_updates_2020_04_03");
+
+  // Update the ap_id
+  let incorrect_comments = comment
+    .filter(ap_id.eq("changeme"))
+    .filter(local.eq(true))
+    .load::<Comment>(conn)?;
+
+  for ccomment in &incorrect_comments {
+    Comment::update_ap_id(&conn, ccomment.id)?;
+  }
+
+  info!("{} comment rows updated.", incorrect_comments.len());
+
+  Ok(())
+}
+
+fn private_message_updates_2020_05_05(conn: &PgConnection) -> Result<(), Error> {
+  use crate::schema::private_message::dsl::*;
+
+  info!("Running private_message_updates_2020_05_05");
+
+  // Update the ap_id
+  let incorrect_pms = private_message
+    .filter(ap_id.eq("changeme"))
+    .filter(local.eq(true))
+    .load::<PrivateMessage>(conn)?;
+
+  for cpm in &incorrect_pms {
+    PrivateMessage::update_ap_id(&conn, cpm.id)?;
+  }
+
+  info!("{} private message rows updated.", incorrect_pms.len());
+
+  Ok(())
+}
index c9bfbac6c313d17a862d4fa1833f26d43d80fd90..ddf1feef862ea10fb77acc31b41a6def01da7758 100644 (file)
@@ -1,6 +1,9 @@
-use super::post::Post;
-use super::*;
-use crate::schema::{comment, comment_like, comment_saved};
+use super::{post::Post, *};
+use crate::{
+  apub::{make_apub_endpoint, EndpointType},
+  naive_now,
+  schema::{comment, comment_like, comment_saved},
+};
 
 // WITH RECURSIVE MyTree AS (
 //     SELECT * FROM comment WHERE parent_id IS NULL
@@ -19,10 +22,12 @@ pub struct Comment {
   pub parent_id: Option<i32>,
   pub content: String,
   pub removed: bool,
-  pub read: bool,
+  pub read: bool, // Whether the recipient has read the comment or not
   pub published: chrono::NaiveDateTime,
   pub updated: Option<chrono::NaiveDateTime>,
   pub deleted: bool,
+  pub ap_id: String,
+  pub local: bool,
 }
 
 #[derive(Insertable, AsChangeset, Clone)]
@@ -34,8 +39,11 @@ pub struct CommentForm {
   pub content: String,
   pub removed: Option<bool>,
   pub read: Option<bool>,
+  pub published: Option<chrono::NaiveDateTime>,
   pub updated: Option<chrono::NaiveDateTime>,
   pub deleted: Option<bool>,
+  pub ap_id: String,
+  pub local: bool,
 }
 
 impl Crud<CommentForm> for Comment {
@@ -68,6 +76,42 @@ impl Crud<CommentForm> for Comment {
   }
 }
 
+impl Comment {
+  pub fn update_ap_id(conn: &PgConnection, comment_id: i32) -> Result<Self, Error> {
+    use crate::schema::comment::dsl::*;
+
+    let apid = make_apub_endpoint(EndpointType::Comment, &comment_id.to_string()).to_string();
+    diesel::update(comment.find(comment_id))
+      .set(ap_id.eq(apid))
+      .get_result::<Self>(conn)
+  }
+
+  pub fn read_from_apub_id(conn: &PgConnection, object_id: &str) -> Result<Self, Error> {
+    use crate::schema::comment::dsl::*;
+    comment.filter(ap_id.eq(object_id)).first::<Self>(conn)
+  }
+
+  pub fn mark_as_read(conn: &PgConnection, comment_id: i32) -> Result<Self, Error> {
+    use crate::schema::comment::dsl::*;
+
+    diesel::update(comment.find(comment_id))
+      .set(read.eq(true))
+      .get_result::<Self>(conn)
+  }
+
+  pub fn permadelete(conn: &PgConnection, comment_id: i32) -> 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()),
+      ))
+      .get_result::<Self>(conn)
+  }
+}
+
 #[derive(Identifiable, Queryable, Associations, PartialEq, Debug, Clone)]
 #[belongs_to(Comment)]
 #[table_name = "comment_like"]
@@ -160,17 +204,16 @@ impl Saveable<CommentSavedForm> for CommentSaved {
 
 #[cfg(test)]
 mod tests {
-  use super::super::community::*;
-  use super::super::post::*;
-  use super::super::user::*;
-  use super::*;
+  use super::{
+    super::{community::*, post::*, user::*},
+    *,
+  };
   #[test]
   fn test_crud() {
     let conn = establish_unpooled_connection();
 
     let new_user = UserForm {
       name: "terry".into(),
-      fedi_name: "rrf".into(),
       preferred_username: None,
       password_encrypted: "nope".into(),
       email: None,
@@ -186,6 +229,12 @@ mod tests {
       lang: "browser".into(),
       show_avatars: true,
       send_notifications_to_email: false,
+      actor_id: "changeme".into(),
+      bio: None,
+      local: true,
+      private_key: None,
+      public_key: None,
+      last_refreshed_at: None,
     };
 
     let inserted_user = User_::create(&conn, &new_user).unwrap();
@@ -200,6 +249,12 @@ mod tests {
       deleted: None,
       updated: None,
       nsfw: false,
+      actor_id: "changeme".into(),
+      local: true,
+      private_key: None,
+      public_key: None,
+      last_refreshed_at: None,
+      published: None,
     };
 
     let inserted_community = Community::create(&conn, &new_community).unwrap();
@@ -220,6 +275,9 @@ mod tests {
       embed_description: None,
       embed_html: None,
       thumbnail_url: None,
+      ap_id: "changeme".into(),
+      local: true,
+      published: None,
     };
 
     let inserted_post = Post::create(&conn, &new_post).unwrap();
@@ -232,7 +290,10 @@ mod tests {
       deleted: None,
       read: None,
       parent_id: None,
+      published: None,
       updated: None,
+      ap_id: "changeme".into(),
+      local: true,
     };
 
     let inserted_comment = Comment::create(&conn, &comment_form).unwrap();
@@ -248,6 +309,8 @@ mod tests {
       parent_id: None,
       published: inserted_comment.published,
       updated: None,
+      ap_id: "changeme".into(),
+      local: true,
     };
 
     let child_comment_form = CommentForm {
@@ -258,7 +321,10 @@ mod tests {
       removed: None,
       deleted: None,
       read: None,
+      published: None,
       updated: None,
+      ap_id: "changeme".into(),
+      local: true,
     };
 
     let inserted_child_comment = Comment::create(&conn, &child_comment_form).unwrap();
index 85b41d826401bb97acbb002f839a7baae31bc17f..52f7d9490777520556f7e065918f194ea717d58c 100644 (file)
@@ -1,5 +1,6 @@
-use super::*;
-use diesel::pg::Pg;
+use crate::db::{fuzzy_search, limit_and_offset, ListingType, MaybeOptional, SortType};
+use diesel::{dsl::*, pg::Pg, result::Error, *};
+use serde::{Deserialize, Serialize};
 
 // The faked schema since diesel doesn't do views
 table! {
@@ -14,10 +15,16 @@ table! {
     published -> Timestamp,
     updated -> Nullable<Timestamp>,
     deleted -> Bool,
+    ap_id -> Text,
+    local -> Bool,
     community_id -> Int4,
+    community_actor_id -> Text,
+    community_local -> Bool,
     community_name -> Varchar,
     banned -> Bool,
     banned_from_community -> Bool,
+    creator_actor_id -> Text,
+    creator_local -> Bool,
     creator_name -> Varchar,
     creator_avatar -> Nullable<Text>,
     score -> BigInt,
@@ -43,10 +50,16 @@ table! {
     published -> Timestamp,
     updated -> Nullable<Timestamp>,
     deleted -> Bool,
+    ap_id -> Text,
+    local -> Bool,
     community_id -> Int4,
+    community_actor_id -> Text,
+    community_local -> Bool,
     community_name -> Varchar,
     banned -> Bool,
     banned_from_community -> Bool,
+    creator_actor_id -> Text,
+    creator_local -> Bool,
     creator_name -> Varchar,
     creator_avatar -> Nullable<Text>,
     score -> BigInt,
@@ -75,10 +88,16 @@ pub struct CommentView {
   pub published: chrono::NaiveDateTime,
   pub updated: Option<chrono::NaiveDateTime>,
   pub deleted: bool,
+  pub ap_id: String,
+  pub local: bool,
   pub community_id: i32,
+  pub community_actor_id: String,
+  pub community_local: bool,
   pub community_name: String,
   pub banned: bool,
   pub banned_from_community: bool,
+  pub creator_actor_id: String,
+  pub creator_local: bool,
   pub creator_name: String,
   pub creator_avatar: Option<String>,
   pub score: i64,
@@ -282,10 +301,16 @@ table! {
     published -> Timestamp,
     updated -> Nullable<Timestamp>,
     deleted -> Bool,
+    ap_id -> Text,
+    local -> Bool,
     community_id -> Int4,
+    community_actor_id -> Text,
+    community_local -> Bool,
     community_name -> Varchar,
     banned -> Bool,
     banned_from_community -> Bool,
+    creator_actor_id -> Text,
+    creator_local -> Bool,
     creator_name -> Varchar,
     creator_avatar -> Nullable<Text>,
     score -> BigInt,
@@ -315,10 +340,16 @@ pub struct ReplyView {
   pub published: chrono::NaiveDateTime,
   pub updated: Option<chrono::NaiveDateTime>,
   pub deleted: bool,
+  pub ap_id: String,
+  pub local: bool,
   pub community_id: i32,
+  pub community_actor_id: String,
+  pub community_local: bool,
   pub community_name: String,
   pub banned: bool,
   pub banned_from_community: bool,
+  pub creator_actor_id: String,
+  pub creator_local: bool,
   pub creator_name: String,
   pub creator_avatar: Option<String>,
   pub score: i64,
@@ -423,18 +454,18 @@ impl<'a> ReplyQueryBuilder<'a> {
 
 #[cfg(test)]
 mod tests {
-  use super::super::comment::*;
-  use super::super::community::*;
-  use super::super::post::*;
-  use super::super::user::*;
-  use super::*;
+  use super::{
+    super::{comment::*, community::*, post::*, user::*},
+    *,
+  };
+  use crate::db::{establish_unpooled_connection, Crud, Likeable};
+
   #[test]
   fn test_crud() {
     let conn = establish_unpooled_connection();
 
     let new_user = UserForm {
       name: "timmy".into(),
-      fedi_name: "rrf".into(),
       preferred_username: None,
       password_encrypted: "nope".into(),
       email: None,
@@ -450,6 +481,12 @@ mod tests {
       lang: "browser".into(),
       show_avatars: true,
       send_notifications_to_email: false,
+      actor_id: "changeme".into(),
+      bio: None,
+      local: true,
+      private_key: None,
+      public_key: None,
+      last_refreshed_at: None,
     };
 
     let inserted_user = User_::create(&conn, &new_user).unwrap();
@@ -464,6 +501,12 @@ mod tests {
       deleted: None,
       updated: None,
       nsfw: false,
+      actor_id: "changeme".into(),
+      local: true,
+      private_key: None,
+      public_key: None,
+      last_refreshed_at: None,
+      published: None,
     };
 
     let inserted_community = Community::create(&conn, &new_community).unwrap();
@@ -484,6 +527,9 @@ mod tests {
       embed_description: None,
       embed_html: None,
       thumbnail_url: None,
+      ap_id: "changeme".into(),
+      local: true,
+      published: None,
     };
 
     let inserted_post = Post::create(&conn, &new_post).unwrap();
@@ -496,7 +542,10 @@ mod tests {
       removed: None,
       deleted: None,
       read: None,
+      published: None,
       updated: None,
+      ap_id: "changeme".into(),
+      local: true,
     };
 
     let inserted_comment = Comment::create(&conn, &comment_form).unwrap();
@@ -535,6 +584,12 @@ mod tests {
       my_vote: None,
       subscribed: None,
       saved: None,
+      ap_id: "changeme".to_string(),
+      local: true,
+      community_actor_id: inserted_community.actor_id.to_owned(),
+      community_local: true,
+      creator_actor_id: inserted_user.actor_id.to_owned(),
+      creator_local: true,
     };
 
     let expected_comment_view_with_user = CommentView {
@@ -562,6 +617,12 @@ mod tests {
       my_vote: Some(1),
       subscribed: None,
       saved: None,
+      ap_id: "changeme".to_string(),
+      local: true,
+      community_actor_id: inserted_community.actor_id.to_owned(),
+      community_local: true,
+      creator_actor_id: inserted_user.actor_id.to_owned(),
+      creator_local: true,
     };
 
     let mut read_comment_views_no_user = CommentQueryBuilder::create(&conn)
index 6350096358bd7a8f16092a0f35db1cedb90bfb53..885c9779b0475ad38475f4bcf20751bb4df45d1e 100644 (file)
@@ -1,5 +1,9 @@
-use super::*;
-use crate::schema::{community, community_follower, community_moderator, community_user_ban};
+use crate::{
+  db::{Bannable, Crud, Followable, Joinable},
+  schema::{community, community_follower, community_moderator, community_user_ban},
+};
+use diesel::{dsl::*, result::Error, *};
+use serde::{Deserialize, Serialize};
 
 #[derive(Queryable, Identifiable, PartialEq, Debug, Serialize, Deserialize)]
 #[table_name = "community"]
@@ -15,9 +19,15 @@ pub struct Community {
   pub updated: Option<chrono::NaiveDateTime>,
   pub deleted: bool,
   pub nsfw: bool,
+  pub actor_id: String,
+  pub local: bool,
+  pub private_key: Option<String>,
+  pub public_key: Option<String>,
+  pub last_refreshed_at: chrono::NaiveDateTime,
 }
 
-#[derive(Insertable, AsChangeset, Clone, Serialize, Deserialize)]
+// TODO add better delete, remove, lock actions here.
+#[derive(Insertable, AsChangeset, Clone, Serialize, Deserialize, Debug)]
 #[table_name = "community"]
 pub struct CommunityForm {
   pub name: String,
@@ -26,9 +36,15 @@ pub struct CommunityForm {
   pub category_id: i32,
   pub creator_id: i32,
   pub removed: Option<bool>,
+  pub published: Option<chrono::NaiveDateTime>,
   pub updated: Option<chrono::NaiveDateTime>,
   pub deleted: Option<bool>,
   pub nsfw: bool,
+  pub actor_id: String,
+  pub local: bool,
+  pub private_key: Option<String>,
+  pub public_key: Option<String>,
+  pub last_refreshed_at: Option<chrono::NaiveDateTime>,
 }
 
 impl Crud<CommunityForm> for Community {
@@ -62,15 +78,23 @@ impl Crud<CommunityForm> for Community {
 }
 
 impl Community {
-  pub fn read_from_name(conn: &PgConnection, community_name: String) -> Result<Self, Error> {
+  pub fn read_from_name(conn: &PgConnection, community_name: &str) -> Result<Self, Error> {
     use crate::schema::community::dsl::*;
     community
       .filter(name.eq(community_name))
       .first::<Self>(conn)
   }
 
-  pub fn get_url(&self) -> String {
-    format!("https://{}/c/{}", Settings::get().hostname, self.name)
+  pub fn read_from_actor_id(conn: &PgConnection, community_id: &str) -> Result<Self, Error> {
+    use crate::schema::community::dsl::*;
+    community
+      .filter(actor_id.eq(community_id))
+      .first::<Self>(conn)
+  }
+
+  pub fn list_local(conn: &PgConnection) -> Result<Vec<Self>, Error> {
+    use crate::schema::community::dsl::*;
+    community.filter(local.eq(true)).load::<Community>(conn)
   }
 }
 
@@ -192,7 +216,7 @@ impl Followable<CommunityFollowerForm> for CommunityFollower {
       .values(community_follower_form)
       .get_result::<Self>(conn)
   }
-  fn ignore(
+  fn unfollow(
     conn: &PgConnection,
     community_follower_form: &CommunityFollowerForm,
   ) -> Result<usize, Error> {
@@ -208,15 +232,15 @@ impl Followable<CommunityFollowerForm> for CommunityFollower {
 
 #[cfg(test)]
 mod tests {
-  use super::super::user::*;
-  use super::*;
+  use super::{super::user::*, *};
+  use crate::db::{establish_unpooled_connection, ListingType, SortType};
+
   #[test]
   fn test_crud() {
     let conn = establish_unpooled_connection();
 
     let new_user = UserForm {
       name: "bobbee".into(),
-      fedi_name: "rrf".into(),
       preferred_username: None,
       password_encrypted: "nope".into(),
       email: None,
@@ -232,6 +256,12 @@ mod tests {
       lang: "browser".into(),
       show_avatars: true,
       send_notifications_to_email: false,
+      actor_id: "changeme".into(),
+      bio: None,
+      local: true,
+      private_key: None,
+      public_key: None,
+      last_refreshed_at: None,
     };
 
     let inserted_user = User_::create(&conn, &new_user).unwrap();
@@ -246,6 +276,12 @@ mod tests {
       removed: None,
       deleted: None,
       updated: None,
+      actor_id: "changeme".into(),
+      local: true,
+      private_key: None,
+      public_key: None,
+      last_refreshed_at: None,
+      published: None,
     };
 
     let inserted_community = Community::create(&conn, &new_community).unwrap();
@@ -262,6 +298,11 @@ mod tests {
       deleted: false,
       published: inserted_community.published,
       updated: None,
+      actor_id: "changeme".into(),
+      local: true,
+      private_key: None,
+      public_key: None,
+      last_refreshed_at: inserted_community.published,
     };
 
     let community_follower_form = CommunityFollowerForm {
@@ -311,7 +352,7 @@ mod tests {
     let read_community = Community::read(&conn, inserted_community.id).unwrap();
     let updated_community =
       Community::update(&conn, inserted_community.id, &new_community).unwrap();
-    let ignored_community = CommunityFollower::ignore(&conn, &community_follower_form).unwrap();
+    let ignored_community = CommunityFollower::unfollow(&conn, &community_follower_form).unwrap();
     let left_community = CommunityModerator::leave(&conn, &community_user_form).unwrap();
     let unban = CommunityUserBan::unban(&conn, &community_user_ban_form).unwrap();
     let num_deleted = Community::delete(&conn, inserted_community.id).unwrap();
index f24c922c397db237173a4e3c04222899f675e6dc..ea7b2a7cadfa51950221307b63c188befa6d541c 100644 (file)
@@ -1,6 +1,7 @@
 use super::community_view::community_mview::BoxedQuery;
-use super::*;
-use diesel::pg::Pg;
+use crate::db::{fuzzy_search, limit_and_offset, MaybeOptional, SortType};
+use diesel::{pg::Pg, result::Error, *};
+use serde::{Deserialize, Serialize};
 
 table! {
   community_view (id) {
@@ -15,6 +16,11 @@ table! {
     updated -> Nullable<Timestamp>,
     deleted -> Bool,
     nsfw -> Bool,
+    actor_id -> Text,
+    local -> Bool,
+    last_refreshed_at -> Timestamp,
+    creator_actor_id -> Text,
+    creator_local -> Bool,
     creator_name -> Varchar,
     creator_avatar -> Nullable<Text>,
     category_name -> Varchar,
@@ -40,6 +46,11 @@ table! {
     updated -> Nullable<Timestamp>,
     deleted -> Bool,
     nsfw -> Bool,
+    actor_id -> Text,
+    local -> Bool,
+    last_refreshed_at -> Timestamp,
+    creator_actor_id -> Text,
+    creator_local -> Bool,
     creator_name -> Varchar,
     creator_avatar -> Nullable<Text>,
     category_name -> Varchar,
@@ -58,8 +69,12 @@ table! {
     community_id -> Int4,
     user_id -> Int4,
     published -> Timestamp,
+    user_actor_id -> Text,
+    user_local -> Bool,
     user_name -> Varchar,
     avatar -> Nullable<Text>,
+    community_actor_id -> Text,
+    community_local -> Bool,
     community_name -> Varchar,
   }
 }
@@ -70,8 +85,12 @@ table! {
     community_id -> Int4,
     user_id -> Int4,
     published -> Timestamp,
+    user_actor_id -> Text,
+    user_local -> Bool,
     user_name -> Varchar,
     avatar -> Nullable<Text>,
+    community_actor_id -> Text,
+    community_local -> Bool,
     community_name -> Varchar,
   }
 }
@@ -82,8 +101,12 @@ table! {
     community_id -> Int4,
     user_id -> Int4,
     published -> Timestamp,
+    user_actor_id -> Text,
+    user_local -> Bool,
     user_name -> Varchar,
     avatar -> Nullable<Text>,
+    community_actor_id -> Text,
+    community_local -> Bool,
     community_name -> Varchar,
   }
 }
@@ -104,6 +127,11 @@ pub struct CommunityView {
   pub updated: Option<chrono::NaiveDateTime>,
   pub deleted: bool,
   pub nsfw: bool,
+  pub actor_id: String,
+  pub local: bool,
+  pub last_refreshed_at: chrono::NaiveDateTime,
+  pub creator_actor_id: String,
+  pub creator_local: bool,
   pub creator_name: String,
   pub creator_avatar: Option<String>,
   pub category_name: String,
@@ -257,8 +285,12 @@ pub struct CommunityModeratorView {
   pub community_id: i32,
   pub user_id: i32,
   pub published: chrono::NaiveDateTime,
+  pub user_actor_id: String,
+  pub user_local: bool,
   pub user_name: String,
   pub avatar: Option<String>,
+  pub community_actor_id: String,
+  pub community_local: bool,
   pub community_name: String,
 }
 
@@ -287,8 +319,12 @@ pub struct CommunityFollowerView {
   pub community_id: i32,
   pub user_id: i32,
   pub published: chrono::NaiveDateTime,
+  pub user_actor_id: String,
+  pub user_local: bool,
   pub user_name: String,
   pub avatar: Option<String>,
+  pub community_actor_id: String,
+  pub community_local: bool,
   pub community_name: String,
 }
 
@@ -317,8 +353,12 @@ pub struct CommunityUserBanView {
   pub community_id: i32,
   pub user_id: i32,
   pub published: chrono::NaiveDateTime,
+  pub user_actor_id: String,
+  pub user_local: bool,
   pub user_name: String,
   pub avatar: Option<String>,
+  pub community_actor_id: String,
+  pub community_local: bool,
   pub community_name: String,
 }
 
index e0d358ffe7e286915493a024673d8126e5b571c8..da69f8dcdc88dcf925dda24edb93dc8aa5ffae0a 100644 (file)
@@ -1,10 +1,10 @@
 use crate::settings::Settings;
-use diesel::dsl::*;
-use diesel::result::Error;
-use diesel::*;
+use diesel::{dsl::*, result::Error, *};
 use serde::{Deserialize, Serialize};
 
+pub mod activity;
 pub mod category;
+pub mod code_migrations;
 pub mod comment;
 pub mod comment_view;
 pub mod community;
@@ -42,7 +42,7 @@ pub trait Followable<T> {
   fn follow(conn: &PgConnection, form: &T) -> Result<Self, Error>
   where
     Self: Sized;
-  fn ignore(conn: &PgConnection, form: &T) -> Result<usize, Error>
+  fn unfollow(conn: &PgConnection, form: &T) -> Result<usize, Error>
   where
     Self: Sized;
 }
index a8c3df4f15ba8302c901d90148cc329fe9c5cab9..d0f589e30fdfe543d960890ef9726ea6fcf94920 100644 (file)
@@ -1,8 +1,19 @@
-use super::*;
-use crate::schema::{
-  mod_add, mod_add_community, mod_ban, mod_ban_from_community, mod_lock_post, mod_remove_comment,
-  mod_remove_community, mod_remove_post, mod_sticky_post,
+use crate::{
+  db::Crud,
+  schema::{
+    mod_add,
+    mod_add_community,
+    mod_ban,
+    mod_ban_from_community,
+    mod_lock_post,
+    mod_remove_comment,
+    mod_remove_community,
+    mod_remove_post,
+    mod_sticky_post,
+  },
 };
+use diesel::{dsl::*, result::Error, *};
+use serde::{Deserialize, Serialize};
 
 #[derive(Queryable, Identifiable, PartialEq, Debug, Serialize, Deserialize)]
 #[table_name = "mod_remove_post"]
@@ -426,11 +437,12 @@ impl Crud<ModAddForm> for ModAdd {
 
 #[cfg(test)]
 mod tests {
-  use super::super::comment::*;
-  use super::super::community::*;
-  use super::super::post::*;
-  use super::super::user::*;
-  use super::*;
+  use super::{
+    super::{comment::*, community::*, post::*, user::*},
+    *,
+  };
+  use crate::db::{establish_unpooled_connection, ListingType, SortType};
+
   // use Crud;
   #[test]
   fn test_crud() {
@@ -438,7 +450,6 @@ mod tests {
 
     let new_mod = UserForm {
       name: "the mod".into(),
-      fedi_name: "rrf".into(),
       preferred_username: None,
       password_encrypted: "nope".into(),
       email: None,
@@ -454,13 +465,18 @@ mod tests {
       lang: "browser".into(),
       show_avatars: true,
       send_notifications_to_email: false,
+      actor_id: "changeme".into(),
+      bio: None,
+      local: true,
+      private_key: None,
+      public_key: None,
+      last_refreshed_at: None,
     };
 
     let inserted_mod = User_::create(&conn, &new_mod).unwrap();
 
     let new_user = UserForm {
       name: "jim2".into(),
-      fedi_name: "rrf".into(),
       preferred_username: None,
       password_encrypted: "nope".into(),
       email: None,
@@ -476,6 +492,12 @@ mod tests {
       lang: "browser".into(),
       show_avatars: true,
       send_notifications_to_email: false,
+      actor_id: "changeme".into(),
+      bio: None,
+      local: true,
+      private_key: None,
+      public_key: None,
+      last_refreshed_at: None,
     };
 
     let inserted_user = User_::create(&conn, &new_user).unwrap();
@@ -490,6 +512,12 @@ mod tests {
       deleted: None,
       updated: None,
       nsfw: false,
+      actor_id: "changeme".into(),
+      local: true,
+      private_key: None,
+      public_key: None,
+      last_refreshed_at: None,
+      published: None,
     };
 
     let inserted_community = Community::create(&conn, &new_community).unwrap();
@@ -510,6 +538,9 @@ mod tests {
       embed_description: None,
       embed_html: None,
       thumbnail_url: None,
+      ap_id: "changeme".into(),
+      local: true,
+      published: None,
     };
 
     let inserted_post = Post::create(&conn, &new_post).unwrap();
@@ -522,7 +553,10 @@ mod tests {
       deleted: None,
       read: None,
       parent_id: None,
+      published: None,
       updated: None,
+      ap_id: "changeme".into(),
+      local: true,
     };
 
     let inserted_comment = Comment::create(&conn, &comment_form).unwrap();
index 0413e0b10b7a104eea72b9bae49cf0d90e4dcb42..f5b109fe6160f91437dfd147c41f96cea37e196d 100644 (file)
@@ -1,4 +1,6 @@
-use super::*;
+use crate::db::limit_and_offset;
+use diesel::{result::Error, *};
+use serde::{Deserialize, Serialize};
 
 table! {
   mod_remove_post_view (id) {
index 6951fd39936912c789866bd53d12c13c2554aa6f..32d7f187d0ad491f463e85ef6cab22d28aa5c507 100644 (file)
@@ -1,6 +1,8 @@
-use super::*;
-use crate::schema::password_reset_request;
-use crate::schema::password_reset_request::dsl::*;
+use crate::{
+  db::Crud,
+  schema::{password_reset_request, password_reset_request::dsl::*},
+};
+use diesel::{dsl::*, result::Error, *};
 use sha2::{Digest, Sha256};
 
 #[derive(Queryable, Identifiable, PartialEq, Debug)]
@@ -79,8 +81,8 @@ impl PasswordResetRequest {
 
 #[cfg(test)]
 mod tests {
-  use super::super::user::*;
-  use super::*;
+  use super::{super::user::*, *};
+  use crate::db::{establish_unpooled_connection, ListingType, SortType};
 
   #[test]
   fn test_crud() {
@@ -88,7 +90,6 @@ mod tests {
 
     let new_user = UserForm {
       name: "thommy prw".into(),
-      fedi_name: "rrf".into(),
       preferred_username: None,
       password_encrypted: "nope".into(),
       email: None,
@@ -104,6 +105,12 @@ mod tests {
       lang: "browser".into(),
       show_avatars: true,
       send_notifications_to_email: false,
+      actor_id: "changeme".into(),
+      bio: None,
+      local: true,
+      private_key: None,
+      public_key: None,
+      last_refreshed_at: None,
     };
 
     let inserted_user = User_::create(&conn, &new_user).unwrap();
index ffde14d3d702bbe427ca2b6afc933ae1ff6cfd35..d12f98d81542fdb526642db75092df13a5557dcb 100644 (file)
@@ -1,5 +1,11 @@
-use super::*;
-use crate::schema::{post, post_like, post_read, post_saved};
+use crate::{
+  apub::{make_apub_endpoint, EndpointType},
+  db::{Crud, Likeable, Readable, Saveable},
+  naive_now,
+  schema::{post, post_like, post_read, post_saved},
+};
+use diesel::{dsl::*, result::Error, *};
+use serde::{Deserialize, Serialize};
 
 #[derive(Queryable, Identifiable, PartialEq, Debug, Serialize, Deserialize)]
 #[table_name = "post"]
@@ -21,9 +27,11 @@ pub struct Post {
   pub embed_description: Option<String>,
   pub embed_html: Option<String>,
   pub thumbnail_url: Option<String>,
+  pub ap_id: String,
+  pub local: bool,
 }
 
-#[derive(Insertable, AsChangeset, Clone)]
+#[derive(Insertable, AsChangeset, Clone, Debug)]
 #[table_name = "post"]
 pub struct PostForm {
   pub name: String,
@@ -33,6 +41,7 @@ pub struct PostForm {
   pub community_id: i32,
   pub removed: Option<bool>,
   pub locked: Option<bool>,
+  pub published: Option<chrono::NaiveDateTime>,
   pub updated: Option<chrono::NaiveDateTime>,
   pub deleted: Option<bool>,
   pub nsfw: bool,
@@ -41,6 +50,56 @@ pub struct PostForm {
   pub embed_description: Option<String>,
   pub embed_html: Option<String>,
   pub thumbnail_url: Option<String>,
+  pub ap_id: String,
+  pub local: bool,
+}
+
+impl Post {
+  pub fn read(conn: &PgConnection, post_id: i32) -> Result<Self, Error> {
+    use crate::schema::post::dsl::*;
+    post.filter(id.eq(post_id)).first::<Self>(conn)
+  }
+
+  pub fn list_for_community(
+    conn: &PgConnection,
+    the_community_id: i32,
+  ) -> Result<Vec<Self>, Error> {
+    use crate::schema::post::dsl::*;
+    post
+      .filter(community_id.eq(the_community_id))
+      .load::<Self>(conn)
+  }
+
+  pub fn read_from_apub_id(conn: &PgConnection, object_id: &str) -> Result<Self, Error> {
+    use crate::schema::post::dsl::*;
+    post.filter(ap_id.eq(object_id)).first::<Self>(conn)
+  }
+
+  pub fn update_ap_id(conn: &PgConnection, post_id: i32) -> Result<Self, Error> {
+    use crate::schema::post::dsl::*;
+
+    let apid = make_apub_endpoint(EndpointType::Post, &post_id.to_string()).to_string();
+    diesel::update(post.find(post_id))
+      .set(ap_id.eq(apid))
+      .get_result::<Self>(conn)
+  }
+
+  pub fn permadelete(conn: &PgConnection, post_id: i32) -> Result<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))
+      .set((
+        name.eq(perma_deleted),
+        url.eq(perma_deleted_url),
+        body.eq(perma_deleted),
+        deleted.eq(true),
+        updated.eq(naive_now()),
+      ))
+      .get_result::<Self>(conn)
+  }
 }
 
 impl Crud<PostForm> for Post {
@@ -182,16 +241,18 @@ impl Readable<PostReadForm> for PostRead {
 
 #[cfg(test)]
 mod tests {
-  use super::super::community::*;
-  use super::super::user::*;
-  use super::*;
+  use super::{
+    super::{community::*, user::*},
+    *,
+  };
+  use crate::db::{establish_unpooled_connection, ListingType, SortType};
+
   #[test]
   fn test_crud() {
     let conn = establish_unpooled_connection();
 
     let new_user = UserForm {
       name: "jim".into(),
-      fedi_name: "rrf".into(),
       preferred_username: None,
       password_encrypted: "nope".into(),
       email: None,
@@ -207,6 +268,12 @@ mod tests {
       lang: "browser".into(),
       show_avatars: true,
       send_notifications_to_email: false,
+      actor_id: "changeme".into(),
+      bio: None,
+      local: true,
+      private_key: None,
+      public_key: None,
+      last_refreshed_at: None,
     };
 
     let inserted_user = User_::create(&conn, &new_user).unwrap();
@@ -221,6 +288,12 @@ mod tests {
       deleted: None,
       updated: None,
       nsfw: false,
+      actor_id: "changeme".into(),
+      local: true,
+      private_key: None,
+      public_key: None,
+      last_refreshed_at: None,
+      published: None,
     };
 
     let inserted_community = Community::create(&conn, &new_community).unwrap();
@@ -241,6 +314,9 @@ mod tests {
       embed_description: None,
       embed_html: None,
       thumbnail_url: None,
+      ap_id: "changeme".into(),
+      local: true,
+      published: None,
     };
 
     let inserted_post = Post::create(&conn, &new_post).unwrap();
@@ -263,6 +339,8 @@ mod tests {
       embed_description: None,
       embed_html: None,
       thumbnail_url: None,
+      ap_id: "changeme".into(),
+      local: true,
     };
 
     // Post Like
index f48f4f680ccc8a80e65df1c2c8b66d8d26a965f6..0948894221f24c3440c992b92733f3906f9ded08 100644 (file)
@@ -1,6 +1,7 @@
 use super::post_view::post_mview::BoxedQuery;
-use super::*;
-use diesel::pg::Pg;
+use crate::db::{fuzzy_search, limit_and_offset, ListingType, MaybeOptional, SortType};
+use diesel::{dsl::*, pg::Pg, result::Error, *};
+use serde::{Deserialize, Serialize};
 
 // The faked schema since diesel doesn't do views
 table! {
@@ -22,10 +23,16 @@ table! {
     embed_description -> Nullable<Text>,
     embed_html -> Nullable<Text>,
     thumbnail_url -> Nullable<Text>,
+    ap_id -> Text,
+    local -> Bool,
     banned -> Bool,
     banned_from_community -> Bool,
+    creator_actor_id -> Text,
+    creator_local -> Bool,
     creator_name -> Varchar,
     creator_avatar -> Nullable<Text>,
+    community_actor_id -> Text,
+    community_local -> Bool,
     community_name -> Varchar,
     community_removed -> Bool,
     community_deleted -> Bool,
@@ -63,10 +70,16 @@ table! {
     embed_description -> Nullable<Text>,
     embed_html -> Nullable<Text>,
     thumbnail_url -> Nullable<Text>,
+    ap_id -> Text,
+    local -> Bool,
     banned -> Bool,
     banned_from_community -> Bool,
+    creator_actor_id -> Text,
+    creator_local -> Bool,
     creator_name -> Varchar,
     creator_avatar -> Nullable<Text>,
+    community_actor_id -> Text,
+    community_local -> Bool,
     community_name -> Varchar,
     community_removed -> Bool,
     community_deleted -> Bool,
@@ -107,10 +120,16 @@ pub struct PostView {
   pub embed_description: Option<String>,
   pub embed_html: Option<String>,
   pub thumbnail_url: Option<String>,
+  pub ap_id: String,
+  pub local: bool,
   pub banned: bool,
   pub banned_from_community: bool,
+  pub creator_actor_id: String,
+  pub creator_local: bool,
   pub creator_name: String,
   pub creator_avatar: Option<String>,
+  pub community_actor_id: String,
+  pub community_local: bool,
   pub community_name: String,
   pub community_removed: bool,
   pub community_deleted: bool,
@@ -345,10 +364,12 @@ impl PostView {
 
 #[cfg(test)]
 mod tests {
-  use super::super::community::*;
-  use super::super::post::*;
-  use super::super::user::*;
-  use super::*;
+  use super::{
+    super::{community::*, post::*, user::*},
+    *,
+  };
+  use crate::db::{establish_unpooled_connection, Crud, Likeable};
+
   #[test]
   fn test_crud() {
     let conn = establish_unpooled_connection();
@@ -359,7 +380,6 @@ mod tests {
 
     let new_user = UserForm {
       name: user_name.to_owned(),
-      fedi_name: "rrf".into(),
       preferred_username: None,
       password_encrypted: "nope".into(),
       email: None,
@@ -375,6 +395,12 @@ mod tests {
       lang: "browser".into(),
       show_avatars: true,
       send_notifications_to_email: false,
+      actor_id: "changeme".into(),
+      bio: None,
+      local: true,
+      private_key: None,
+      public_key: None,
+      last_refreshed_at: None,
     };
 
     let inserted_user = User_::create(&conn, &new_user).unwrap();
@@ -389,6 +415,12 @@ mod tests {
       deleted: None,
       updated: None,
       nsfw: false,
+      actor_id: "changeme".into(),
+      local: true,
+      private_key: None,
+      public_key: None,
+      last_refreshed_at: None,
+      published: None,
     };
 
     let inserted_community = Community::create(&conn, &new_community).unwrap();
@@ -409,6 +441,9 @@ mod tests {
       embed_description: None,
       embed_html: None,
       thumbnail_url: None,
+      ap_id: "changeme".into(),
+      local: true,
+      published: None,
     };
 
     let inserted_post = Post::create(&conn, &new_post).unwrap();
@@ -473,6 +508,12 @@ mod tests {
       embed_description: None,
       embed_html: None,
       thumbnail_url: None,
+      ap_id: "changeme".to_string(),
+      local: true,
+      creator_actor_id: inserted_user.actor_id.to_owned(),
+      creator_local: true,
+      community_actor_id: inserted_community.actor_id.to_owned(),
+      community_local: true,
     };
 
     let expected_post_listing_with_user = PostView {
@@ -512,6 +553,12 @@ mod tests {
       embed_description: None,
       embed_html: None,
       thumbnail_url: None,
+      ap_id: "changeme".to_string(),
+      local: true,
+      creator_actor_id: inserted_user.actor_id.to_owned(),
+      creator_local: true,
+      community_actor_id: inserted_community.actor_id.to_owned(),
+      community_local: true,
     };
 
     let read_post_listings_with_user = PostQueryBuilder::create(&conn)
index cc073b594e23befdb885b6b0250cea3a3b2e9394..e8b28485eee7cf9f0dd98fbc01c4777fcc4b381b 100644 (file)
@@ -1,5 +1,10 @@
-use super::*;
-use crate::schema::private_message;
+use crate::{
+  apub::{make_apub_endpoint, EndpointType},
+  db::Crud,
+  schema::private_message,
+};
+use diesel::{dsl::*, result::Error, *};
+use serde::{Deserialize, Serialize};
 
 #[derive(Queryable, Identifiable, PartialEq, Debug, Serialize, Deserialize)]
 #[table_name = "private_message"]
@@ -12,6 +17,8 @@ pub struct PrivateMessage {
   pub read: bool,
   pub published: chrono::NaiveDateTime,
   pub updated: Option<chrono::NaiveDateTime>,
+  pub ap_id: String,
+  pub local: bool,
 }
 
 #[derive(Insertable, AsChangeset, Clone)]
@@ -19,10 +26,13 @@ pub struct PrivateMessage {
 pub struct PrivateMessageForm {
   pub creator_id: i32,
   pub recipient_id: i32,
-  pub content: Option<String>,
+  pub content: String,
   pub deleted: Option<bool>,
   pub read: Option<bool>,
+  pub published: Option<chrono::NaiveDateTime>,
   pub updated: Option<chrono::NaiveDateTime>,
+  pub ap_id: String,
+  pub local: bool,
 }
 
 impl Crud<PrivateMessageForm> for PrivateMessage {
@@ -55,17 +65,39 @@ impl Crud<PrivateMessageForm> for PrivateMessage {
   }
 }
 
+impl PrivateMessage {
+  pub fn update_ap_id(conn: &PgConnection, private_message_id: i32) -> Result<Self, Error> {
+    use crate::schema::private_message::dsl::*;
+
+    let apid = make_apub_endpoint(
+      EndpointType::PrivateMessage,
+      &private_message_id.to_string(),
+    )
+    .to_string();
+    diesel::update(private_message.find(private_message_id))
+      .set(ap_id.eq(apid))
+      .get_result::<Self>(conn)
+  }
+
+  pub fn read_from_apub_id(conn: &PgConnection, object_id: &str) -> Result<Self, Error> {
+    use crate::schema::private_message::dsl::*;
+    private_message
+      .filter(ap_id.eq(object_id))
+      .first::<Self>(conn)
+  }
+}
+
 #[cfg(test)]
 mod tests {
-  use super::super::user::*;
-  use super::*;
+  use super::{super::user::*, *};
+  use crate::db::{establish_unpooled_connection, ListingType, SortType};
+
   #[test]
   fn test_crud() {
     let conn = establish_unpooled_connection();
 
     let creator_form = UserForm {
       name: "creator_pm".into(),
-      fedi_name: "rrf".into(),
       preferred_username: None,
       password_encrypted: "nope".into(),
       email: None,
@@ -81,13 +113,18 @@ mod tests {
       lang: "browser".into(),
       show_avatars: true,
       send_notifications_to_email: false,
+      actor_id: "changeme".into(),
+      bio: None,
+      local: true,
+      private_key: None,
+      public_key: None,
+      last_refreshed_at: None,
     };
 
     let inserted_creator = User_::create(&conn, &creator_form).unwrap();
 
     let recipient_form = UserForm {
       name: "recipient_pm".into(),
-      fedi_name: "rrf".into(),
       preferred_username: None,
       password_encrypted: "nope".into(),
       email: None,
@@ -103,17 +140,26 @@ mod tests {
       lang: "browser".into(),
       show_avatars: true,
       send_notifications_to_email: false,
+      actor_id: "changeme".into(),
+      bio: None,
+      local: true,
+      private_key: None,
+      public_key: None,
+      last_refreshed_at: None,
     };
 
     let inserted_recipient = User_::create(&conn, &recipient_form).unwrap();
 
     let private_message_form = PrivateMessageForm {
-      content: Some("A test private message".into()),
+      content: "A test private message".into(),
       creator_id: inserted_creator.id,
       recipient_id: inserted_recipient.id,
       deleted: None,
       read: None,
+      published: None,
       updated: None,
+      ap_id: "changeme".into(),
+      local: true,
     };
 
     let inserted_private_message = PrivateMessage::create(&conn, &private_message_form).unwrap();
@@ -127,6 +173,8 @@ mod tests {
       read: false,
       updated: None,
       published: inserted_private_message.published,
+      ap_id: "changeme".into(),
+      local: true,
     };
 
     let read_private_message = PrivateMessage::read(&conn, inserted_private_message.id).unwrap();
index e22bef50e7665f4e85d8be89b97e3aee6f5fb6cd..9a1df43972ee62f5ee07ee728b592343ce5db982 100644 (file)
@@ -1,5 +1,6 @@
-use super::*;
-use diesel::pg::Pg;
+use crate::db::{limit_and_offset, MaybeOptional};
+use diesel::{pg::Pg, result::Error, *};
+use serde::{Deserialize, Serialize};
 
 // The faked schema since diesel doesn't do views
 table! {
@@ -12,10 +13,16 @@ table! {
     read -> Bool,
     published -> Timestamp,
     updated -> Nullable<Timestamp>,
+    ap_id -> Text,
+    local -> Bool,
     creator_name -> Varchar,
     creator_avatar -> Nullable<Text>,
+    creator_actor_id -> Text,
+    creator_local -> Bool,
     recipient_name -> Varchar,
     recipient_avatar -> Nullable<Text>,
+    recipient_actor_id -> Text,
+    recipient_local -> Bool,
   }
 }
 
@@ -29,10 +36,16 @@ table! {
     read -> Bool,
     published -> Timestamp,
     updated -> Nullable<Timestamp>,
+    ap_id -> Text,
+    local -> Bool,
     creator_name -> Varchar,
     creator_avatar -> Nullable<Text>,
+    creator_actor_id -> Text,
+    creator_local -> Bool,
     recipient_name -> Varchar,
     recipient_avatar -> Nullable<Text>,
+    recipient_actor_id -> Text,
+    recipient_local -> Bool,
   }
 }
 
@@ -49,10 +62,16 @@ pub struct PrivateMessageView {
   pub read: bool,
   pub published: chrono::NaiveDateTime,
   pub updated: Option<chrono::NaiveDateTime>,
+  pub ap_id: String,
+  pub local: bool,
   pub creator_name: String,
   pub creator_avatar: Option<String>,
+  pub creator_actor_id: String,
+  pub creator_local: bool,
   pub recipient_name: String,
   pub recipient_avatar: Option<String>,
+  pub recipient_actor_id: String,
+  pub recipient_local: bool,
 }
 
 pub struct PrivateMessageQueryBuilder<'a> {
index 3b8366d816c6188dead6fc23cf306538d9e35667..c752bfe796a6a1c731a1534182566469a9d91888 100644 (file)
@@ -1,5 +1,6 @@
-use super::*;
-use crate::schema::site;
+use crate::{db::Crud, schema::site};
+use diesel::{dsl::*, result::Error, *};
+use serde::{Deserialize, Serialize};
 
 #[derive(Queryable, Identifiable, PartialEq, Debug, Serialize, Deserialize)]
 #[table_name = "site"]
index 674a7a6e75d724092bfdd75e759a3acb3d1c1268..bb9b54aa611825eba581ae9f02ae9ea23d636285 100644 (file)
@@ -1,4 +1,5 @@
-use super::*;
+use diesel::{result::Error, *};
+use serde::{Deserialize, Serialize};
 
 table! {
   site_view (id) {
index e7c7696502b334a0123749f42d855615fa72f712..8b2ff62aa739b8e1484365e0ecd559315911192b 100644 (file)
@@ -1,16 +1,20 @@
-use super::*;
-use crate::schema::user_;
-use crate::schema::user_::dsl::*;
-use crate::{is_email_regex, Settings};
+use crate::{
+  db::Crud,
+  is_email_regex,
+  naive_now,
+  schema::{user_, user_::dsl::*},
+  settings::Settings,
+};
 use bcrypt::{hash, DEFAULT_COST};
+use diesel::{dsl::*, result::Error, *};
 use jsonwebtoken::{decode, encode, DecodingKey, EncodingKey, Header, TokenData, Validation};
+use serde::{Deserialize, Serialize};
 
 #[derive(Queryable, Identifiable, PartialEq, Debug)]
 #[table_name = "user_"]
 pub struct User_ {
   pub id: i32,
   pub name: String,
-  pub fedi_name: String,
   pub preferred_username: Option<String>,
   pub password_encrypted: String,
   pub email: Option<String>,
@@ -27,13 +31,18 @@ pub struct User_ {
   pub show_avatars: bool,
   pub send_notifications_to_email: bool,
   pub matrix_user_id: Option<String>,
+  pub actor_id: String,
+  pub bio: Option<String>,
+  pub local: bool,
+  pub private_key: Option<String>,
+  pub public_key: Option<String>,
+  pub last_refreshed_at: chrono::NaiveDateTime,
 }
 
-#[derive(Insertable, AsChangeset, Clone)]
+#[derive(Insertable, AsChangeset, Clone, Debug)]
 #[table_name = "user_"]
 pub struct UserForm {
   pub name: String,
-  pub fedi_name: String,
   pub preferred_username: Option<String>,
   pub password_encrypted: String,
   pub admin: bool,
@@ -49,6 +58,12 @@ pub struct UserForm {
   pub show_avatars: bool,
   pub send_notifications_to_email: bool,
   pub matrix_user_id: Option<String>,
+  pub actor_id: String,
+  pub bio: Option<String>,
+  pub local: bool,
+  pub private_key: Option<String>,
+  pub public_key: Option<String>,
+  pub last_refreshed_at: Option<chrono::NaiveDateTime>,
 }
 
 impl Crud<UserForm> for User_ {
@@ -78,6 +93,7 @@ impl User_ {
     Self::create(&conn, &edited_user)
   }
 
+  // TODO do more individual updates like these
   pub fn update_password(
     conn: &PgConnection,
     user_id: i32,
@@ -86,13 +102,33 @@ impl User_ {
     let password_hash = hash(new_password, DEFAULT_COST).expect("Couldn't hash password");
 
     diesel::update(user_.find(user_id))
-      .set(password_encrypted.eq(password_hash))
+      .set((
+        password_encrypted.eq(password_hash),
+        updated.eq(naive_now()),
+      ))
       .get_result::<Self>(conn)
   }
 
-  pub fn read_from_name(conn: &PgConnection, from_user_name: String) -> Result<Self, Error> {
+  pub fn read_from_name(conn: &PgConnection, from_user_name: &str) -> Result<Self, Error> {
     user_.filter(name.eq(from_user_name)).first::<Self>(conn)
   }
+
+  pub fn add_admin(conn: &PgConnection, user_id: i32, added: bool) -> Result<Self, Error> {
+    diesel::update(user_.find(user_id))
+      .set(admin.eq(added))
+      .get_result::<Self>(conn)
+  }
+
+  pub fn ban_user(conn: &PgConnection, user_id: i32, ban: bool) -> Result<Self, Error> {
+    diesel::update(user_.find(user_id))
+      .set(banned.eq(ban))
+      .get_result::<Self>(conn)
+  }
+
+  pub fn read_from_actor_id(conn: &PgConnection, object_id: &str) -> Result<Self, Error> {
+    use crate::schema::user_::dsl::*;
+    user_.filter(actor_id.eq(object_id)).first::<Self>(conn)
+  }
 }
 
 #[derive(Debug, Serialize, Deserialize)]
@@ -129,7 +165,7 @@ impl User_ {
     let my_claims = Claims {
       id: self.id,
       username: self.name.to_owned(),
-      iss: self.fedi_name.to_owned(),
+      iss: Settings::get().hostname,
       show_nsfw: self.show_nsfw,
       theme: self.theme.to_owned(),
       default_sort_type: self.default_sort_type,
@@ -177,8 +213,8 @@ impl User_ {
 
 #[cfg(test)]
 mod tests {
-  use super::User_;
-  use super::*;
+  use super::{User_, *};
+  use crate::db::{establish_unpooled_connection, ListingType, SortType};
 
   #[test]
   fn test_crud() {
@@ -186,7 +222,6 @@ mod tests {
 
     let new_user = UserForm {
       name: "thommy".into(),
-      fedi_name: "rrf".into(),
       preferred_username: None,
       password_encrypted: "nope".into(),
       email: None,
@@ -202,6 +237,12 @@ mod tests {
       lang: "browser".into(),
       show_avatars: true,
       send_notifications_to_email: false,
+      actor_id: "changeme".into(),
+      bio: None,
+      local: true,
+      private_key: None,
+      public_key: None,
+      last_refreshed_at: None,
     };
 
     let inserted_user = User_::create(&conn, &new_user).unwrap();
@@ -209,7 +250,6 @@ mod tests {
     let expected_user = User_ {
       id: inserted_user.id,
       name: "thommy".into(),
-      fedi_name: "rrf".into(),
       preferred_username: None,
       password_encrypted: "nope".into(),
       email: None,
@@ -226,6 +266,12 @@ mod tests {
       lang: "browser".into(),
       show_avatars: true,
       send_notifications_to_email: false,
+      actor_id: "changeme".into(),
+      bio: None,
+      local: true,
+      private_key: None,
+      public_key: None,
+      last_refreshed_at: inserted_user.published,
     };
 
     let read_user = User_::read(&conn, inserted_user.id).unwrap();
index 0cf257955e749905716c1f5b7a15e18b6936f98d..b9129b01f5cae832cf6541510114ba0dbd411965 100644 (file)
@@ -1,6 +1,7 @@
 use super::comment::Comment;
-use super::*;
-use crate::schema::user_mention;
+use crate::{db::Crud, schema::user_mention};
+use diesel::{dsl::*, result::Error, *};
+use serde::{Deserialize, Serialize};
 
 #[derive(Queryable, Associations, Identifiable, PartialEq, Debug, Serialize, Deserialize)]
 #[belongs_to(Comment)]
@@ -53,18 +54,18 @@ impl Crud<UserMentionForm> for UserMention {
 
 #[cfg(test)]
 mod tests {
-  use super::super::comment::*;
-  use super::super::community::*;
-  use super::super::post::*;
-  use super::super::user::*;
-  use super::*;
+  use super::{
+    super::{comment::*, community::*, post::*, user::*},
+    *,
+  };
+  use crate::db::{establish_unpooled_connection, ListingType, SortType};
+
   #[test]
   fn test_crud() {
     let conn = establish_unpooled_connection();
 
     let new_user = UserForm {
       name: "terrylake".into(),
-      fedi_name: "rrf".into(),
       preferred_username: None,
       password_encrypted: "nope".into(),
       email: None,
@@ -80,13 +81,18 @@ mod tests {
       lang: "browser".into(),
       show_avatars: true,
       send_notifications_to_email: false,
+      actor_id: "changeme".into(),
+      bio: None,
+      local: true,
+      private_key: None,
+      public_key: None,
+      last_refreshed_at: None,
     };
 
     let inserted_user = User_::create(&conn, &new_user).unwrap();
 
     let recipient_form = UserForm {
       name: "terrylakes recipient".into(),
-      fedi_name: "rrf".into(),
       preferred_username: None,
       password_encrypted: "nope".into(),
       email: None,
@@ -102,6 +108,12 @@ mod tests {
       lang: "browser".into(),
       show_avatars: true,
       send_notifications_to_email: false,
+      actor_id: "changeme".into(),
+      bio: None,
+      local: true,
+      private_key: None,
+      public_key: None,
+      last_refreshed_at: None,
     };
 
     let inserted_recipient = User_::create(&conn, &recipient_form).unwrap();
@@ -116,6 +128,12 @@ mod tests {
       deleted: None,
       updated: None,
       nsfw: false,
+      actor_id: "changeme".into(),
+      local: true,
+      private_key: None,
+      public_key: None,
+      last_refreshed_at: None,
+      published: None,
     };
 
     let inserted_community = Community::create(&conn, &new_community).unwrap();
@@ -136,6 +154,9 @@ mod tests {
       embed_description: None,
       embed_html: None,
       thumbnail_url: None,
+      ap_id: "changeme".into(),
+      local: true,
+      published: None,
     };
 
     let inserted_post = Post::create(&conn, &new_post).unwrap();
@@ -148,7 +169,10 @@ mod tests {
       deleted: None,
       read: None,
       parent_id: None,
+      published: None,
       updated: None,
+      ap_id: "changeme".into(),
+      local: true,
     };
 
     let inserted_comment = Comment::create(&conn, &comment_form).unwrap();
index 8046747e68ea48c79788ad20db9418679261ee75..100445b9938371aab7c08ffff4857ba7ace7be29 100644 (file)
@@ -1,5 +1,6 @@
-use super::*;
-use diesel::pg::Pg;
+use crate::db::{limit_and_offset, MaybeOptional, SortType};
+use diesel::{dsl::*, pg::Pg, result::Error, *};
+use serde::{Deserialize, Serialize};
 
 // The faked schema since diesel doesn't do views
 table! {
@@ -7,6 +8,8 @@ table! {
     id -> Int4,
     user_mention_id -> Int4,
     creator_id -> Int4,
+    creator_actor_id -> Text,
+    creator_local -> Bool,
     post_id -> Int4,
     parent_id -> Nullable<Int4>,
     content -> Text,
@@ -16,6 +19,8 @@ table! {
     updated -> Nullable<Timestamp>,
     deleted -> Bool,
     community_id -> Int4,
+    community_actor_id -> Text,
+    community_local -> Bool,
     community_name -> Varchar,
     banned -> Bool,
     banned_from_community -> Bool,
@@ -29,6 +34,8 @@ table! {
     my_vote -> Nullable<Int4>,
     saved -> Nullable<Bool>,
     recipient_id -> Int4,
+    recipient_actor_id -> Text,
+    recipient_local -> Bool,
   }
 }
 
@@ -37,6 +44,8 @@ table! {
     id -> Int4,
     user_mention_id -> Int4,
     creator_id -> Int4,
+    creator_actor_id -> Text,
+    creator_local -> Bool,
     post_id -> Int4,
     parent_id -> Nullable<Int4>,
     content -> Text,
@@ -46,6 +55,8 @@ table! {
     updated -> Nullable<Timestamp>,
     deleted -> Bool,
     community_id -> Int4,
+    community_actor_id -> Text,
+    community_local -> Bool,
     community_name -> Varchar,
     banned -> Bool,
     banned_from_community -> Bool,
@@ -59,6 +70,8 @@ table! {
     my_vote -> Nullable<Int4>,
     saved -> Nullable<Bool>,
     recipient_id -> Int4,
+    recipient_actor_id -> Text,
+    recipient_local -> Bool,
   }
 }
 
@@ -70,6 +83,8 @@ pub struct UserMentionView {
   pub id: i32,
   pub user_mention_id: i32,
   pub creator_id: i32,
+  pub creator_actor_id: String,
+  pub creator_local: bool,
   pub post_id: i32,
   pub parent_id: Option<i32>,
   pub content: String,
@@ -79,6 +94,8 @@ pub struct UserMentionView {
   pub updated: Option<chrono::NaiveDateTime>,
   pub deleted: bool,
   pub community_id: i32,
+  pub community_actor_id: String,
+  pub community_local: bool,
   pub community_name: String,
   pub banned: bool,
   pub banned_from_community: bool,
@@ -92,6 +109,8 @@ pub struct UserMentionView {
   pub my_vote: Option<i32>,
   pub saved: Option<bool>,
   pub recipient_id: i32,
+  pub recipient_actor_id: String,
+  pub recipient_local: bool,
 }
 
 pub struct UserMentionQueryBuilder<'a> {
index 2274ecbdffd2950dbdb78486c7d030811ec932b5..57e2a4c9c2524c5b9a8b4b6550c7b1105e42db5a 100644 (file)
@@ -1,15 +1,18 @@
 use super::user_view::user_mview::BoxedQuery;
-use super::*;
-use diesel::pg::Pg;
+use crate::db::{fuzzy_search, limit_and_offset, MaybeOptional, SortType};
+use diesel::{dsl::*, pg::Pg, result::Error, *};
+use serde::{Deserialize, Serialize};
 
 table! {
   user_view (id) {
     id -> Int4,
+    actor_id -> Text,
     name -> Varchar,
     avatar -> Nullable<Text>,
     email -> Nullable<Text>,
     matrix_user_id -> Nullable<Text>,
-    fedi_name -> Varchar,
+    bio -> Nullable<Text>,
+    local -> Bool,
     admin -> Bool,
     banned -> Bool,
     show_avatars -> Bool,
@@ -25,11 +28,13 @@ table! {
 table! {
   user_mview (id) {
     id -> Int4,
+    actor_id -> Text,
     name -> Varchar,
     avatar -> Nullable<Text>,
     email -> Nullable<Text>,
     matrix_user_id -> Nullable<Text>,
-    fedi_name -> Varchar,
+    bio -> Nullable<Text>,
+    local -> Bool,
     admin -> Bool,
     banned -> Bool,
     show_avatars -> Bool,
@@ -48,11 +53,13 @@ table! {
 #[table_name = "user_view"]
 pub struct UserView {
   pub id: i32,
+  pub actor_id: String,
   pub name: String,
   pub avatar: Option<String>,
   pub email: Option<String>,
   pub matrix_user_id: Option<String>,
-  pub fedi_name: String,
+  pub bio: Option<String>,
+  pub local: bool,
   pub admin: bool,
   pub banned: bool,
   pub show_avatars: bool,
index ebfe17d9c9ef803c3863a2ab52ca83ca4e45f937..2391449caf818155ebc3c500c6d15537030259d8 100644 (file)
@@ -16,6 +16,8 @@ pub extern crate dotenv;
 pub extern crate jsonwebtoken;
 pub extern crate lettre;
 pub extern crate lettre_email;
+extern crate log;
+pub extern crate openssl;
 pub extern crate rand;
 pub extern crate regex;
 pub extern crate rss;
@@ -34,22 +36,27 @@ pub mod settings;
 pub mod version;
 pub mod websocket;
 
+use crate::settings::Settings;
 use actix_web::dev::ConnectionInfo;
-use chrono::{DateTime, NaiveDateTime, Utc};
-use lettre::smtp::authentication::{Credentials, Mechanism};
-use lettre::smtp::extension::ClientId;
-use lettre::smtp::ConnectionReuseParameters;
-use lettre::{ClientSecurity, SmtpClient, Transport};
+use chrono::{DateTime, FixedOffset, Local, NaiveDateTime, Utc};
+use itertools::Itertools;
+use lettre::{
+  smtp::{
+    authentication::{Credentials, Mechanism},
+    extension::ClientId,
+    ConnectionReuseParameters,
+  },
+  ClientSecurity,
+  SmtpClient,
+  Transport,
+};
 use lettre_email::Email;
 use log::error;
 use percent_encoding::{utf8_percent_encode, NON_ALPHANUMERIC};
-use rand::distributions::Alphanumeric;
-use rand::{thread_rng, Rng};
+use rand::{distributions::Alphanumeric, thread_rng, Rng};
 use regex::{Regex, RegexBuilder};
 use serde::Deserialize;
 
-use crate::settings::Settings;
-
 pub type ConnectionId = usize;
 pub type PostId = i32;
 pub type CommunityId = i32;
@@ -68,6 +75,11 @@ pub fn naive_from_unix(time: i64) -> NaiveDateTime {
   NaiveDateTime::from_timestamp(time, 0)
 }
 
+pub fn convert_datetime(datetime: NaiveDateTime) -> DateTime<FixedOffset> {
+  let now = Local::now();
+  DateTime::<FixedOffset>::from_utc(datetime, *now.offset())
+}
+
 pub fn is_email_regex(test: &str) -> bool {
   EMAIL_REGEX.is_match(test)
 }
@@ -111,20 +123,6 @@ pub fn slurs_vec_to_str(slurs: Vec<&str>) -> String {
   [start, combined].concat()
 }
 
-pub fn extract_usernames(test: &str) -> Vec<&str> {
-  let mut matches: Vec<&str> = USERNAME_MATCHES_REGEX
-    .find_iter(test)
-    .map(|mat| mat.as_str())
-    .collect();
-
-  // Unique
-  matches.sort_unstable();
-  matches.dedup();
-
-  // Remove /u/
-  matches.iter().map(|t| &t[3..]).collect()
-}
-
 pub fn generate_random_string() -> String {
   thread_rng().sample_iter(&Alphanumeric).take(30).collect()
 }
@@ -279,6 +277,33 @@ pub fn get_ip(conn_info: &ConnectionInfo) -> String {
     .to_string()
 }
 
+// TODO nothing is done with community / group webfingers yet, so just ignore those for now
+#[derive(Clone, PartialEq, Eq, Hash)]
+pub struct MentionData {
+  pub name: String,
+  pub domain: String,
+}
+
+impl MentionData {
+  pub fn is_local(&self) -> bool {
+    Settings::get().hostname.eq(&self.domain)
+  }
+  pub fn full_name(&self) -> String {
+    format!("@{}@{}", &self.name, &self.domain)
+  }
+}
+
+pub fn scrape_text_for_mentions(text: &str) -> Vec<MentionData> {
+  let mut out: Vec<MentionData> = Vec::new();
+  for caps in WEBFINGER_USER_REGEX.captures_iter(text) {
+    out.push(MentionData {
+      name: caps["name"].to_string(),
+      domain: caps["domain"].to_string(),
+    });
+  }
+  out.into_iter().unique().collect()
+}
+
 pub fn is_valid_username(name: &str) -> bool {
   VALID_USERNAME_REGEX.is_match(name)
 }
@@ -290,10 +315,26 @@ pub fn is_valid_community_name(name: &str) -> bool {
 #[cfg(test)]
 mod tests {
   use crate::{
-    extract_usernames, is_email_regex, is_image_content_type, is_valid_community_name,
-    is_valid_username, remove_slurs, slur_check, slurs_vec_to_str,
+    is_email_regex,
+    is_image_content_type,
+    is_valid_community_name,
+    is_valid_username,
+    remove_slurs,
+    scrape_text_for_mentions,
+    slur_check,
+    slurs_vec_to_str,
   };
 
+  #[test]
+  fn test_mentions_regex() {
+    let text = "Just read a great blog post by [@tedu@honk.teduangst.com](/u/test). And another by !test_community@fish.teduangst.com . Another [@lemmy@lemmy_alpha:8540](/u/fish)";
+    let mentions = scrape_text_for_mentions(text);
+
+    assert_eq!(mentions[0].name, "tedu".to_string());
+    assert_eq!(mentions[0].domain, "honk.teduangst.com".to_string());
+    assert_eq!(mentions[1].domain, "lemmy_alpha:8540".to_string());
+  }
+
   #[test]
   fn test_image() {
     assert!(is_image_content_type("https://1734811051.rsc.cdn77.org/data/images/full/365645/as-virus-kills-navajos-in-their-homes-tribal-women-provide-lifeline.jpg?w=600?w=650").is_ok());
@@ -355,13 +396,6 @@ mod tests {
     }
   }
 
-  #[test]
-  fn test_extract_usernames() {
-    let usernames = extract_usernames("this is a user mention for [/u/testme](/u/testme) and thats all. Oh [/u/another](/u/another) user. And the first again [/u/testme](/u/testme) okay");
-    let expected = vec!["another", "testme"];
-    assert_eq!(usernames, expected);
-  }
-
   // These helped with testing
   // #[test]
   // fn test_iframely() {
@@ -388,6 +422,9 @@ lazy_static! {
   static ref EMAIL_REGEX: Regex = Regex::new(r"^[a-zA-Z0-9.!#$%&’*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$").unwrap();
   static ref SLUR_REGEX: Regex = RegexBuilder::new(r"(fag(g|got|tard)?|maricos?|cock\s?sucker(s|ing)?|nig(\b|g?(a|er)?(s|z)?)\b|dindu(s?)|mudslime?s?|kikes?|mongoloids?|towel\s*heads?|\bspi(c|k)s?\b|\bchinks?|niglets?|beaners?|\bnips?\b|\bcoons?\b|jungle\s*bunn(y|ies?)|jigg?aboo?s?|\bpakis?\b|rag\s*heads?|gooks?|cunts?|bitch(es|ing|y)?|puss(y|ies?)|twats?|feminazis?|whor(es?|ing)|\bslut(s|t?y)?|\btrann?(y|ies?)|ladyboy(s?)|\b(b|re|r)tard(ed)?s?)").case_insensitive(true).build().unwrap();
   static ref USERNAME_MATCHES_REGEX: Regex = Regex::new(r"/u/[a-zA-Z][0-9a-zA-Z_]*").unwrap();
+  // TODO keep this old one, it didn't work with port well tho
+  // static ref WEBFINGER_USER_REGEX: Regex = Regex::new(r"@(?P<name>[\w.]+)@(?P<domain>[a-zA-Z0-9._-]+\.[a-zA-Z0-9_-]+)").unwrap();
+  static ref WEBFINGER_USER_REGEX: Regex = Regex::new(r"@(?P<name>[\w.]+)@(?P<domain>[a-zA-Z0-9._:-]+)").unwrap();
   static ref VALID_USERNAME_REGEX: Regex = Regex::new(r"^[a-zA-Z0-9_]{3,20}$").unwrap();
   static ref VALID_COMMUNITY_NAME_REGEX: Regex = Regex::new(r"^[a-z0-9_]{3,20}$").unwrap();
 }
index 3f91965e10a84b252f8ce600edb1e6fd70ee61b4..2f53f3ac68f92e40fd110c4725ca8fb03faead6c 100644 (file)
@@ -6,14 +6,21 @@ pub extern crate lazy_static;
 
 use crate::lemmy_server::actix_web::dev::Service;
 use actix::prelude::*;
-use actix_web::body::Body;
-use actix_web::dev::{ServiceRequest, ServiceResponse};
-use actix_web::http::header::CONTENT_TYPE;
-use actix_web::http::{header::CACHE_CONTROL, HeaderValue};
-use actix_web::*;
-use diesel::r2d2::{ConnectionManager, Pool};
-use diesel::PgConnection;
+use actix_web::{
+  body::Body,
+  dev::{ServiceRequest, ServiceResponse},
+  http::{
+    header::{CACHE_CONTROL, CONTENT_TYPE},
+    HeaderValue,
+  },
+  *,
+};
+use diesel::{
+  r2d2::{ConnectionManager, Pool},
+  PgConnection,
+};
 use lemmy_server::{
+  db::code_migrations::run_advanced_migrations,
   rate_limit::{rate_limiter::RateLimiter, RateLimit},
   routes::{api, federation, feeds, index, nodeinfo, webfinger},
   settings::Settings,
@@ -48,6 +55,7 @@ async fn main() -> io::Result<()> {
   // Run the migrations from code
   let conn = pool.get().unwrap();
   embedded_migrations::run(&conn).unwrap();
+  run_advanced_migrations(&conn).unwrap();
 
   // Set up the rate limiter
   let rate_limiter = RateLimit {
index 8d698b78bc81c464bb693bbbe2b14f5642ab8453..b4c2dc5db9a6e0e67cc3d5143b23900fd9df1251 100644 (file)
@@ -1,23 +1,18 @@
-pub mod rate_limiter;
-
 use super::{IPAddr, Settings};
-use crate::api::APIError;
-use crate::get_ip;
-use crate::settings::RateLimitConfig;
+use crate::{api::APIError, get_ip, settings::RateLimitConfig};
 use actix_web::dev::{Service, ServiceRequest, ServiceResponse, Transform};
-use failure::Error;
 use futures::future::{ok, Ready};
-use log::debug;
 use rate_limiter::{RateLimitType, RateLimiter};
-use std::collections::HashMap;
-use std::future::Future;
-use std::pin::Pin;
-use std::sync::Arc;
-use std::task::{Context, Poll};
-use std::time::SystemTime;
-use strum::IntoEnumIterator;
+use std::{
+  future::Future,
+  pin::Pin,
+  sync::Arc,
+  task::{Context, Poll},
+};
 use tokio::sync::Mutex;
 
+pub mod rate_limiter;
+
 #[derive(Debug, Clone)]
 pub struct RateLimit {
   pub rate_limiter: Arc<Mutex<RateLimiter>>,
index a30efa0946e17250d42b39a8f3f38a9c0ab22a12..b3ac7093c9a79ffb3902d16bb07d883a7bcb62dc 100644 (file)
@@ -1,4 +1,9 @@
-use super::*;
+use super::IPAddr;
+use crate::api::APIError;
+use failure::Error;
+use log::debug;
+use std::{collections::HashMap, time::SystemTime};
+use strum::IntoEnumIterator;
 
 #[derive(Debug, Clone)]
 pub struct RateLimitBucket {
index 1565afb80c9b85eb810df89b0f60fe83587e0c52..6ee94691d7773fcaa054f5ea455bb2cb179d26f7 100644 (file)
@@ -1,10 +1,11 @@
-use super::*;
-use crate::api::comment::*;
-use crate::api::community::*;
-use crate::api::post::*;
-use crate::api::site::*;
-use crate::api::user::*;
-use crate::rate_limit::RateLimit;
+use crate::{
+  api::{comment::*, community::*, post::*, site::*, user::*, Oper, Perform},
+  rate_limit::RateLimit,
+  routes::{ChatServerParam, DbPoolParam},
+  websocket::WebsocketInfo,
+};
+use actix_web::{error::ErrorBadRequest, *};
+use serde::Serialize;
 
 pub fn config(cfg: &mut web::ServiceConfig, rate_limit: &RateLimit) {
   cfg.service(
@@ -83,6 +84,14 @@ pub fn config(cfg: &mut web::ServiceConfig, rate_limit: &RateLimit) {
           .route("/like", web::post().to(route_post::<CreateCommentLike>))
           .route("/save", web::put().to(route_post::<SaveComment>)),
       )
+      // Private Message
+      .service(
+        web::scope("/private_message")
+          .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>)),
+      )
       // User
       .service(
         // Account action, I don't like that it's in /user maybe /accounts
index bc627cb0e8842413b9563ef175496207061785e9..fe6e3365789b2bd30a19596adb950ad821f43c0b 100644 (file)
@@ -1,18 +1,45 @@
-use super::*;
-use crate::apub;
+use crate::{
+  apub::{
+    comment::get_apub_comment,
+    community::*,
+    community_inbox::community_inbox,
+    post::get_apub_post,
+    shared_inbox::shared_inbox,
+    user::*,
+    user_inbox::user_inbox,
+    APUB_JSON_CONTENT_TYPE,
+  },
+  settings::Settings,
+};
+use actix_web::*;
 
 pub fn config(cfg: &mut web::ServiceConfig) {
-  cfg
-    .route(
-      "/federation/c/{community_name}",
-      web::get().to(apub::community::get_apub_community),
-    )
-    .route(
-      "/federation/c/{community_name}/followers",
-      web::get().to(apub::community::get_apub_community_followers),
-    )
-    .route(
-      "/federation/u/{user_name}",
-      web::get().to(apub::user::get_apub_user),
-    );
+  if Settings::get().federation.enabled {
+    println!("federation enabled, host is {}", Settings::get().hostname);
+    cfg
+      .service(
+        web::scope("/")
+          .guard(guard::Header("Accept", APUB_JSON_CONTENT_TYPE))
+          .route(
+            "/c/{community_name}",
+            web::get().to(get_apub_community_http),
+          )
+          .route(
+            "/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("/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)),
+      )
+      // Inboxes dont work with the header guard for some reason.
+      .route("/c/{community_name}/inbox", web::post().to(community_inbox))
+      .route("/u/{user_name}/inbox", web::post().to(user_inbox))
+      .route("/inbox", web::post().to(shared_inbox));
+  }
 }
index 815953c55abb722a8c017aad23dc268db4b790c3..b76751a6710f4bdf57f507a3f16106ab935eb2eb 100644 (file)
@@ -1,11 +1,28 @@
-use super::*;
-use crate::db::comment_view::{ReplyQueryBuilder, ReplyView};
-use crate::db::community::Community;
-use crate::db::post_view::{PostQueryBuilder, PostView};
-use crate::db::site_view::SiteView;
-use crate::db::user::{Claims, User_};
-use crate::db::user_mention_view::{UserMentionQueryBuilder, UserMentionView};
-use crate::db::{ListingType, SortType};
+use crate::{
+  db::{
+    comment_view::{ReplyQueryBuilder, ReplyView},
+    community::Community,
+    post_view::{PostQueryBuilder, PostView},
+    site_view::SiteView,
+    user::{Claims, User_},
+    user_mention_view::{UserMentionQueryBuilder, UserMentionView},
+    ListingType,
+    SortType,
+  },
+  markdown_to_html,
+  routes::DbPoolParam,
+  settings::Settings,
+};
+use actix_web::{error::ErrorBadRequest, *};
+use chrono::{DateTime, NaiveDateTime, Utc};
+use diesel::{
+  r2d2::{ConnectionManager, Pool},
+  PgConnection,
+};
+use rss::{CategoryBuilder, ChannelBuilder, GuidBuilder, Item, ItemBuilder};
+use serde::Deserialize;
+use std::str::FromStr;
+use strum::ParseError;
 
 #[derive(Deserialize)]
 pub struct Params {
@@ -21,14 +38,11 @@ enum RequestType {
 
 pub fn config(cfg: &mut web::ServiceConfig) {
   cfg
-    .route("/feeds/{type}/{name}.xml", web::get().to(feeds::get_feed))
-    .route("/feeds/all.xml", web::get().to(feeds::get_all_feed));
+    .route("/feeds/{type}/{name}.xml", web::get().to(get_feed))
+    .route("/feeds/all.xml", web::get().to(get_all_feed));
 }
 
-async fn get_all_feed(
-  info: web::Query<Params>,
-  db: web::Data<Pool<ConnectionManager<PgConnection>>>,
-) -> Result<HttpResponse, Error> {
+async fn get_all_feed(info: web::Query<Params>, db: DbPoolParam) -> Result<HttpResponse, Error> {
   let res = web::block(move || {
     let conn = db.get()?;
     get_feed_all_data(&conn, &get_sort_type(info)?)
@@ -144,8 +158,7 @@ fn get_feed_community(
   community_name: String,
 ) -> Result<ChannelBuilder, failure::Error> {
   let site_view = SiteView::read(&conn)?;
-  let community = Community::read_from_name(&conn, community_name)?;
-  let community_url = community.get_url();
+  let community = Community::read_from_name(&conn, &community_name)?;
 
   let posts = PostQueryBuilder::create(&conn)
     .listing_type(ListingType::All)
@@ -158,7 +171,7 @@ fn get_feed_community(
   let mut channel_builder = ChannelBuilder::default();
   channel_builder
     .title(&format!("{} - {}", site_view.name, community.name))
-    .link(community_url)
+    .link(community.actor_id)
     .items(items);
 
   if let Some(community_desc) = community.description {
index 895af435005e7ebf3cbe3a0aeb3ae7726f14ecd0..2f462aa5f12492446e4ba5a598ee146297396ba5 100644 (file)
@@ -1,4 +1,6 @@
-use super::*;
+use crate::settings::Settings;
+use actix_files::NamedFile;
+use actix_web::*;
 
 pub fn config(cfg: &mut web::ServiceConfig) {
   cfg
index 37c56eba07a241656aba4398f2128a134f6fdc68..bcb7e45fa33bd631191838008c070b1a839194d7 100644 (file)
@@ -1,35 +1,20 @@
-use crate::api::{Oper, Perform};
-use crate::db::site_view::SiteView;
-use crate::rate_limit::rate_limiter::RateLimiter;
-use crate::websocket::{server::ChatServer, WebsocketInfo};
-use crate::{get_ip, markdown_to_html, version, Settings};
+pub mod api;
+pub mod federation;
+pub mod feeds;
+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_files::NamedFile;
-use actix_web::{body::Body, error::ErrorBadRequest, web::Query, *};
-use actix_web_actors::ws;
-use chrono::{DateTime, NaiveDateTime, Utc};
+use actix_web::*;
 use diesel::{
   r2d2::{ConnectionManager, Pool},
   PgConnection,
 };
-use log::{error, info};
-use regex::Regex;
-use rss::{CategoryBuilder, ChannelBuilder, GuidBuilder, Item, ItemBuilder};
-use serde::{Deserialize, Serialize};
-use serde_json::json;
-use std::str::FromStr;
 use std::sync::{Arc, Mutex};
-use std::time::{Duration, Instant};
-use strum::ParseError;
 
 pub type DbPoolParam = web::Data<Pool<ConnectionManager<PgConnection>>>;
 pub type RateLimitParam = web::Data<Arc<Mutex<RateLimiter>>>;
 pub type ChatServerParam = web::Data<Addr<ChatServer>>;
-
-pub mod api;
-pub mod federation;
-pub mod feeds;
-pub mod index;
-pub mod nodeinfo;
-pub mod webfinger;
-pub mod websocket;
index af6fc28227056b1173dd848f530329a8b76dc3f3..db206a3e8a70a94533ded1161531a233dc4bd4da 100644 (file)
@@ -1,4 +1,13 @@
-use super::*;
+use crate::{
+  apub::get_apub_protocol_string,
+  db::site_view::SiteView,
+  routes::DbPoolParam,
+  version,
+  Settings,
+};
+use actix_web::{body::Body, error::ErrorBadRequest, *};
+use serde::{Deserialize, Serialize};
+use url::Url;
 
 pub fn config(cfg: &mut web::ServiceConfig) {
   cfg
@@ -6,26 +15,32 @@ pub fn config(cfg: &mut web::ServiceConfig) {
     .route("/.well-known/nodeinfo", web::get().to(node_info_well_known));
 }
 
-async fn node_info_well_known() -> HttpResponse<Body> {
+async fn node_info_well_known() -> Result<HttpResponse<Body>, failure::Error> {
   let node_info = NodeInfoWellKnown {
     links: NodeInfoWellKnownLinks {
-      rel: "http://nodeinfo.diaspora.software/ns/schema/2.0".to_string(),
-      href: format!("https://{}/nodeinfo/2.0.json", Settings::get().hostname),
+      rel: Url::parse("http://nodeinfo.diaspora.software/ns/schema/2.0")?,
+      href: Url::parse(&format!(
+        "{}://{}/nodeinfo/2.0.json",
+        get_apub_protocol_string(),
+        Settings::get().hostname
+      ))?,
     },
   };
-  HttpResponse::Ok().json(node_info)
+  Ok(HttpResponse::Ok().json(node_info))
 }
 
-async fn node_info(
-  db: web::Data<Pool<ConnectionManager<PgConnection>>>,
-) -> Result<HttpResponse, Error> {
+async fn node_info(db: DbPoolParam) -> Result<HttpResponse, Error> {
   let res = web::block(move || {
     let conn = db.get()?;
     let site_view = match SiteView::read(&conn) {
       Ok(site_view) => site_view,
       Err(_) => return Err(format_err!("not_found")),
     };
-    let protocols = vec![];
+    let protocols = if Settings::get().federation.enabled {
+      vec!["activitypub".to_string()]
+    } else {
+      vec![]
+    };
     Ok(NodeInfo {
       version: "2.0".to_string(),
       software: NodeInfoSoftware {
@@ -49,41 +64,41 @@ async fn node_info(
   Ok(res)
 }
 
-#[derive(Serialize)]
-struct NodeInfoWellKnown {
-  links: NodeInfoWellKnownLinks,
+#[derive(Serialize, Deserialize, Debug)]
+pub struct NodeInfoWellKnown {
+  pub links: NodeInfoWellKnownLinks,
 }
 
-#[derive(Serialize)]
-struct NodeInfoWellKnownLinks {
-  rel: String,
-  href: String,
+#[derive(Serialize, Deserialize, Debug)]
+pub struct NodeInfoWellKnownLinks {
+  pub rel: Url,
+  pub href: Url,
 }
 
-#[derive(Serialize)]
-struct NodeInfo {
-  version: String,
-  software: NodeInfoSoftware,
-  protocols: Vec<String>,
-  usage: NodeInfoUsage,
+#[derive(Serialize, Deserialize, Debug)]
+pub struct NodeInfo {
+  pub version: String,
+  pub software: NodeInfoSoftware,
+  pub protocols: Vec<String>,
+  pub usage: NodeInfoUsage,
 }
 
-#[derive(Serialize)]
-struct NodeInfoSoftware {
-  name: String,
-  version: String,
+#[derive(Serialize, Deserialize, Debug)]
+pub struct NodeInfoSoftware {
+  pub name: String,
+  pub version: String,
 }
 
-#[derive(Serialize)]
+#[derive(Serialize, Deserialize, Debug)]
 #[serde(rename_all = "camelCase")]
-struct NodeInfoUsage {
-  users: NodeInfoUsers,
-  local_posts: i64,
-  local_comments: i64,
-  open_registrations: bool,
+pub struct NodeInfoUsage {
+  pub users: NodeInfoUsers,
+  pub local_posts: i64,
+  pub local_comments: i64,
+  pub open_registrations: bool,
 }
 
-#[derive(Serialize)]
-struct NodeInfoUsers {
-  total: i64,
+#[derive(Serialize, Deserialize, Debug)]
+pub struct NodeInfoUsers {
+  pub total: i64,
 }
index 18a21efa1203dd2c1a21df6ca483f916cd55f193..9fa01a1475e63a6f3d3bc05ae881fbddf451432e 100644 (file)
@@ -1,16 +1,41 @@
-use super::*;
-use crate::db::community::Community;
+use crate::{
+  db::{community::Community, user::User_},
+  routes::DbPoolParam,
+  Settings,
+};
+use actix_web::{error::ErrorBadRequest, web::Query, *};
+use regex::Regex;
+use serde::{Deserialize, Serialize};
 
 #[derive(Deserialize)]
 pub struct Params {
   resource: String,
 }
 
+#[derive(Serialize, Deserialize, Debug)]
+pub struct WebFingerResponse {
+  pub subject: String,
+  pub aliases: Vec<String>,
+  pub links: Vec<WebFingerLink>,
+}
+
+#[derive(Serialize, Deserialize, Debug)]
+pub struct WebFingerLink {
+  pub rel: Option<String>,
+  #[serde(rename(serialize = "type", deserialize = "type"))]
+  pub type_: Option<String>,
+  pub href: Option<String>,
+  #[serde(skip_serializing_if = "Option::is_none")]
+  pub template: Option<String>,
+}
+
 pub fn config(cfg: &mut web::ServiceConfig) {
-  cfg.route(
-    ".well-known/webfinger",
-    web::get().to(get_webfinger_response),
-  );
+  if Settings::get().federation.enabled {
+    cfg.route(
+      ".well-known/webfinger",
+      web::get().to(get_webfinger_response),
+    );
+  }
 }
 
 lazy_static! {
@@ -19,6 +44,11 @@ lazy_static! {
     Settings::get().hostname
   ))
   .unwrap();
+  static ref WEBFINGER_USER_REGEX: Regex = Regex::new(&format!(
+    "^acct:([a-z0-9_]{{3, 20}})@{}$",
+    Settings::get().hostname
+  ))
+  .unwrap();
 }
 
 /// Responds to webfinger requests of the following format. There isn't any real documentation for
@@ -29,56 +59,63 @@ lazy_static! {
 /// https://radical.town/.well-known/webfinger?resource=acct:felix@radical.town
 async fn get_webfinger_response(
   info: Query<Params>,
-  db: web::Data<Pool<ConnectionManager<PgConnection>>>,
+  db: DbPoolParam,
 ) -> Result<HttpResponse, Error> {
   let res = web::block(move || {
     let conn = db.get()?;
 
-    let regex_parsed = WEBFINGER_COMMUNITY_REGEX
+    let community_regex_parsed = WEBFINGER_COMMUNITY_REGEX
       .captures(&info.resource)
-      .map(|c| c.get(1));
-    // TODO: replace this with .flatten() once we are running rust 1.40
-    let regex_parsed_flattened = match regex_parsed {
-      Some(s) => s,
-      None => None,
-    };
-    let community_name = match regex_parsed_flattened {
-      Some(c) => c.as_str(),
-      None => return Err(format_err!("not_found")),
-    };
+      .map(|c| c.get(1))
+      .flatten();
 
-    // Make sure the requested community exists.
-    let community = match Community::read_from_name(&conn, community_name.to_string()) {
-      Ok(o) => o,
-      Err(_) => return Err(format_err!("not_found")),
+    let user_regex_parsed = WEBFINGER_USER_REGEX
+      .captures(&info.resource)
+      .map(|c| c.get(1))
+      .flatten();
+
+    let url = if let Some(community_name) = community_regex_parsed {
+      // Make sure the requested community exists.
+      let community = match Community::read_from_name(&conn, &community_name.as_str()) {
+        Ok(o) => o,
+        Err(_) => return Err(format_err!("not_found")),
+      };
+      community.actor_id
+    } else if let Some(user_name) = user_regex_parsed {
+      // Make sure the requested user exists.
+      let user = match User_::read_from_name(&conn, &user_name.as_str()) {
+        Ok(o) => o,
+        Err(_) => return Err(format_err!("not_found")),
+      };
+      user.actor_id
+    } else {
+      return Err(format_err!("not_found"));
     };
 
-    let community_url = community.get_url();
+    let wf_res = WebFingerResponse {
+      subject: info.resource.to_owned(),
+      aliases: vec![url.to_owned()],
+      links: vec![
+        WebFingerLink {
+          rel: Some("http://webfinger.net/rel/profile-page".to_string()),
+          type_: Some("text/html".to_string()),
+          href: Some(url.to_owned()),
+          template: None,
+        },
+        WebFingerLink {
+          rel: Some("self".to_string()),
+          type_: Some("application/activity+json".to_string()),
+          href: Some(url),
+          template: None,
+        }, // TODO: this also needs to return the subscribe link once that's implemented
+           //{
+           //  "rel": "http://ostatus.org/schema/1.0/subscribe",
+           //  "template": "https://my_instance.com/authorize_interaction?uri={uri}"
+           //}
+      ],
+    };
 
-    Ok(json!({
-    "subject": info.resource,
-    "aliases": [
-      community_url,
-    ],
-    "links": [
-    {
-      "rel": "http://webfinger.net/rel/profile-page",
-      "type": "text/html",
-      "href": community_url
-    },
-    {
-      "rel": "self",
-      "type": "application/activity+json",
-      // Yes this is correct, this link doesn't include the `.json` extension
-      "href": community_url
-    }
-    // TODO: this also needs to return the subscribe link once that's implemented
-    //{
-    //  "rel": "http://ostatus.org/schema/1.0/subscribe",
-    //  "template": "https://my_instance.com/authorize_interaction?uri={uri}"
-    //}
-    ]
-    }))
+    Ok(wf_res)
   })
   .await
   .map(|json| HttpResponse::Ok().json(json))
index 3814a8e9b5c012b9fcb48817f2765b7c075598e4..2f964d4313c90c258057450d6e7966320a15eb1c 100644 (file)
@@ -1,5 +1,12 @@
-use super::*;
-use crate::websocket::server::*;
+use crate::{
+  get_ip,
+  websocket::server::{ChatServer, *},
+};
+use actix::prelude::*;
+use actix_web::*;
+use actix_web_actors::ws;
+use log::{debug, error, info};
+use std::time::{Duration, Instant};
 
 /// How often heartbeat pings are sent
 const HEARTBEAT_INTERVAL: Duration = Duration::from_secs(5);
@@ -32,7 +39,6 @@ struct WSSession {
   /// Client must send ping at least once per 10 seconds (CLIENT_TIMEOUT),
   /// otherwise we drop connection.
   hb: Instant,
-  // db: Pool<ConnectionManager<PgConnection>>,
 }
 
 impl Actor for WSSession {
@@ -144,7 +150,7 @@ impl WSSession {
       // check client heartbeats
       if Instant::now().duration_since(act.hb) > CLIENT_TIMEOUT {
         // heartbeat timed out
-        error!("Websocket Client heartbeat failed, disconnecting!");
+        debug!("Websocket Client heartbeat failed, disconnecting!");
 
         // notify chat server
         act.cs_addr.do_send(Disconnect {
index d9449fef97a019d7e246adc4f26bfbd778caa467..8096d30105bcdca84c4e8722af0a7883f053d22c 100644 (file)
@@ -1,3 +1,14 @@
+table! {
+    activity (id) {
+        id -> Int4,
+        user_id -> Int4,
+        data -> Jsonb,
+        local -> Bool,
+        published -> Timestamp,
+        updated -> Nullable<Timestamp>,
+    }
+}
+
 table! {
     category (id) {
         id -> Int4,
@@ -17,6 +28,8 @@ table! {
         published -> Timestamp,
         updated -> Nullable<Timestamp>,
         deleted -> Bool,
+        ap_id -> Varchar,
+        local -> Bool,
     }
 }
 
@@ -53,6 +66,11 @@ table! {
         updated -> Nullable<Timestamp>,
         deleted -> Bool,
         nsfw -> Bool,
+        actor_id -> Varchar,
+        local -> Bool,
+        private_key -> Nullable<Text>,
+        public_key -> Nullable<Text>,
+        last_refreshed_at -> Timestamp,
     }
 }
 
@@ -211,6 +229,8 @@ table! {
         embed_description -> Nullable<Text>,
         embed_html -> Nullable<Text>,
         thumbnail_url -> Nullable<Text>,
+        ap_id -> Varchar,
+        local -> Bool,
     }
 }
 
@@ -252,6 +272,8 @@ table! {
         read -> Bool,
         published -> Timestamp,
         updated -> Nullable<Timestamp>,
+        ap_id -> Varchar,
+        local -> Bool,
     }
 }
 
@@ -273,7 +295,6 @@ table! {
     user_ (id) {
         id -> Int4,
         name -> Varchar,
-        fedi_name -> Varchar,
         preferred_username -> Nullable<Varchar>,
         password_encrypted -> Text,
         email -> Nullable<Text>,
@@ -290,6 +311,12 @@ table! {
         show_avatars -> Bool,
         send_notifications_to_email -> Bool,
         matrix_user_id -> Nullable<Text>,
+        actor_id -> Varchar,
+        bio -> Nullable<Text>,
+        local -> Bool,
+        private_key -> Nullable<Text>,
+        public_key -> Nullable<Text>,
+        last_refreshed_at -> Timestamp,
     }
 }
 
@@ -311,6 +338,7 @@ table! {
     }
 }
 
+joinable!(activity -> user_ (user_id));
 joinable!(comment -> post (post_id));
 joinable!(comment -> user_ (creator_id));
 joinable!(comment_like -> comment (comment_id));
@@ -353,6 +381,7 @@ joinable!(user_mention -> comment (comment_id));
 joinable!(user_mention -> user_ (recipient_id));
 
 allow_tables_to_appear_in_same_query!(
+  activity,
   category,
   comment,
   comment_like,
index 9d58d5b5d8ecce52e757f536b212db383570bc68..b3173457f216e1a52c014af53094a904ed2f86b4 100644 (file)
@@ -1,10 +1,7 @@
 use config::{Config, ConfigError, Environment, File};
 use failure::Error;
 use serde::Deserialize;
-use std::env;
-use std::fs;
-use std::net::IpAddr;
-use std::sync::RwLock;
+use std::{env, fs, net::IpAddr, sync::RwLock};
 
 static CONFIG_FILE_DEFAULTS: &str = "config/defaults.hjson";
 static CONFIG_FILE: &str = "config/config.hjson";
@@ -20,6 +17,7 @@ pub struct Settings {
   pub front_end_dir: String,
   pub rate_limit: RateLimitConfig,
   pub email: Option<EmailConfig>,
+  pub federation: Federation,
 }
 
 #[derive(Debug, Deserialize, Clone)]
@@ -59,6 +57,13 @@ pub struct Database {
   pub pool_size: u32,
 }
 
+#[derive(Debug, Deserialize, Clone)]
+pub struct Federation {
+  pub enabled: bool,
+  pub tls_enabled: bool,
+  pub allowed_instances: String,
+}
+
 lazy_static! {
   static ref SETTINGS: RwLock<Settings> = RwLock::new(match Settings::init() {
     Ok(c) => c,
index fd200d7d6069c7c88ff91ca50fca49f128f6faa0..4eb43e49d9528a0796d13868c71e6eb1d7bf2d83 100644 (file)
@@ -2,16 +2,20 @@ pub mod server;
 
 use crate::ConnectionId;
 use actix::prelude::*;
-use diesel::r2d2::{ConnectionManager, Pool};
-use diesel::PgConnection;
+use diesel::{
+  r2d2::{ConnectionManager, Pool},
+  PgConnection,
+};
 use failure::Error;
 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};
-use std::str::FromStr;
+use std::{
+  collections::{HashMap, HashSet},
+  str::FromStr,
+};
 
 #[derive(EnumString, ToString, Debug, Clone)]
 pub enum UserOperation {
index 25766011cd85d18b592bc6e40cc68cc17c9f7478..e4543ea1fa3321b54f7b3e8ea3c4bff811939354 100644 (file)
@@ -3,15 +3,16 @@
 //! room through `ChatServer`.
 
 use super::*;
-use crate::api::comment::*;
-use crate::api::community::*;
-use crate::api::post::*;
-use crate::api::site::*;
-use crate::api::user::*;
-use crate::api::*;
-use crate::rate_limit::RateLimit;
-use crate::websocket::UserOperation;
-use crate::{CommunityId, ConnectionId, IPAddr, PostId, UserId};
+use crate::{
+  api::{comment::*, community::*, post::*, site::*, user::*, *},
+  rate_limit::RateLimit,
+  websocket::UserOperation,
+  CommunityId,
+  ConnectionId,
+  IPAddr,
+  PostId,
+  UserId,
+};
 
 /// Chat server sends this messages to session
 #[derive(Message)]
index aa6ed178a16c088e064aa05420ff296f0f272a22..cdf79c84f6f949b6dfc81cb61800510a3126f39e 100644 (file)
@@ -1,2 +1,3 @@
 fuse.js
 translation_report.ts
+src/api_tests
index cc0ab540cf6c3e10587f607caf42c6149f99c2b7..bdd11a35d62235550e745a80d39e6c2603cbe911 100644 (file)
@@ -6,14 +6,11 @@ _site
 .git
 build
 .build
-.git
-.history
 .idea
 .jshintrc
 .nyc_output
 .sass-cache
 .vscode
-build
 coverage
 jsconfig.json
 Gemfile.lock
diff --git a/ui/jest.config.js b/ui/jest.config.js
new file mode 100644 (file)
index 0000000..abe695b
--- /dev/null
@@ -0,0 +1,10 @@
+module.exports = {
+  preset: 'ts-jest',
+  testEnvironment: 'node',
+  testTimeout: 30000,
+  globals: {
+    'ts-jest': {
+      diagnostics: false,
+    },
+  },
+};
index 21458f0d2f20563e94f9467da69f25e3f0f3dc26..0101ce13a59e29f25598e4ad4e6c4d84acb8fe8c 100644 (file)
@@ -6,6 +6,7 @@
   "license": "AGPL-3.0-or-later",
   "main": "index.js",
   "scripts": {
+    "api-test": "jest src/api_tests/api.spec.ts",
     "build": "node fuse prod",
     "lint": "tsc --noEmit && eslint --report-unused-disable-directives --ext .js,.ts,.tsx src",
     "prebuild": "node generate_translations.js",
@@ -18,7 +19,7 @@
     "@types/autosize": "^3.0.6",
     "@types/js-cookie": "^2.2.6",
     "@types/jwt-decode": "^2.2.1",
-    "@types/markdown-it": "^10.0.0",
+    "@types/markdown-it": "^0.0.9",
     "@types/markdown-it-container": "^2.0.2",
     "@types/node": "^13.11.1",
     "autosize": "^4.0.2",
@@ -38,6 +39,7 @@
     "markdown-it-emoji": "^1.4.0",
     "mobius1-selectr": "^2.4.13",
     "moment": "^2.24.0",
+    "node-fetch": "^2.6.0",
     "prettier": "^2.0.4",
     "reconnecting-websocket": "^4.4.0",
     "rxjs": "^6.5.5",
     "ws": "^7.2.3"
   },
   "devDependencies": {
+    "@types/jest": "^25.2.1",
+    "@types/node-fetch": "^2.5.6",
     "eslint": "^6.5.1",
     "eslint-plugin-inferno": "^7.14.3",
     "eslint-plugin-jane": "^7.2.1",
     "fuse-box": "^3.1.3",
+    "jest": "^25.4.0",
     "lint-staged": "^10.1.3",
     "sortpack": "^2.1.4",
+    "ts-jest": "^25.4.0",
     "ts-node": "^8.8.2",
     "ts-transform-classcat": "^1.0.0",
     "ts-transform-inferno": "^4.0.3",
diff --git a/ui/src/api_tests/api.spec.ts b/ui/src/api_tests/api.spec.ts
new file mode 100644 (file)
index 0000000..7337201
--- /dev/null
@@ -0,0 +1,1487 @@
+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);
+}
index 56af71149a7a40a7a331a6e12f872b89923fd990..0034c229e9129724937c057f62ff642e2380de4f 100644 (file)
@@ -113,6 +113,9 @@ export class AdminSettings extends Component<any, AdminSettingsState> {
                 user={{
                   name: admin.name,
                   avatar: admin.avatar,
+                  id: admin.id,
+                  local: admin.local,
+                  actor_id: admin.actor_id,
                 }}
               />
             </li>
@@ -133,6 +136,9 @@ export class AdminSettings extends Component<any, AdminSettingsState> {
                 user={{
                   name: banned.name,
                   avatar: banned.avatar,
+                  id: banned.id,
+                  local: banned.local,
+                  actor_id: banned.actor_id,
                 }}
               />
             </li>
index 4d29fe173208a4753c7e91279836c91f5a5070c4..155efe8e0f66ac88df350a65328991195c35d372 100644 (file)
@@ -154,6 +154,9 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
                   user={{
                     name: node.comment.creator_name,
                     avatar: node.comment.creator_avatar,
+                    id: node.comment.creator_id,
+                    local: node.comment.creator_local,
+                    actor_id: node.comment.creator_actor_id,
                   }}
                 />
               </span>
index 8d130ae7119bb2d20e63a90d72d5dc7cc94dc993..a3e340ff96672d49547af11dab9db0ed21b185b7 100644 (file)
@@ -14,6 +14,7 @@ import {
 } from '../interfaces';
 import { WebSocketService } from '../services';
 import { wsJsonToRes, toast } from '../utils';
+import { CommunityLink } from './community-link';
 import { i18n } from '../i18next';
 
 declare const Sortable: any;
@@ -104,9 +105,7 @@ export class Communities extends Component<any, CommunitiesState> {
                   {this.state.communities.map(community => (
                     <tr>
                       <td>
-                        <Link to={`/c/${community.name}`}>
-                          {community.name}
-                        </Link>
+                        <CommunityLink community={community} />
                       </td>
                       <td class="d-none d-lg-table-cell">{community.title}</td>
                       <td>{community.category_name}</td>
diff --git a/ui/src/components/community-link.tsx b/ui/src/components/community-link.tsx
new file mode 100644 (file)
index 0000000..eb55400
--- /dev/null
@@ -0,0 +1,38 @@
+import { Component } from 'inferno';
+import { Link } from 'inferno-router';
+import { Community } from '../interfaces';
+import { hostname } from '../utils';
+
+interface CommunityOther {
+  name: string;
+  id?: number; // Necessary if its federated
+  local?: boolean;
+  actor_id?: string;
+}
+
+interface CommunityLinkProps {
+  community: Community | CommunityOther;
+  realLink?: boolean;
+}
+
+export class CommunityLink extends Component<CommunityLinkProps, any> {
+  constructor(props: any, context: any) {
+    super(props, context);
+  }
+
+  render() {
+    let community = this.props.community;
+    let name_: string, link: string;
+    let local = community.local == null ? true : community.local;
+    if (local) {
+      name_ = community.name;
+      link = `/c/${community.name}`;
+    } else {
+      name_ = `${community.name}@${hostname(community.actor_id)}`;
+      link = !this.props.realLink
+        ? `/community/${community.id}`
+        : community.actor_id;
+    }
+    return <Link to={link}>{name_}</Link>;
+  }
+}
index a921de2c130fb258fb3ac7029cdb8341aad08652..373d8f807fefd902b5bacdc75ce6d4f2a1efddbf 100644 (file)
@@ -80,6 +80,11 @@ export class Community extends Component<any, State> {
       removed: null,
       nsfw: false,
       deleted: null,
+      local: null,
+      actor_id: null,
+      last_refreshed_at: null,
+      creator_actor_id: null,
+      creator_local: null,
     },
     moderators: [],
     admins: [],
index 829af2228081af5e4503a0ede2a20e8274faa6a7..c168feb0c4570e09c5b93696105eb91e9d26a5fd 100644 (file)
@@ -34,6 +34,7 @@ import { ListingTypeSelect } from './listing-type-select';
 import { DataTypeSelect } from './data-type-select';
 import { SiteForm } from './site-form';
 import { UserListing } from './user-listing';
+import { CommunityLink } from './community-link';
 import {
   wsJsonToRes,
   repoUrl,
@@ -190,9 +191,14 @@ export class Main extends Component<any, MainState> {
                       <ul class="list-inline">
                         {this.state.subscribedCommunities.map(community => (
                           <li class="list-inline-item">
-                            <Link to={`/c/${community.community_name}`}>
-                              {community.community_name}
-                            </Link>
+                            <CommunityLink
+                              community={{
+                                name: community.community_name,
+                                id: community.community_id,
+                                local: community.community_local,
+                                actor_id: community.community_actor_id,
+                              }}
+                            />
                           </li>
                         ))}
                       </ul>
@@ -228,7 +234,7 @@ export class Main extends Component<any, MainState> {
         <ul class="list-inline">
           {this.state.trendingCommunities.map(community => (
             <li class="list-inline-item">
-              <Link to={`/c/${community.name}`}>{community.name}</Link>
+              <CommunityLink community={community} />
             </li>
           ))}
         </ul>
@@ -321,6 +327,9 @@ export class Main extends Component<any, MainState> {
                     user={{
                       name: admin.name,
                       avatar: admin.avatar,
+                      local: admin.local,
+                      actor_id: admin.actor_id,
+                      id: admin.id,
                     }}
                   />
                 </li>
index c507ab3e700378c83bfb82916276c61aa032ae03..9f5aa363e63726d0566c00acd6407033b70be185 100644 (file)
@@ -35,6 +35,7 @@ import {
   setupTribute,
   setupTippy,
   emojiPicker,
+  hostname,
   pictrsDeleteToast,
 } from '../utils';
 import autosize from 'autosize';
@@ -333,7 +334,11 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
                 >
                   <option>{i18n.t('select_a_community')}</option>
                   {this.state.communities.map(community => (
-                    <option value={community.id}>{community.name}</option>
+                    <option value={community.id}>
+                      {community.local
+                        ? community.name
+                        : `${hostname(community.actor_id)}/${community.name}`}
+                    </option>
                   ))}
                 </select>
               </div>
index 7d10acf72139bd41586fa5e3299fa3b548498175..b4cc4f928298e82549fc8f395c66fdea751aa6d3 100644 (file)
@@ -20,6 +20,7 @@ import { MomentTime } from './moment-time';
 import { PostForm } from './post-form';
 import { IFramelyCard } from './iframely-card';
 import { UserListing } from './user-listing';
+import { CommunityLink } from './community-link';
 import {
   md,
   mdToHtml,
@@ -30,6 +31,7 @@ import {
   getUnixTime,
   pictrsImage,
   setupTippy,
+  hostname,
   previewLines,
 } from '../utils';
 import { i18n } from '../i18next';
@@ -314,22 +316,21 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
                     </Link>
                   )}
                 </h5>
-                {post.url &&
-                  !(new URL(post.url).hostname == window.location.hostname) && (
-                    <small class="d-inline-block">
-                      <a
-                        className="ml-2 text-muted font-italic"
-                        href={post.url}
-                        target="_blank"
-                        title={post.url}
-                      >
-                        {new URL(post.url).hostname}
-                        <svg class="ml-1 icon icon-inline">
-                          <use xlinkHref="#icon-external-link"></use>
-                        </svg>
-                      </a>
-                    </small>
-                  )}
+                {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}
+                    >
+                      {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 ? (
@@ -422,6 +423,9 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
                     user={{
                       name: post.creator_name,
                       avatar: post.creator_avatar,
+                      id: post.creator_id,
+                      local: post.creator_local,
+                      actor_id: post.creator_actor_id,
                     }}
                   />
                   {this.isMod && (
@@ -442,9 +446,14 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
                   {this.props.showCommunity && (
                     <span>
                       <span> {i18n.t('to')} </span>
-                      <Link to={`/c/${post.community_name}`}>
-                        {post.community_name}
-                      </Link>
+                      <CommunityLink
+                        community={{
+                          name: post.community_name,
+                          id: post.community_id,
+                          local: post.community_local,
+                          actor_id: post.community_actor_id,
+                        }}
+                      />
                     </span>
                   )}
                 </li>
index 496f3ae5755b491b90e99c244ee3db70fc57f09b..8cb7590e615c4ebf377964a9f1736e1cd78df03d 100644 (file)
@@ -135,6 +135,9 @@ export class PrivateMessageForm extends Component<
                     user={{
                       name: this.state.recipient.name,
                       avatar: this.state.recipient.avatar,
+                      id: this.state.recipient.id,
+                      local: this.state.recipient.local,
+                      actor_id: this.state.recipient.actor_id,
                     }}
                   />
                 </div>
index c32f4f2fbb7d99f05aadc87cf27f4fa6f65e7e3d..a02f035ffc34d8377c421b1c8faa160b75b9abab 100644 (file)
@@ -22,8 +22,6 @@ import {
   fetchLimit,
   routeSearchTypeToEnum,
   routeSortTypeToEnum,
-  pictrsAvatarThumbnail,
-  showAvatars,
   toast,
   createCommentLikeRes,
   createPostLikeFindRes,
@@ -31,6 +29,7 @@ import {
 } from '../utils';
 import { PostListing } from './post-listing';
 import { UserListing } from './user-listing';
+import { CommunityLink } from './community-link';
 import { SortSelect } from './sort-select';
 import { CommentNodes } from './comment-nodes';
 import { i18n } from '../i18next';
@@ -110,7 +109,6 @@ export class Search extends Component<any, SearchState> {
       nextProps.history.action == 'POP' ||
       nextProps.history.action == 'PUSH'
     ) {
-      this.state = this.emptyState;
       this.state.q = this.getSearchQueryFromProps(nextProps);
       this.state.type_ = this.getSearchTypeFromProps(nextProps);
       this.state.sort = this.getSortTypeFromProps(nextProps);
@@ -253,16 +251,7 @@ export class Search extends Component<any, SearchState> {
                 />
               )}
               {i.type_ == 'communities' && (
-                <div>
-                  <span>
-                    <Link to={`/c/${(i.data as Community).name}`}>{`/c/${
-                      (i.data as Community).name
-                    }`}</Link>
-                  </span>
-                  <span>{` - ${(i.data as Community).title} - ${
-                    (i.data as Community).number_of_subscribers
-                  } subscribers`}</span>
-                </div>
+                <div>{this.communityListing(i.data as Community)}</div>
               )}
               {i.type_ == 'users' && (
                 <div>
@@ -316,20 +305,28 @@ export class Search extends Component<any, SearchState> {
       <>
         {this.state.searchResponse.communities.map(community => (
           <div class="row">
-            <div class="col-12">
-              <span>
-                <Link
-                  to={`/c/${community.name}`}
-                >{`/c/${community.name}`}</Link>
-              </span>
-              <span>{` - ${community.title} - ${community.number_of_subscribers} subscribers`}</span>
-            </div>
+            <div class="col-12">{this.communityListing(community)}</div>
           </div>
         ))}
       </>
     );
   }
 
+  communityListing(community: Community) {
+    return (
+      <>
+        <span>
+          <CommunityLink community={community} />
+        </span>
+        <span>{` - ${community.title} - 
+        ${i18n.t('number_of_subscribers', {
+          count: community.number_of_subscribers,
+        })}
+      `}</span>
+      </>
+    );
+  }
+
   users() {
     return (
       <>
index b280ef4fbfabc64f7efc247c461b172dbd770cb0..17ac64b99c605f70f9857bba084b29ea8aa20e5d 100644 (file)
@@ -8,14 +8,10 @@ import {
   UserView,
 } from '../interfaces';
 import { WebSocketService, UserService } from '../services';
-import {
-  mdToHtml,
-  getUnixTime,
-  pictrsAvatarThumbnail,
-  showAvatars,
-} from '../utils';
+import { mdToHtml, getUnixTime, hostname } from '../utils';
 import { CommunityForm } from './community-form';
 import { UserListing } from './user-listing';
+import { CommunityLink } from './community-link';
 import { i18n } from '../i18next';
 
 interface SidebarProps {
@@ -65,6 +61,15 @@ 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">
@@ -82,9 +87,7 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
                 </small>
               )}
             </h5>
-            <Link className="text-muted" to={`/c/${community.name}`}>
-              /c/{community.name}
-            </Link>
+            <CommunityLink community={community} realLink />
             <ul class="list-inline mb-1 text-muted font-weight-bold">
               {this.canMod && (
                 <>
@@ -212,11 +215,15 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
                     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'
index ea87fd3aae778d8313853abe0ffbccc8d053fcc6..0e150b9420d3468d61cadd01ea229ab2dca01c54 100644 (file)
@@ -1,15 +1,19 @@
 import { Component } from 'inferno';
 import { Link } from 'inferno-router';
 import { UserView } from '../interfaces';
-import { pictrsAvatarThumbnail, showAvatars } from '../utils';
+import { pictrsAvatarThumbnail, showAvatars, hostname } from '../utils';
 
 interface UserOther {
   name: string;
+  id?: number; // Necessary if its federated
   avatar?: string;
+  local?: boolean;
+  actor_id?: string;
 }
 
 interface UserListingProps {
   user: UserView | UserOther;
+  realLink?: boolean;
 }
 
 export class UserListing extends Component<UserListingProps, any> {
@@ -19,8 +23,19 @@ 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;
+
+    if (local) {
+      name_ = user.name;
+      link = `/u/${user.name}`;
+    } else {
+      name_ = `${user.name}@${hostname(user.actor_id)}`;
+      link = !this.props.realLink ? `/user/${user.id}` : user.actor_id;
+    }
+
     return (
-      <Link className="text-body font-weight-bold" to={`/u/${user.name}`}>
+      <Link className="text-body font-weight-bold" to={link}>
         {user.avatar && showAvatars() && (
           <img
             height="32"
@@ -29,7 +44,7 @@ export class UserListing extends Component<UserListingProps, any> {
             class="rounded-circle mr-2"
           />
         )}
-        <span>{user.name}</span>
+        <span>{name_}</span>
       </Link>
     );
   }
index a791f0c8d6db79ff7d4af7786a01d440d06419fb..f635a1cd0b1d8eb48582b62f9c06fdb7234a0bb7 100644 (file)
@@ -40,6 +40,7 @@ import {
   setupTippy,
 } from '../utils';
 import { PostListing } from './post-listing';
+import { UserListing } from './user-listing';
 import { SortSelect } from './sort-select';
 import { ListingTypeSelect } from './listing-type-select';
 import { CommentNodes } from './comment-nodes';
@@ -90,6 +91,8 @@ export class User extends Component<any, UserState> {
       avatar: null,
       show_avatars: null,
       send_notifications_to_email: null,
+      actor_id: null,
+      local: null,
     },
     user_id: null,
     username: null,
@@ -398,7 +401,9 @@ export class User extends Component<any, UserState> {
           <div class="card-body">
             <h5>
               <ul class="list-inline mb-0">
-                <li className="list-inline-item">{user.name}</li>
+                <li className="list-inline-item">
+                  <UserListing user={user} realLink />
+                </li>
                 {user.banned && (
                   <li className="list-inline-item badge badge-danger">
                     {i18n.t('banned')}
index 5003986b51a83e29a9aa5e2d6c24fd60cdbe0a08..a57b93498fcd533993f025b9c654735ef5795456 100644 (file)
@@ -1,6 +1,6 @@
 const host = `${window.location.hostname}`;
 const port = `${
-  window.location.port == '4444' ? '8536' : window.location.port
+  window.location.port == '4444' ? '8540' : window.location.port
 }`;
 const endpoint = `${host}:${port}`;
 
index b77ccac65b657f120df8ae416f7ac43fb988e268..7e29319f9a666a0029ab35444b32118f6add1b01 100644 (file)
@@ -100,10 +100,13 @@ export interface User {
 
 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;
@@ -117,15 +120,21 @@ export interface UserView {
 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;
@@ -136,6 +145,9 @@ export interface Community {
   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;
@@ -161,13 +173,19 @@ export interface Post {
   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_avatar?: string;
+  community_actor_id: string;
+  community_local: boolean;
   community_name: string;
   community_removed: boolean;
   community_deleted: boolean;
@@ -188,6 +206,8 @@ export interface Post {
 
 export interface Comment {
   id: number;
+  ap_id: string;
+  local: boolean;
   creator_id: number;
   post_id: number;
   parent_id?: number;
@@ -198,9 +218,13 @@ export interface Comment {
   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;
   score: number;
@@ -213,6 +237,8 @@ export interface Comment {
   saved?: boolean;
   user_mention_id?: number; // For mention type
   recipient_id?: number;
+  recipient_actor_id?: string;
+  recipient_local?: boolean;
   depth?: number;
 }
 
@@ -247,10 +273,16 @@ export interface PrivateMessage {
   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 {
@@ -628,7 +660,7 @@ export interface CommentForm {
   post_id: number;
   parent_id?: number;
   edit_id?: number;
-  creator_id: number;
+  creator_id?: number;
   removed?: boolean;
   deleted?: boolean;
   reason?: string;
index bdb9afbd2da3f243fc74ef8c0ce0a833f0526d68..071b86acea6045bfe2928f738b056f8c411d4bfa 100644 (file)
@@ -592,7 +592,10 @@ export function setupTribute(): Tribute {
       {
         trigger: '@',
         selectTemplate: (item: any) => {
-          return `[/u/${item.original.key}](/u/${item.original.key})`;
+          let link = item.original.local
+            ? `[${item.original.key}](/u/${item.original.name})`
+            : `[${item.original.key}](/user/${item.original.id})`;
+          return link;
         },
         values: (text: string, cb: any) => {
           userSearch(text, (users: any) => cb(users));
@@ -605,9 +608,12 @@ export function setupTribute(): Tribute {
 
       // Communities
       {
-        trigger: '#',
+        trigger: '!',
         selectTemplate: (item: any) => {
-          return `[/c/${item.original.key}](/c/${item.original.key})`;
+          let link = item.original.local
+            ? `[${item.original.key}](/c/${item.original.name})`
+            : `[${item.original.key}](/community/${item.original.id})`;
+          return link;
         },
         values: (text: string, cb: any) => {
           communitySearch(text, (communities: any) => cb(communities));
@@ -650,7 +656,12 @@ function userSearch(text: string, cb: any) {
         if (res.op == UserOperation.Search) {
           let data = res.data as SearchResponse;
           let users = data.users.map(u => {
-            return { key: u.name };
+            return {
+              key: `@${u.name}@${hostname(u.actor_id)}`,
+              name: u.name,
+              local: u.local,
+              id: u.id,
+            };
           });
           cb(users);
           this.userSub.unsubscribe();
@@ -681,8 +692,13 @@ function communitySearch(text: string, cb: any) {
         let res = wsJsonToRes(msg);
         if (res.op == UserOperation.Search) {
           let data = res.data as SearchResponse;
-          let communities = data.communities.map(u => {
-            return { key: u.name };
+          let communities = data.communities.map(c => {
+            return {
+              key: `!${c.name}@${hostname(c.actor_id)}`,
+              name: c.name,
+              local: c.local,
+              id: c.id,
+            };
           });
           cb(communities);
           this.communitySub.unsubscribe();
@@ -933,6 +949,13 @@ export function previewLines(text: string, lines: number = 3): string {
     .join('\n');
 }
 
+export function hostname(url: string): string {
+  let cUrl = new URL(url);
+  return window.location.port
+    ? `${cUrl.hostname}:${cUrl.port}`
+    : `${cUrl.hostname}`;
+}
+
 function canUseWebP() {
   // TODO pictshare might have a webp conversion bug, try disabling this
   return false;
index 35ad32a0ac2ea8390e621ed82083676bca5d49c0..076083fb9878bd2f2d4c84f44c9f59553e45b5b6 100644 (file)
@@ -9,6 +9,28 @@
   dependencies:
     "@babel/highlight" "^7.8.3"
 
+"@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"
+    convert-source-map "^1.7.0"
+    debug "^4.1.0"
+    gensync "^1.0.0-beta.1"
+    json5 "^2.1.2"
+    lodash "^4.17.13"
+    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"
     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==
+  dependencies:
+    "@babel/types" "^7.9.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"
     "@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"
   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"
   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"
     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"
   resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.8.4.tgz#d1dbe64691d60358a974295fa53da074dd2ce8e8"
   integrity sha512-0fKu/QqildpXmPVaRBoXOlyBb3MC+J0A66x97qEfLOMkn3u6nfY5esWogQwi/K0BjASYy4DbnsEWnpNL6qT5Mw==
 
+"@babel/plugin-syntax-async-generators@^7.8.4":
+  version "7.8.4"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz#a983fb1aeb2ec3f6ed042a210f640e90e786fe0d"
+  integrity sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.8.0"
+
+"@babel/plugin-syntax-bigint@^7.8.3":
+  version "7.8.3"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz#4c9a6f669f5d0cdf1b90a1671e9a146be5300cea"
+  integrity sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==
+  dependencies:
+    "@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==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.8.3"
+
+"@babel/plugin-syntax-json-strings@^7.8.3":
+  version "7.8.3"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz#01ca21b668cd8218c9e640cb6dd88c5412b2c96a"
+  integrity sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==
+  dependencies:
+    "@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==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.8.3"
+
+"@babel/plugin-syntax-nullish-coalescing-operator@^7.8.3":
+  version "7.8.3"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz#167ed70368886081f74b5c36c65a88c03b66d1a9"
+  integrity sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==
+  dependencies:
+    "@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==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.8.3"
+
+"@babel/plugin-syntax-object-rest-spread@^7.8.3":
+  version "7.8.3"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz#60e225edcbd98a640332a2e72dd3e66f1af55871"
+  integrity sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.8.0"
+
+"@babel/plugin-syntax-optional-catch-binding@^7.8.3":
+  version "7.8.3"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz#6111a265bcfb020eb9efd0fdfd7d26402b9ed6c1"
+  integrity sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.8.0"
+
+"@babel/plugin-syntax-optional-chaining@^7.8.3":
+  version "7.8.3"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz#4f69c2ab95167e0180cd5336613f8c5788f7d48a"
+  integrity sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==
+  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"
   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==
+  dependencies:
+    "@babel/code-frame" "^7.8.3"
+    "@babel/parser" "^7.8.6"
+    "@babel/types" "^7.8.6"
+
 "@babel/template@^7.8.3":
   version "7.8.3"
   resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.8.3.tgz#e02ad04fe262a657809327f578056ca15fd4d1b8"
     "@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"
     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"
     lodash "^4.17.13"
     to-fast-properties "^2.0.0"
 
+"@bcoe/v8-coverage@^0.2.3":
+  version "0.2.3"
+  resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39"
+  integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==
+
+"@cnakazawa/watch@^1.0.3":
+  version "1.0.4"
+  resolved "https://registry.yarnpkg.com/@cnakazawa/watch/-/watch-1.0.4.tgz#f864ae85004d0fcab6f50be9141c4da368d1656a"
+  integrity sha512-v9kIhKwjeZThiWrLmj0y17CWoyddASLj9O2yvbZkbvw/N3rWOYy9zkV66ursAoVr0mV15bL8g0c4QZUE6cdDoQ==
+  dependencies:
+    exec-sh "^0.3.2"
+    minimist "^1.2.0"
+
 "@fortawesome/fontawesome-common-types@^0.2.28":
   version "0.2.28"
   resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-0.2.28.tgz#1091bdfe63b3f139441e9cba27aa022bff97d8b2"
   dependencies:
     "@fortawesome/fontawesome-common-types" "^0.2.28"
 
+"@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==
+  dependencies:
+    camelcase "^5.3.1"
+    find-up "^4.1.0"
+    js-yaml "^3.13.1"
+    resolve-from "^5.0.0"
+
+"@istanbuljs/schema@^0.1.2":
+  version "0.1.2"
+  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==
+  dependencies:
+    "@jest/types" "^25.4.0"
+    chalk "^3.0.0"
+    jest-message-util "^25.4.0"
+    jest-util "^25.4.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==
+  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"
+    ansi-escapes "^4.2.1"
+    chalk "^3.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"
+    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==
+  dependencies:
+    "@jest/fake-timers" "^25.4.0"
+    "@jest/types" "^25.4.0"
+    jest-mock "^25.4.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==
+  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/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==
+  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"
+    collect-v8-coverage "^1.0.0"
+    exit "^0.1.2"
+    glob "^7.1.2"
+    istanbul-lib-coverage "^3.0.0"
+    istanbul-lib-instrument "^4.0.0"
+    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"
+    slash "^3.0.0"
+    source-map "^0.6.0"
+    string-length "^3.1.0"
+    terminal-link "^2.0.0"
+    v8-to-istanbul "^4.1.3"
+  optionalDependencies:
+    node-notifier "^6.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==
+  dependencies:
+    callsites "^3.0.0"
+    graceful-fs "^4.2.3"
+    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==
+  dependencies:
+    "@jest/console" "^25.4.0"
+    "@jest/types" "^25.4.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==
+  dependencies:
+    "@jest/test-result" "^25.4.0"
+    jest-haste-map "^25.4.0"
+    jest-runner "^25.4.0"
+    jest-runtime "^25.4.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==
+  dependencies:
+    "@babel/core" "^7.1.0"
+    "@jest/types" "^25.4.0"
+    babel-plugin-istanbul "^6.0.0"
+    chalk "^3.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"
+    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==
+  dependencies:
+    "@types/istanbul-lib-coverage" "^2.0.0"
+    "@types/istanbul-reports" "^1.1.1"
+    "@types/yargs" "^15.0.0"
+    chalk "^3.0.0"
+
 "@joeattardi/emoji-button@^2.12.1":
   version "2.12.1"
   resolved "https://registry.yarnpkg.com/@joeattardi/emoji-button/-/emoji-button-2.12.1.tgz#190df7c00721e04742ed6f8852db828798a4cf98"
   dependencies:
     any-observable "^0.3.0"
 
+"@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==
+  dependencies:
+    type-detect "4.0.8"
+
 "@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==
+  dependencies:
+    "@babel/parser" "^7.1.0"
+    "@babel/types" "^7.0.0"
+    "@types/babel__generator" "*"
+    "@types/babel__template" "*"
+    "@types/babel__traverse" "*"
+
+"@types/babel__generator@*":
+  version "7.6.1"
+  resolved "https://registry.yarnpkg.com/@types/babel__generator/-/babel__generator-7.6.1.tgz#4901767b397e8711aeb99df8d396d7ba7b7f0e04"
+  integrity sha512-bBKm+2VPJcMRVwNhxKu8W+5/zT7pwNEqeokFOmbvVSqGzFneNxYcEBro9Ac7/N9tlsaPYnZLK8J1LWKkMsLAew==
+  dependencies:
+    "@babel/types" "^7.0.0"
+
+"@types/babel__template@*":
+  version "7.0.2"
+  resolved "https://registry.yarnpkg.com/@types/babel__template/-/babel__template-7.0.2.tgz#4ff63d6b52eddac1de7b975a5223ed32ecea9307"
+  integrity sha512-/K6zCpeW7Imzgab2bLkLEbz0+1JlFSrUMdw7KoIIu+IUdu51GWaBZpd3y1VXGVXzynvGa4DaIaxNZHiON3GXUg==
+  dependencies:
+    "@babel/parser" "^7.1.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==
+  dependencies:
+    "@babel/types" "^7.3.0"
+
 "@types/color-name@^1.1.1":
   version "1.1.1"
   resolved "https://registry.yarnpkg.com/@types/color-name/-/color-name-1.1.1.tgz#1c1261bbeaa10a8055bbc5d8ab84b7b2afc846a0"
   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/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==
+
+"@types/istanbul-lib-report@*":
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz#c14c24f18ea8190c118ee7562b7ff99a36552686"
+  integrity sha512-plGgXAPfVKFoYfa9NpYDAkseG+g6Jr294RqeqcqDixSbU34MZVJRi/P+7Y8GDpzkEwLaGZZOpKIEmeVZNtKsrg==
+  dependencies:
+    "@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==
+  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==
+  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"
   dependencies:
     "@types/markdown-it" "*"
 
-"@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==
   dependencies:
     "@types/linkify-it" "*"
 
-"@types/markdown-it@^10.0.0":
-  version "10.0.0"
-  resolved "https://registry.yarnpkg.com/@types/markdown-it/-/markdown-it-10.0.0.tgz#a2b5f9fb444bb27c1e0c4a0116fea09b3c6ebc1e"
-  integrity sha512-7UPBg1W0rfsqQ1JwNFfhxibKO0t7Q0scNt96XcFIFLGE/vhZamzZayaFS2LKha/26Pz7b/2GgiaxQZ1GUwW0dA==
+"@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==
   dependencies:
-    "@types/linkify-it" "*"
-    "@types/mdurl" "*"
+    "@types/node" "*"
+    form-data "^3.0.0"
 
-"@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@*":
+  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/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/sizzle@*":
   version "2.3.2"
   resolved "https://registry.yarnpkg.com/@types/sizzle/-/sizzle-2.3.2.tgz#a811b8c18e2babab7d542b3365887ae2e4d9de47"
   integrity sha512-7EJYyKTL7tFR8+gDbB6Wwz/arpGa0Mywk1TJbNzKzHtzbwVmY4HR9WqS5VV7dsBUKQmPNr192jHr/VpBluj/hg==
 
+"@types/stack-utils@^1.0.1":
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-1.0.1.tgz#0a851d3bd96498fa25c33ab7278ed3bd65f06c3e"
+  integrity sha512-l42BggppR6zLmpfU6fq9HEa2oGPEI8yrSPL3GITjfRInppYFahObbIQOQK3UGxEnyQpltZLaPe75046NOZQikw==
+
+"@types/yargs-parser@*":
+  version "15.0.0"
+  resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-15.0.0.tgz#cb3f9f741869e20cce330ffbeb9271590483882d"
+  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==
+  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"
     semver "^6.3.0"
     tsutils "^3.17.1"
 
+abab@^2.0.0:
+  version "2.0.3"
+  resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.3.tgz#623e2075e02eb2d3f2475e49f99c91846467907a"
+  integrity sha512-tsFzPpcttalNjFBCFMqsKYQcWxxen1pgJR56by//QwvJc4/OUS3kPOOttx2tSIfjsylB0pYu7f5D3K1RCxUnUg==
+
 accepts@~1.3.7:
   version "1.3.7"
   resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.7.tgz#531bc726517a3b2b41f850021c6cc15eaab507cd"
@@ -347,6 +847,14 @@ 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==
+  dependencies:
+    acorn "^6.0.1"
+    acorn-walk "^6.0.1"
+
 acorn-jsx@^4.0.1:
   version "4.1.1"
   resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-4.1.1.tgz#e8e41e48ea2fe0c896740610ab6a4ffd8add225e"
@@ -359,11 +867,21 @@ acorn-jsx@^5.1.0:
   resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.1.0.tgz#294adb71b57398b0680015f0a38c563ee1db5384"
   integrity sha512-tMUqwBWfLFbJbizRmEcWSLw6HnFzfdJs2sOJEOwwtVPMoH/0Ay+E703oZz78VSXZiiDcZrQ5XKjPIUQixhmgVw==
 
+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@^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.0:
   version "7.1.0"
   resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.1.0.tgz#949d36f2c292535da602283586c2477c57eb2d6c"
@@ -431,7 +949,7 @@ ansi-styles@^3.2.0, ansi-styles@^3.2.1:
   dependencies:
     color-convert "^1.9.0"
 
-ansi-styles@^4.1.0:
+ansi-styles@^4.0.0, ansi-styles@^4.1.0:
   version "4.2.1"
   resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.2.1.tgz#90ae75c424d008d2624c5bf29ead3177ebfcf359"
   integrity sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==
@@ -457,6 +975,22 @@ anymatch@^1.3.0:
     micromatch "^2.1.5"
     normalize-path "^2.0.0"
 
+anymatch@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-2.0.0.tgz#bcb24b4f37934d9aa7ac17b4adaf89e7c76ef2eb"
+  integrity sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==
+  dependencies:
+    micromatch "^3.1.4"
+    normalize-path "^2.1.1"
+
+anymatch@^3.0.3:
+  version "3.1.1"
+  resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.1.tgz#c55ecf02185e2469259399310c173ce31233b142"
+  integrity sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg==
+  dependencies:
+    normalize-path "^3.0.0"
+    picomatch "^2.0.4"
+
 app-root-path@^1.3.0:
   version "1.4.0"
   resolved "https://registry.yarnpkg.com/app-root-path/-/app-root-path-1.4.0.tgz#6335d865c9640d0fad99004e5a79232238e92dfa"
@@ -509,6 +1043,11 @@ 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"
@@ -618,6 +1157,61 @@ 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==
+  dependencies:
+    "@jest/transform" "^25.4.0"
+    "@jest/types" "^25.4.0"
+    "@types/babel__core" "^7.1.7"
+    babel-plugin-istanbul "^6.0.0"
+    babel-preset-jest "^25.4.0"
+    chalk "^3.0.0"
+    slash "^3.0.0"
+
+babel-plugin-istanbul@^6.0.0:
+  version "6.0.0"
+  resolved "https://registry.yarnpkg.com/babel-plugin-istanbul/-/babel-plugin-istanbul-6.0.0.tgz#e159ccdc9af95e0b570c75b4573b7c34d671d765"
+  integrity sha512-AF55rZXpe7trmEylbaE1Gv54wn6rwU03aptvRoVIGP8YykoSxqdVLV1TfwflBCE/QtHmqtP8SWlTENqbK8GCSQ==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.0.0"
+    "@istanbuljs/load-nyc-config" "^1.0.0"
+    "@istanbuljs/schema" "^0.1.2"
+    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==
+  dependencies:
+    "@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==
+  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-json-strings" "^7.8.3"
+    "@babel/plugin-syntax-logical-assignment-operators" "^7.8.3"
+    "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.3"
+    "@babel/plugin-syntax-numeric-separator" "^7.8.3"
+    "@babel/plugin-syntax-object-rest-spread" "^7.8.3"
+    "@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==
+  dependencies:
+    babel-plugin-jest-hoist "^25.4.0"
+    babel-preset-current-node-syntax "^0.1.2"
+
 balanced-match@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767"
@@ -734,7 +1328,33 @@ braces@^3.0.1:
   dependencies:
     fill-range "^7.0.1"
 
-buffer-from@^1.0.0:
+browser-process-hrtime@^1.0.0:
+  version "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"
+  integrity sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==
+  dependencies:
+    fast-json-stable-stringify "2.x"
+
+bser@2.1.1:
+  version "2.1.1"
+  resolved "https://registry.yarnpkg.com/bser/-/bser-2.1.1.tgz#e6787da20ece9d07998533cfd9de6f5c38f4bc05"
+  integrity sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==
+  dependencies:
+    node-int64 "^0.4.0"
+
+buffer-from@1.x, buffer-from@^1.0.0:
   version "1.1.1"
   resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef"
   integrity sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==
@@ -764,6 +1384,18 @@ callsites@^3.0.0:
   resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73"
   integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==
 
+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==
+
+capture-exit@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/capture-exit/-/capture-exit-2.0.0.tgz#fb953bfaebeb781f62898239dabb426d08a509a4"
+  integrity sha512-PiT/hQmTonHhl/HFGN+Lx3JJUznrVYJ3+AQsnthneZbvW7x+f08Tk7yLJTLEOUvBTbduLeeBkxEaYXUOUrRq6g==
+  dependencies:
+    rsvp "^4.8.4"
+
 caseless@~0.12.0:
   version "0.12.0"
   resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc"
@@ -902,11 +1534,30 @@ cli-width@^2.0.0:
   resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-2.2.0.tgz#ff19ede8a9a5e579324147b0c11f0fbcbabed639"
   integrity sha1-/xnt6Kml5XkyQUewwR8PvLq+1jk=
 
+cliui@^6.0.0:
+  version "6.0.0"
+  resolved "https://registry.yarnpkg.com/cliui/-/cliui-6.0.0.tgz#511d702c0c4e41ca156d7d0e96021f23e13225b1"
+  integrity sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==
+  dependencies:
+    string-width "^4.2.0"
+    strip-ansi "^6.0.0"
+    wrap-ansi "^6.2.0"
+
+co@^4.6.0:
+  version "4.6.0"
+  resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184"
+  integrity sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=
+
 code-point-at@^1.0.0:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77"
   integrity sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=
 
+collect-v8-coverage@^1.0.0:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/collect-v8-coverage/-/collect-v8-coverage-1.0.1.tgz#cc2c8e94fc18bbdffe64d6534570c8a673b27f59"
+  integrity sha512-iBPtljfCNcTKNAto0KEtDfZ3qzjJvqE3aTGZsbhjSBlorqpXJlaWWtPO35D+ZImoC3KWejX64o+yPGxhWSTzfg==
+
 collection-visit@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/collection-visit/-/collection-visit-1.0.0.tgz#4bc0373c164bc3291b4d368c829cf1a80a59dca0"
@@ -939,7 +1590,7 @@ 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==
 
-combined-stream@^1.0.6, combined-stream@~1.0.6:
+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"
   integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==
@@ -988,6 +1639,13 @@ content-type@~1.0.4:
   resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b"
   integrity sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==
 
+convert-source-map@^1.4.0, convert-source-map@^1.6.0, convert-source-map@^1.7.0:
+  version "1.7.0"
+  resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.7.0.tgz#17a2cb882d7f77d3490585e2ce6c524424a3a442"
+  integrity sha512-4FJkXzKXEDB1snCFZlLP4gpC3JILicCpGbzG9f9G7tGqGCzETQ2hWPrcinA9oU4wtf2biUaEH5065UnMeR33oA==
+  dependencies:
+    safe-buffer "~5.1.1"
+
 cookie-signature@1.0.6:
   version "1.0.6"
   resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c"
@@ -1024,7 +1682,7 @@ cosmiconfig@^6.0.0:
     path-type "^4.0.0"
     yaml "^1.7.2"
 
-cross-spawn@^6.0.5:
+cross-spawn@^6.0.0, cross-spawn@^6.0.5:
   version "6.0.5"
   resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4"
   integrity sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==
@@ -1044,6 +1702,23 @@ cross-spawn@^7.0.0:
     shebang-command "^2.0.0"
     which "^2.0.1"
 
+cssom@^0.4.1:
+  version "0.4.4"
+  resolved "https://registry.yarnpkg.com/cssom/-/cssom-0.4.4.tgz#5a66cf93d2d0b661d80bf6a44fb65f5c2e4e0a10"
+  integrity sha512-p3pvU7r1MyyqbTk+WbNJIgJjG2VmTIaB10rI93LzVPrmDJKkzKYMtxxyAvQXR/NS6otuzveI7+7BBq3SjBS2mw==
+
+cssom@~0.3.6:
+  version "0.3.8"
+  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==
+  dependencies:
+    cssom "~0.3.6"
+
 damerau-levenshtein@^1.0.4:
   version "1.0.6"
   resolved "https://registry.yarnpkg.com/damerau-levenshtein/-/damerau-levenshtein-1.0.6.tgz#143c1641cb3d85c60c32329e26899adea8701791"
@@ -1056,6 +1731,15 @@ 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==
+  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"
@@ -1075,6 +1759,11 @@ debug@^4.0.1, debug@^4.1.0, debug@^4.1.1:
   dependencies:
     ms "^2.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=
+
 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"
@@ -1090,6 +1779,11 @@ deep-is@~0.1.3:
   resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34"
   integrity sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=
 
+deepmerge@^4.2.2:
+  version "4.2.2"
+  resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.2.2.tgz#44d2ea3679b8f4d4ffba33f03d865fc1e7bf4955"
+  integrity sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==
+
 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"
@@ -1134,6 +1828,16 @@ destroy@~1.0.4:
   resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80"
   integrity sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=
 
+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==
+
+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@^4.0.1:
   version "4.0.2"
   resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d"
@@ -1161,6 +1865,13 @@ 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==
+  dependencies:
+    webidl-conversions "^4.0.2"
+
 dotenv@^8.2.0:
   version "8.2.0"
   resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-8.2.0.tgz#97e619259ada750eea3e4ea3e26bceea5424b16a"
@@ -1259,6 +1970,18 @@ escape-string-regexp@^1.0.2, escape-string-regexp@^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"
+
 escodegen@^1.8.1:
   version "1.13.0"
   resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-1.13.0.tgz#c7adf9bd3f3cc675bb752f202f79a720189cab29"
@@ -1589,7 +2312,25 @@ exec-sh@^0.2.0:
   dependencies:
     merge "^1.2.0"
 
-execa@^3.4.0:
+exec-sh@^0.3.2:
+  version "0.3.4"
+  resolved "https://registry.yarnpkg.com/exec-sh/-/exec-sh-0.3.4.tgz#3a018ceb526cc6f6df2bb504b2bfe8e3a4934ec5"
+  integrity sha512-sEFIkc61v75sWeOe72qyrqg2Qg0OuLESziUDk/O/z2qgS15y2gWVFrI6f2Qn/qw/0/NCfCEsmNA4zOjkwEZT1A==
+
+execa@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/execa/-/execa-1.0.0.tgz#c6236a5bb4df6d6f15e88e7f017798216749ddd8"
+  integrity sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==
+  dependencies:
+    cross-spawn "^6.0.0"
+    get-stream "^4.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@^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==
@@ -1605,6 +2346,11 @@ execa@^3.4.0:
     signal-exit "^3.0.2"
     strip-final-newline "^2.0.0"
 
+exit@^0.1.2:
+  version "0.1.2"
+  resolved "https://registry.yarnpkg.com/exit/-/exit-0.1.2.tgz#0632638f8d877cc82107d30a0fff1a17cba1cd0c"
+  integrity sha1-BjJjj42HfMghB9MKD/8aF8uhzQw=
+
 expand-brackets@^0.1.4:
   version "0.1.5"
   resolved "https://registry.yarnpkg.com/expand-brackets/-/expand-brackets-0.1.5.tgz#df07284e342a807cd733ac5af72411e581d1177b"
@@ -1632,6 +2378,18 @@ 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==
+  dependencies:
+    "@jest/types" "^25.4.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"
+
 express@^4.14.0:
   version "4.17.1"
   resolved "https://registry.yarnpkg.com/express/-/express-4.17.1.tgz#4491fc38605cf51f8629d39c2b5d026f98a4c134"
@@ -1747,7 +2505,7 @@ fast-diff@^1.1.2:
   resolved "https://registry.yarnpkg.com/fast-diff/-/fast-diff-1.2.0.tgz#73ee11982d86caaf7959828d519cfe927fac5f03"
   integrity sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w==
 
-fast-json-stable-stringify@^2.0.0:
+fast-json-stable-stringify@2.x, fast-json-stable-stringify@^2.0.0:
   version "2.1.0"
   resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633"
   integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==
@@ -1757,6 +2515,13 @@ fast-levenshtein@~2.0.6:
   resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917"
   integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=
 
+fb-watchman@^2.0.0:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/fb-watchman/-/fb-watchman-2.0.1.tgz#fc84fb39d2709cf3ff6d743706157bb5708a8a85"
+  integrity sha512-DkPJKQeY6kKwmuMretBhr7G6Vodr7bFwDYTXIkfG1gjvNpaxBTQV3PbXg6bR1c1UP4jPOX0jHUbbHANL9vRjVg==
+  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"
@@ -1920,6 +2685,15 @@ forever-agent@~0.6.1:
   resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91"
   integrity sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=
 
+form-data@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/form-data/-/form-data-3.0.0.tgz#31b7e39c85f1355b7139ee0c647cf0de7f83c682"
+  integrity sha512-CKMFDglpbMi6PyN+brwB9Q/GOw0eAnsrEZDgcsH5Krhz5Od/haKHAX0NmQfha2zPPz0JpWzA7GJHGSnvCRLWsg==
+  dependencies:
+    asynckit "^0.4.0"
+    combined-stream "^1.0.8"
+    mime-types "^2.1.12"
+
 form-data@~2.3.2:
   version "2.3.3"
   resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.3.tgz#dcce52c05f644f298c6a7ab936bd724ceffbf3a6"
@@ -1977,6 +2751,11 @@ fsevents@^1.0.0:
     bindings "^1.5.0"
     nan "^2.12.1"
 
+fsevents@^2.1.2:
+  version "2.1.3"
+  resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.1.3.tgz#fb738703ae8d2f9fe900c33836ddebee8b97f23e"
+  integrity sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ==
+
 function-bind@^1.1.1:
   version "1.1.1"
   resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d"
@@ -2033,6 +2812,16 @@ fuse-concat-with-sourcemaps@^1.0.5:
   dependencies:
     source-map "^0.6.1"
 
+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==
+
+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"
+  integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==
+
 get-own-enumerable-property-symbols@^3.0.0:
   version "3.0.2"
   resolved "https://registry.yarnpkg.com/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-3.0.2.tgz#b5fde77f22cbe35f390b4e089922c50bce6ef664"
@@ -2043,6 +2832,13 @@ get-stdin@^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@^4.0.0:
+  version "4.1.0"
+  resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-4.1.0.tgz#c1b255575f3dc21d59bfc79cd3d2b46b1c3a54b5"
+  integrity sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==
+  dependencies:
+    pump "^3.0.0"
+
 get-stream@^5.0.0:
   version "5.1.0"
   resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-5.1.0.tgz#01203cdc92597f9b909067c3e656cc1f4d3c4dc9"
@@ -2089,7 +2885,7 @@ glob-parent@^5.0.0:
   dependencies:
     is-glob "^4.0.1"
 
-glob@^7.1.1, glob@^7.1.3, glob@^7.1.6:
+glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6:
   version "7.1.6"
   resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6"
   integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==
@@ -2113,17 +2909,22 @@ globals@^12.1.0:
   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.0:
+graceful-fs@^4.1.11, graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, 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==
 
+growly@^1.3.0:
+  version "1.3.0"
+  resolved "https://registry.yarnpkg.com/growly/-/growly-1.3.0.tgz#f10748cbe76af964b7c96c93c6bcc28af120c081"
+  integrity sha1-8QdIy+dq+WS3yWyTxrzCivEgwIE=
+
 har-schema@^2.0.0:
   version "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.0, 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==
@@ -2213,6 +3014,18 @@ hosted-git-info@^2.1.4:
   resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.5.tgz#759cfcf2c4d156ade59b0b2dfabddc42a6b9c70c"
   integrity sha512-kssjab8CvdXfcXMXVcvsXum4Hwdq9XGtRD3TteMEvEbq0LXyiNQr6AprqKqfeaDXze7SxWvRxdpwE6ku7ikLkg==
 
+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==
+  dependencies:
+    whatwg-encoding "^1.0.1"
+
+html-escaper@^2.0.0:
+  version "2.0.2"
+  resolved "https://registry.yarnpkg.com/html-escaper/-/html-escaper-2.0.2.tgz#dfd60027da36a36dfcbe236262c00a5822681453"
+  integrity sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==
+
 html-parse-stringify2@^2.0.1:
   version "2.0.1"
   resolved "https://registry.yarnpkg.com/html-parse-stringify2/-/html-parse-stringify2-2.0.1.tgz#dc5670b7292ca158b7bc916c9a6735ac8872834a"
@@ -2309,6 +3122,14 @@ import-fresh@^3.0.0, import-fresh@^3.1.0:
     parent-module "^1.0.0"
     resolve-from "^4.0.0"
 
+import-local@^3.0.2:
+  version "3.0.2"
+  resolved "https://registry.yarnpkg.com/import-local/-/import-local-3.0.2.tgz#a8cfd0431d1de4a2199703d003e3e62364fa6db6"
+  integrity sha512-vjL3+w0oulAVZ0hBHnxa/Nm5TAurf9YLQJDhqRZyqb+VKGOB6LU8t9H1Nr5CIo16vh9XfJTOoHwU0B71S557gA==
+  dependencies:
+    pkg-dir "^4.2.0"
+    resolve-cwd "^3.0.0"
+
 import-modules@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/import-modules/-/import-modules-2.0.0.tgz#9c1e13b4e7a15682f70a6e3fa29534e4540cfc5d"
@@ -2463,6 +3284,11 @@ internal-slot@^1.0.2:
     has "^1.0.3"
     side-channel "^1.0.2"
 
+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"
@@ -2504,6 +3330,13 @@ is-callable@^1.1.4, is-callable@^1.1.5:
   resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.1.5.tgz#f7e46b596890456db74e7f6e976cb3273d06faab"
   integrity sha512-ESKv5sMCJB2jnHTWZ3O5itG+O128Hsus4K4Qh1h2/cgn2vbgnLSVqfV46AeJA9D5EeeLa9w81KUXMtn34zhX+Q==
 
+is-ci@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/is-ci/-/is-ci-2.0.0.tgz#6bc6334181810e04b5c22b3d589fdca55026404c"
+  integrity sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w==
+  dependencies:
+    ci-info "^2.0.0"
+
 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"
@@ -2592,6 +3425,11 @@ is-fullwidth-code-point@^3.0.0:
   resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d"
   integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==
 
+is-generator-fn@^2.0.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/is-generator-fn/-/is-generator-fn-2.1.0.tgz#7d140adc389aaf3011a8f2a2a4cfa6faadffb118"
+  integrity sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==
+
 is-glob@^2.0.0, is-glob@^2.0.1:
   version "2.0.1"
   resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-2.0.1.tgz#d096f926a3ded5600f3fdfd91198cb0888c2d863"
@@ -2698,7 +3536,7 @@ is-symbol@^1.0.2:
   dependencies:
     has-symbols "^1.0.1"
 
-is-typedarray@~1.0.0:
+is-typedarray@^1.0.0, is-typedarray@~1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a"
   integrity sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=
@@ -2708,37 +3546,440 @@ 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==
+
 isarray@0.0.1:
   version "0.0.1"
   resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf"
   integrity sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=
 
-isarray@1.0.0, isarray@^1.0.0, isarray@~1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11"
-  integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=
+isarray@1.0.0, isarray@^1.0.0, isarray@~1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11"
+  integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=
+
+isexe@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10"
+  integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=
+
+isobject@^2.0.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/isobject/-/isobject-2.1.0.tgz#f065561096a3f1da2ef46272f815c840d87e0c89"
+  integrity sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=
+  dependencies:
+    isarray "1.0.0"
+
+isobject@^3.0.0, isobject@^3.0.1:
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df"
+  integrity sha1-TkMekrEalzFjaqH5yNHMvP2reN8=
+
+isstream@~0.1.2:
+  version "0.1.2"
+  resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a"
+  integrity sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=
+
+istanbul-lib-coverage@^3.0.0:
+  version "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==
+  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"
+
+istanbul-lib-report@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz#7518fe52ea44de372f460a76b5ecda9ffb73d8a6"
+  integrity sha512-wcdi+uAKzfiGT2abPpKZ0hSU1rGQjUQnLvtY5MpQ7QCTahD3VODhcu4wcfY1YtkGaDD5yuydOLINXsfbus9ROw==
+  dependencies:
+    istanbul-lib-coverage "^3.0.0"
+    make-dir "^3.0.0"
+    supports-color "^7.1.0"
+
+istanbul-lib-source-maps@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.0.tgz#75743ce6d96bb86dc7ee4352cf6366a23f0b1ad9"
+  integrity sha512-c16LpFRkR8vQXyHZ5nLpY35JZtzj1PQY1iZmesUbf1FZHbIupcWfjgOXBY9YHkLEQ6puz1u4Dgj6qmU/DisrZg==
+  dependencies:
+    debug "^4.1.1"
+    istanbul-lib-coverage "^3.0.0"
+    source-map "^0.6.1"
+
+istanbul-reports@^3.0.2:
+  version "3.0.2"
+  resolved "https://registry.yarnpkg.com/istanbul-reports/-/istanbul-reports-3.0.2.tgz#d593210e5000683750cb09fc0644e4b6e27fd53b"
+  integrity sha512-9tZvz7AiR3PEDNGiV9vIouQ/EAcqMXFmkcA1CDFTwOB98OZVDL0PH9glHotf5Ugp6GCOTypfzGWI/OqjWNCRUw==
+  dependencies:
+    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==
+  dependencies:
+    "@jest/types" "^25.4.0"
+    execa "^3.2.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==
+  dependencies:
+    "@jest/core" "^25.4.0"
+    "@jest/test-result" "^25.4.0"
+    "@jest/types" "^25.4.0"
+    chalk "^3.0.0"
+    exit "^0.1.2"
+    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"
+    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==
+  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"
+    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"
+    micromatch "^4.0.2"
+    pretty-format "^25.4.0"
+    realpath-native "^2.0.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==
+  dependencies:
+    chalk "^3.0.0"
+    diff-sequences "^25.2.6"
+    jest-get-type "^25.2.6"
+    pretty-format "^25.4.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==
+  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==
+  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-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==
+  dependencies:
+    "@jest/types" "^25.4.0"
+    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"
+    micromatch "^4.0.2"
+    sane "^4.0.3"
+    walker "^1.0.7"
+    which "^2.0.2"
+  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==
+  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"
+    co "^4.6.0"
+    expect "^25.4.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"
+    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==
+  dependencies:
+    jest-get-type "^25.2.6"
+    pretty-format "^25.4.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==
+  dependencies:
+    chalk "^3.0.0"
+    jest-diff "^25.4.0"
+    jest-get-type "^25.2.6"
+    pretty-format "^25.4.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==
+  dependencies:
+    "@babel/code-frame" "^7.0.0"
+    "@jest/types" "^25.4.0"
+    "@types/stack-utils" "^1.0.1"
+    chalk "^3.0.0"
+    micromatch "^4.0.2"
+    slash "^3.0.0"
+    stack-utils "^1.0.1"
+
+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==
+  dependencies:
+    "@jest/types" "^25.4.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"
+    jest-pnp-resolver "^1.2.1"
+    read-pkg-up "^7.0.1"
+    realpath-native "^2.0.0"
+    resolve "^1.15.1"
+    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==
+  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"
+    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"
+    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"
+    "@types/yargs" "^15.0.0"
+    chalk "^3.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"
+    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-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==
+  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"
+    natural-compare "^1.4.0"
+    pretty-format "^25.4.0"
+    semver "^6.3.0"
 
-isexe@^2.0.0:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10"
-  integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=
+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==
+  dependencies:
+    "@jest/types" "^25.4.0"
+    chalk "^3.0.0"
+    is-ci "^2.0.0"
+    make-dir "^3.0.0"
 
-isobject@^2.0.0:
-  version "2.1.0"
-  resolved "https://registry.yarnpkg.com/isobject/-/isobject-2.1.0.tgz#f065561096a3f1da2ef46272f815c840d87e0c89"
-  integrity sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=
+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==
   dependencies:
-    isarray "1.0.0"
+    "@jest/types" "^25.4.0"
+    camelcase "^5.3.1"
+    chalk "^3.0.0"
+    jest-get-type "^25.2.6"
+    leven "^3.1.0"
+    pretty-format "^25.4.0"
 
-isobject@^3.0.0, isobject@^3.0.1:
-  version "3.0.1"
-  resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df"
-  integrity sha1-TkMekrEalzFjaqH5yNHMvP2reN8=
+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==
+  dependencies:
+    "@jest/test-result" "^25.4.0"
+    "@jest/types" "^25.4.0"
+    ansi-escapes "^4.2.1"
+    chalk "^3.0.0"
+    jest-util "^25.4.0"
+    string-length "^3.1.0"
 
-isstream@~0.1.2:
-  version "0.1.2"
-  resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a"
-  integrity sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=
+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==
+  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==
+  dependencies:
+    "@jest/core" "^25.4.0"
+    import-local "^3.0.2"
+    jest-cli "^25.4.0"
 
 js-cookie@^2.2.0:
   version "2.2.1"
@@ -2763,6 +4004,38 @@ 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"
+    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"
+    tough-cookie "^3.0.1"
+    w3c-hr-time "^1.0.1"
+    w3c-xmlserializer "^1.1.2"
+    webidl-conversions "^4.0.2"
+    whatwg-encoding "^1.0.5"
+    whatwg-mimetype "^2.3.0"
+    whatwg-url "^7.0.0"
+    ws "^7.0.0"
+    xml-name-validator "^3.0.0"
+
 jsesc@^2.5.1:
   version "2.5.2"
   resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4"
@@ -2798,6 +4071,13 @@ json-stringify-safe@~5.0.1:
   resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb"
   integrity sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=
 
+json5@2.x, json5@^2.1.2:
+  version "2.1.3"
+  resolved "https://registry.yarnpkg.com/json5/-/json5-2.1.3.tgz#c9b0f7fa9233bfe5807fe66fcf3a5617ed597d43"
+  integrity sha512-KXPvOm8K9IJKFM0bmdn8QXh7udDh1g/giieX0NLCaMnb4hEiVFqnop2ImTXCc5e0/oHz3LTqmHGtExn5hfMkOA==
+  dependencies:
+    minimist "^1.2.5"
+
 jsonfile@^4.0.0:
   version "4.0.0"
   resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-4.0.0.tgz#8771aae0799b64076b76640fca058f9c10e33ecb"
@@ -2861,6 +4141,11 @@ kind-of@^6.0.0, kind-of@^6.0.2:
   resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd"
   integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==
 
+kleur@^3.0.3:
+  version "3.0.3"
+  resolved "https://registry.yarnpkg.com/kleur/-/kleur-3.0.3.tgz#a79c9ecc86ee1ce3fa6206d1216c501f147fc07e"
+  integrity sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==
+
 lego-api@^1.0.7:
   version "1.0.8"
   resolved "https://registry.yarnpkg.com/lego-api/-/lego-api-1.0.8.tgz#5e26be726c5e11d540f89e7c6b1abf8c5834bd01"
@@ -2868,6 +4153,11 @@ lego-api@^1.0.7:
   dependencies:
     chain-able "^3.0.0"
 
+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:
   version "0.3.0"
   resolved "https://registry.yarnpkg.com/levn/-/levn-0.3.0.tgz#3b09924edf9f083c0490fdd4c0bc4421e04764ee"
@@ -2981,6 +4271,16 @@ lodash.get@^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=
+
+lodash.sortby@^4.7.0:
+  version "4.7.0"
+  resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438"
+  integrity sha1-7dFMgk4sycHgsKG0K7UhBRakJDg=
+
 lodash.zip@^4.2.0:
   version "4.2.0"
   resolved "https://registry.yarnpkg.com/lodash.zip/-/lodash.zip-4.2.0.tgz#ec6662e4896408ed4ab6c542a3990b72cc080020"
@@ -3014,6 +4314,13 @@ log-update@^2.3.0:
     cli-cursor "^2.0.0"
     wrap-ansi "^3.0.1"
 
+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==
+  dependencies:
+    "@sinonjs/commons" "^1.7.0"
+
 loose-envify@^1.2.0, loose-envify@^1.4.0:
   version "1.4.0"
   resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf"
@@ -3021,11 +4328,30 @@ loose-envify@^1.2.0, loose-envify@^1.4.0:
   dependencies:
     js-tokens "^3.0.0 || ^4.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"
+  integrity sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==
+  dependencies:
+    semver "^6.0.0"
+
+make-error@1.x:
+  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==
 
+makeerror@1.0.x:
+  version "1.0.11"
+  resolved "https://registry.yarnpkg.com/makeerror/-/makeerror-1.0.11.tgz#e01a5c9109f2af79660e4e8b9587790184f5a96c"
+  integrity sha1-4BpckQnyr3lmDk6LlYd5AYT1qWw=
+  dependencies:
+    tmpl "1.0.x"
+
 map-cache@^0.2.2:
   version "0.2.2"
   resolved "https://registry.yarnpkg.com/map-cache/-/map-cache-0.2.2.tgz#c32abd0bd6525d9b051645bb4f26ac5dc98a0dbf"
@@ -3094,6 +4420,14 @@ 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"
@@ -3113,7 +4447,7 @@ micromatch@^2.1.5:
     parse-glob "^3.0.4"
     regex-cache "^0.4.2"
 
-micromatch@^3.1.10:
+micromatch@^3.1.10, micromatch@^3.1.4:
   version "3.1.10"
   resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-3.1.10.tgz#70859bc95c9840952f359a068a3fc49f9ecfac23"
   integrity sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==
@@ -3132,14 +4466,6 @@ micromatch@^3.1.10:
     snapdragon "^0.8.1"
     to-regex "^3.0.2"
 
-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.43.0:
   version "1.43.0"
   resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.43.0.tgz#0a12e0502650e473d735535050e7c8f4eb4fae58"
@@ -3179,6 +4505,11 @@ minimist@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:
+  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"
@@ -3192,6 +4523,11 @@ mixin-deep@^1.2.0:
     for-in "^1.0.2"
     is-extendable "^1.0.1"
 
+mkdirp@1.x:
+  version "1.0.4"
+  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"
@@ -3286,6 +4622,32 @@ nice-try@^1.0.4:
   resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366"
   integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==
 
+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-int64@^0.4.0:
+  version "0.4.0"
+  resolved "https://registry.yarnpkg.com/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b"
+  integrity sha1-h6kGXNs1XTGC2PlM4RGIuCXGijs=
+
+node-modules-regexp@^1.0.0:
+  version "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==
+  dependencies:
+    growly "^1.3.0"
+    is-wsl "^2.1.1"
+    semver "^6.3.0"
+    shellwords "^0.1.1"
+    which "^1.3.1"
+
 normalize-package-data@^2.3.2, normalize-package-data@^2.5.0:
   version "2.5.0"
   resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.5.0.tgz#e66db1838b200c1dfc233225d12cb36520e234a8"
@@ -3296,7 +4658,7 @@ 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-path@^2.0.0, normalize-path@^2.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"
   integrity sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=
@@ -3308,6 +4670,13 @@ normalize-path@^3.0.0:
   resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65"
   integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==
 
+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"
+  integrity sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=
+  dependencies:
+    path-key "^2.0.0"
+
 npm-run-path@^4.0.0:
   version "4.0.1"
   resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-4.0.1.tgz#b7ecd1e5ed53da8e37a55e1c2269e0b97ed748ea"
@@ -3320,6 +4689,11 @@ number-is-nan@^1.0.0:
   resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d"
   integrity sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=
 
+nwsapi@^2.2.0:
+  version "2.2.0"
+  resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.2.0.tgz#204879a9e3d068ff2a55139c2c772780681a38b7"
+  integrity sha512-h2AatdwYH+JHiZpv7pt/gSX1XoRGb7L/qSIeuqA6GwYoF9w1vP1cw42TO0aI2pNyshRK5893hNSl+1//vHK7hQ==
+
 oauth-sign@~0.9.0:
   version "0.9.0"
   resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455"
@@ -3466,6 +4840,16 @@ os-tmpdir@~1.0.2:
   resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274"
   integrity sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=
 
+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"
+  integrity sha512-ZuRs1miPT4HrjFa+9fRfOFXxGJfORgelKV9f9nNOWw2gl6gVsRaVDOQP0+MI0G0wGKns1Yacsu0GjOFbTK0JFQ==
+
+p-finally@^1.0.0:
+  version "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"
@@ -3548,6 +4932,11 @@ parse-json@^5.0.0:
     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==
+
 parseurl@~1.3.3:
   version "1.3.3"
   resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4"
@@ -3573,7 +4962,7 @@ 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-key@^2.0.1:
+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"
   integrity sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=
@@ -3624,6 +5013,11 @@ 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:
+  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"
@@ -3634,6 +5028,13 @@ pify@^2.0.0:
   resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c"
   integrity sha1-7RQaasBDqEnqWISY59yosVMw6Qw=
 
+pirates@^4.0.1:
+  version "4.0.1"
+  resolved "https://registry.yarnpkg.com/pirates/-/pirates-4.0.1.tgz#643a92caf894566f91b2b986d2c66950a8e2fb87"
+  integrity sha512-WuNqLTbMI3tmfef2TKxlQmAiLHKtFhlsCZnPIpuv2Ow0RDVO8lfy1Opf4NUzlMXLjPl+Men7AuVdX6TA+s+uGA==
+  dependencies:
+    node-modules-regexp "^1.0.0"
+
 pkg-dir@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-2.0.0.tgz#f6d5d1109e19d63edf428e0bd57e12777615334b"
@@ -3655,6 +5056,11 @@ 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==
+
 posix-character-classes@^0.1.0:
   version "0.1.1"
   resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab"
@@ -3691,6 +5097,16 @@ prettier@^2.0.4:
   resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.0.4.tgz#2d1bae173e355996ee355ec9830a7a1ee05457ef"
   integrity sha512-SVJIQ51spzFDvh4fIbCLvciiDMCrRhlN3mbZvv/+ycjvmF5E73bKdGfU8QDLNmjYJf+lsGnDBC4UUnvTe5OO0w==
 
+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==
+  dependencies:
+    "@jest/types" "^25.4.0"
+    ansi-regex "^5.0.0"
+    ansi-styles "^4.0.0"
+    react-is "^16.12.0"
+
 pretty-time@^0.2.0:
   version "0.2.0"
   resolved "https://registry.yarnpkg.com/pretty-time/-/pretty-time-0.2.0.tgz#7a3bdec4049c620cd7c42b7f342b74d56e73d74e"
@@ -3714,6 +5130,14 @@ progress@^2.0.0:
   resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8"
   integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==
 
+prompts@^2.0.1:
+  version "2.3.2"
+  resolved "https://registry.yarnpkg.com/prompts/-/prompts-2.3.2.tgz#480572d89ecf39566d2bd3fe2c9fccb7c4c0b068"
+  integrity sha512-Q06uKs2CkNYVID0VqwfAl9mipo99zkBv/n2JtWY89Yxa3ZabWSrs0e2KTudKVa3peLUvYXMefDqIleLPVUBZMA==
+  dependencies:
+    kleur "^3.0.3"
+    sisteransi "^1.0.4"
+
 prop-types@^15.7.2:
   version "15.7.2"
   resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5"
@@ -3736,6 +5160,11 @@ psl@^1.1.24:
   resolved "https://registry.yarnpkg.com/psl/-/psl-1.7.0.tgz#f1c4c47a8ef97167dea5d6bbf4816d736e884a3c"
   integrity sha512-5NsSEDv8zY70ScRnOTn7bK7eanl2MvFrOrS/R6x+dBt5g1ghnj9Zv90kO8GwT8gxcu2ANyFprnFYB85IogIJOQ==
 
+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@^3.0.0:
   version "3.0.0"
   resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64"
@@ -3749,7 +5178,7 @@ punycode@^1.4.1:
   resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e"
   integrity sha1-wNWmOycYgArY4esPpSachN1BhF4=
 
-punycode@^2.1.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==
@@ -3788,6 +5217,11 @@ raw-body@2.4.0:
     iconv-lite "0.4.24"
     unpipe "1.0.0"
 
+react-is@^16.12.0:
+  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"
@@ -3859,6 +5293,11 @@ 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"
@@ -3968,6 +5407,22 @@ 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==
+  dependencies:
+    lodash "^4.17.15"
+
+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==
+  dependencies:
+    request-promise-core "1.1.3"
+    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"
@@ -3994,16 +5449,64 @@ request@^2.79.0:
     tunnel-agent "^0.6.0"
     uuid "^3.3.2"
 
+request@^2.88.0:
+  version "2.88.2"
+  resolved "https://registry.yarnpkg.com/request/-/request-2.88.2.tgz#d73c918731cb5a87da047e207234146f664d12b3"
+  integrity sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==
+  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.3"
+    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.5.0"
+    tunnel-agent "^0.6.0"
+    uuid "^3.3.2"
+
+require-directory@^2.1.1:
+  version "2.1.1"
+  resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42"
+  integrity sha1-jGStX9MNqxyXbiNE/+f3kqam30I=
+
+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"
+  integrity sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==
+
 reserved-words@^0.1.2:
   version "0.1.2"
   resolved "https://registry.yarnpkg.com/reserved-words/-/reserved-words-0.1.2.tgz#00a0940f98cd501aeaaac316411d9adc52b31ab1"
   integrity sha1-AKCUD5jNUBrqqsMWQR2a3FKzGrE=
 
+resolve-cwd@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-3.0.0.tgz#0f0075f1bb2544766cf73ba6a6e2adfebcb13f2d"
+  integrity sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==
+  dependencies:
+    resolve-from "^5.0.0"
+
 resolve-from@^4.0.0:
   version "4.0.0"
   resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6"
   integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==
 
+resolve-from@^5.0.0:
+  version "5.0.0"
+  resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-5.0.0.tgz#c35225843df8f776df21c57557bc087e9dfdfc69"
+  integrity sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==
+
 resolve-pathname@^3.0.0:
   version "3.0.0"
   resolved "https://registry.yarnpkg.com/resolve-pathname/-/resolve-pathname-3.0.0.tgz#99d02224d3cf263689becbb393bc560313025dcd"
@@ -4014,6 +5517,18 @@ 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"
@@ -4056,6 +5571,18 @@ rimraf@2.6.3:
   dependencies:
     glob "^7.1.3"
 
+rimraf@^3.0.0:
+  version "3.0.2"
+  resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a"
+  integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==
+  dependencies:
+    glob "^7.1.3"
+
+rsvp@^4.8.4:
+  version "4.8.5"
+  resolved "https://registry.yarnpkg.com/rsvp/-/rsvp-4.8.5.tgz#c8f155311d167f68f21e168df71ec5b083113734"
+  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"
@@ -4118,6 +5645,28 @@ safe-regex@^2.1.1:
   resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
   integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
 
+sane@^4.0.3:
+  version "4.1.0"
+  resolved "https://registry.yarnpkg.com/sane/-/sane-4.1.0.tgz#ed881fd922733a6c461bc189dc2b6c006f3ffded"
+  integrity sha512-hhbzAgTIX8O7SHfp2c8/kREfEn4qO/9q8C9beyY6+tvZ87EpoZ3i1RIEvp27YBswnNbY9mWd6paKVmKbAgLfZA==
+  dependencies:
+    "@cnakazawa/watch" "^1.0.3"
+    anymatch "^2.0.0"
+    capture-exit "^2.0.0"
+    exec-sh "^0.3.2"
+    execa "^1.0.0"
+    fb-watchman "^2.0.0"
+    micromatch "^3.1.4"
+    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==
+  dependencies:
+    xmlchars "^2.1.1"
+
 semver-compare@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/semver-compare/-/semver-compare-1.0.0.tgz#0dee216a1c941ab37e9efb1788f6afc5ff5537fc"
@@ -4128,12 +5677,12 @@ semver-regex@^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.5.0:
+"semver@2 || 3 || 4 || 5", semver@^5.4.1, semver@^5.5.0:
   version "5.7.1"
   resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7"
   integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==
 
-semver@^6.1.0, semver@^6.1.2, semver@^6.3.0:
+semver@6.x, semver@^6.0.0, semver@^6.1.0, semver@^6.1.2, 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==
@@ -4172,6 +5721,11 @@ serve-static@1.14.1:
     parseurl "~1.3.3"
     send "0.17.1"
 
+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=
+
 set-value@^2.0.0, set-value@^2.0.1:
   version "2.0.1"
   resolved "https://registry.yarnpkg.com/set-value/-/set-value-2.0.1.tgz#a18d40530e6f07de4228c7defe4227af8cad005b"
@@ -4211,6 +5765,11 @@ shebang-regex@^3.0.0:
   resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172"
   integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==
 
+shellwords@^0.1.1:
+  version "0.1.1"
+  resolved "https://registry.yarnpkg.com/shellwords/-/shellwords-0.1.1.tgz#d6b9181c1a48d397324c84871efbcfc73fc0654b"
+  integrity sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww==
+
 shorthash@0.0.2:
   version "0.0.2"
   resolved "https://registry.yarnpkg.com/shorthash/-/shorthash-0.0.2.tgz#59b268eecbde59038b30da202bcfbddeb2c4a4eb"
@@ -4224,11 +5783,21 @@ side-channel@^1.0.2:
     es-abstract "^1.17.0-next.1"
     object-inspect "^1.7.0"
 
+signal-exit@^3.0.0:
+  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"
+  integrity sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==
+
 slash@^3.0.0:
   version "3.0.0"
   resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634"
@@ -4382,6 +5951,11 @@ 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==
+
 static-extend@^0.1.1:
   version "0.1.2"
   resolved "https://registry.yarnpkg.com/static-extend/-/static-extend-0.1.2.tgz#60809c39cbff55337226fd5e0b520f341f1fb5c6"
@@ -4395,6 +5969,11 @@ static-extend@^0.1.1:
   resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c"
   integrity sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=
 
+stealthy-require@^1.1.1:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/stealthy-require/-/stealthy-require-1.1.1.tgz#35b09875b4ff49f26a777e509b3090a3226bf24b"
+  integrity sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks=
+
 stream-browserify@^2.0.1:
   version "2.0.2"
   resolved "https://registry.yarnpkg.com/stream-browserify/-/stream-browserify-2.0.2.tgz#87521d38a44aa7ee91ce1cd2a47df0cb49dd660b"
@@ -4408,6 +5987,14 @@ string-argv@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==
+  dependencies:
+    astral-regex "^1.0.0"
+    strip-ansi "^5.2.0"
+
 string-width@^1.0.1:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3"
@@ -4434,7 +6021,7 @@ string-width@^3.0.0:
     is-fullwidth-code-point "^2.0.0"
     strip-ansi "^5.1.0"
 
-string-width@^4.1.0:
+string-width@^4.1.0, string-width@^4.2.0:
   version "4.2.0"
   resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.0.tgz#952182c46cc7b2c313d1596e623992bd163b72b5"
   integrity sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==
@@ -4520,6 +6107,16 @@ strip-bom@^3.0.0:
   resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3"
   integrity sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=
 
+strip-bom@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-4.0.0.tgz#9c3505c1db45bcedca3d9cf7a16f5c5aa3901878"
+  integrity sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==
+
+strip-eof@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/strip-eof/-/strip-eof-1.0.0.tgz#bb43ff5598a6eb05d89b59fcd129c983313606bf"
+  integrity sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=
+
 strip-final-newline@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz#89b852fb2fcbe936f6f4b3187afb0a12c1ab58ad"
@@ -4542,18 +6139,31 @@ supports-color@^5.3.0, supports-color@^5.4.0:
   dependencies:
     has-flag "^3.0.0"
 
-supports-color@^7.1.0:
+supports-color@^7.0.0, supports-color@^7.1.0:
   version "7.1.0"
   resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.1.0.tgz#68e32591df73e25ad1c4b49108a2ec507962bfd1"
   integrity sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==
   dependencies:
     has-flag "^4.0.0"
 
+supports-hyperlinks@^2.0.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/supports-hyperlinks/-/supports-hyperlinks-2.1.0.tgz#f663df252af5f37c5d49bbd7eeefa9e0b9e59e47"
+  integrity sha512-zoE5/e+dnEijk6ASB6/qrK+oYdm2do1hjoLWrqUC/8WEIW1gbxFcKuBof7sW8ArN6e+AYvsE8HBGiVRWL/F5CA==
+  dependencies:
+    has-flag "^4.0.0"
+    supports-color "^7.0.0"
+
 symbol-observable@^1.1.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:
+  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==
+
 tabbable@^4.0.0:
   version "4.0.0"
   resolved "https://registry.yarnpkg.com/tabbable/-/tabbable-4.0.0.tgz#5bff1d1135df1482cf0f0206434f15eadbeb9261"
@@ -4569,6 +6179,14 @@ table@^5.2.3:
     slice-ansi "^2.1.0"
     string-width "^3.0.0"
 
+terminal-link@^2.0.0:
+  version "2.1.1"
+  resolved "https://registry.yarnpkg.com/terminal-link/-/terminal-link-2.1.1.tgz#14a64a27ab3c0df933ea546fba55f2d078edc994"
+  integrity sha512-un0FmiRUQNr5PJqy9kP7c40F5BOfpGlYTrxonDChEZB7pzZxRNp/bt+ymiy9/npwXya9KH99nJ/GXFIiUkYGFQ==
+  dependencies:
+    ansi-escapes "^4.2.1"
+    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"
@@ -4578,11 +6196,25 @@ terser@^4.6.11:
     source-map "~0.6.1"
     source-map-support "~0.5.12"
 
+test-exclude@^6.0.0:
+  version "6.0.0"
+  resolved "https://registry.yarnpkg.com/test-exclude/-/test-exclude-6.0.0.tgz#04a8698661d805ea6fa293b6cb9e63ac044ef15e"
+  integrity sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==
+  dependencies:
+    "@istanbuljs/schema" "^0.1.2"
+    glob "^7.1.4"
+    minimatch "^3.0.4"
+
 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=
 
+throat@^5.0.0:
+  version "5.0.0"
+  resolved "https://registry.yarnpkg.com/throat/-/throat-5.0.0.tgz#c5199235803aad18754a667d659b5e72ce16764b"
+  integrity sha512-fcwX4mndzpLQKBS1DVYhGAcYaYt7vsHNIvQV+WXMvnow5cgjPphq5CaayLaGsjRdSCKZFNGt7/GYAuXaNOiYCA==
+
 through@^2.3.6:
   version "2.3.8"
   resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5"
@@ -4617,6 +6249,11 @@ tmp@^0.0.33:
   dependencies:
     os-tmpdir "~1.0.2"
 
+tmpl@1.0.x:
+  version "1.0.4"
+  resolved "https://registry.yarnpkg.com/tmpl/-/tmpl-1.0.4.tgz#23640dd7b42d00433911140820e5cf440e521dd1"
+  integrity sha1-I2QN17QtAEM5ERQIIOXPRA5SHdE=
+
 to-fast-properties@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e"
@@ -4664,6 +6301,23 @@ toidentifier@1.0.0:
   resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.0.tgz#7e1be3470f1e77948bc43d94a3c8f4d7752ba553"
   integrity sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==
 
+tough-cookie@^2.3.3, tough-cookie@~2.5.0:
+  version "2.5.0"
+  resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.5.0.tgz#cd9fb2a0aa1d5a12b473bd9fb96fa3dcff65ade2"
+  integrity sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==
+  dependencies:
+    psl "^1.1.28"
+    punycode "^2.1.1"
+
+tough-cookie@^3.0.1:
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-3.0.1.tgz#9df4f57e739c26930a018184887f4adb7dca73b2"
+  integrity sha512-yQyJ0u4pZsv9D4clxO69OEjLWYw+jbgspjTue4lTQZLfV0c5l1VmK2y1JK8E9ahdpltPOaAThPcp5nKPUgSnsg==
+  dependencies:
+    ip-regex "^2.1.0"
+    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"
@@ -4672,11 +6326,35 @@ tough-cookie@~2.4.3:
     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=
+  dependencies:
+    punycode "^2.1.0"
+
 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==
+  dependencies:
+    bs-logger "0.x"
+    buffer-from "1.x"
+    fast-json-stable-stringify "2.x"
+    json5 "2.x"
+    lodash.memoize "4.x"
+    make-error "1.x"
+    micromatch "4.x"
+    mkdirp "1.x"
+    resolve "1.x"
+    semver "6.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"
@@ -4749,6 +6427,11 @@ type-check@~0.3.2:
   dependencies:
     prelude-ls "~1.1.2"
 
+type-detect@4.0.8:
+  version "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.6.0:
   version "0.6.0"
   resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.6.0.tgz#8d2a2370d3df886eb5c90ada1c5bf6188acf838b"
@@ -4767,6 +6450,13 @@ type-is@~1.6.17, type-is@~1.6.18:
     media-typer "0.3.0"
     mime-types "~2.1.24"
 
+typedarray-to-buffer@^3.1.5:
+  version "3.1.5"
+  resolved "https://registry.yarnpkg.com/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz#a97ee7a9ff42691b9f783ff1bc5112fe3fca9080"
+  integrity sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==
+  dependencies:
+    is-typedarray "^1.0.0"
+
 typescript@^3.8.3:
   version "3.8.3"
   resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.8.3.tgz#409eb8544ea0335711205869ec458ab109ee1061"
@@ -4875,6 +6565,15 @@ v8-compile-cache@^2.0.3:
   resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.1.0.tgz#e14de37b31a6d194f5690d67efc4e7f6fc6ab30e"
   integrity sha512-usZBT3PW+LOjM25wbqIlZwPeJV+3OSz3M1k1Ws8snlW39dZyYL9lOGC5FgPVHfk0jKmjiDV8Z0mIbVQPiwFs7g==
 
+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==
+  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:
   version "3.0.4"
   resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz#fc91f6b9c7ba15c857f4cb2c5defeec39d4f410a"
@@ -4907,6 +6606,29 @@ 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:
+  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==
+  dependencies:
+    domexception "^1.0.1"
+    webidl-conversions "^4.0.2"
+    xml-name-validator "^3.0.0"
+
+walker@^1.0.7, walker@~1.0.5:
+  version "1.0.7"
+  resolved "https://registry.yarnpkg.com/walker/-/walker-1.0.7.tgz#2f7f9b8fd10d677262b18a884e28d19618e028fb"
+  integrity sha1-L3+bj9ENZ3JisYqITijRlhjgKPs=
+  dependencies:
+    makeerror "1.0.x"
+
 watch@^1.0.1:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/watch/-/watch-1.0.2.tgz#340a717bde765726fa0aa07d721e0147a551df0c"
@@ -4915,19 +6637,50 @@ 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==
+
+whatwg-encoding@^1.0.1, 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:
+  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==
+  dependencies:
+    lodash.sortby "^4.7.0"
+    tr46 "^1.0.1"
+    webidl-conversions "^4.0.2"
+
+which-module@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a"
+  integrity sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=
+
 which-pm-runs@^1.0.0:
   version "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.2.9, which@^1.3.1:
   version "1.3.1"
   resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a"
   integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==
   dependencies:
     isexe "^2.0.0"
 
-which@^2.0.1:
+which@^2.0.1, which@^2.0.2:
   version "2.0.2"
   resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1"
   integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==
@@ -4947,11 +6700,30 @@ wrap-ansi@^3.0.1:
     string-width "^2.1.1"
     strip-ansi "^4.0.0"
 
+wrap-ansi@^6.2.0:
+  version "6.2.0"
+  resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-6.2.0.tgz#e9393ba07102e6c91a3b221478f0257cd2856e53"
+  integrity sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==
+  dependencies:
+    ansi-styles "^4.0.0"
+    string-width "^4.1.0"
+    strip-ansi "^6.0.0"
+
 wrappy@1:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
   integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=
 
+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"
+  integrity sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==
+  dependencies:
+    imurmurhash "^0.1.4"
+    is-typedarray "^1.0.0"
+    signal-exit "^3.0.2"
+    typedarray-to-buffer "^3.1.5"
+
 write@1.0.3:
   version "1.0.3"
   resolved "https://registry.yarnpkg.com/write/-/write-1.0.3.tgz#0800e14523b923a387e415123c865616aae0f5c3"
@@ -4967,11 +6739,21 @@ ws@^1.1.1:
     options ">=0.0.5"
     ultron "1.0.x"
 
-ws@^7.2.3:
+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==
 
+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:
+  version "2.2.0"
+  resolved "https://registry.yarnpkg.com/xmlchars/-/xmlchars-2.2.0.tgz#060fe1bcb7f9c76fe2a17db86a9bc3ab894210cb"
+  integrity sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==
+
 xregexp@^4.3.0:
   version "4.3.0"
   resolved "https://registry.yarnpkg.com/xregexp/-/xregexp-4.3.0.tgz#7e92e73d9174a99a59743f67a4ce879a04b5ae50"
@@ -4984,6 +6766,11 @@ xtend@^4.0.1:
   resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54"
   integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==
 
+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==
+
 yaml@^1.7.2:
   version "1.7.2"
   resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.7.2.tgz#f26aabf738590ab61efaca502358e48dc9f348b2"
@@ -4991,6 +6778,31 @@ yaml@^1.7.2:
   dependencies:
     "@babel/runtime" "^7.6.3"
 
+yargs-parser@18.x, yargs-parser@^18.1.1:
+  version "18.1.3"
+  resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-18.1.3.tgz#be68c4975c6b2abf469236b0c870362fab09a7b0"
+  integrity sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==
+  dependencies:
+    camelcase "^5.0.0"
+    decamelize "^1.2.0"
+
+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==
+  dependencies:
+    cliui "^6.0.0"
+    decamelize "^1.2.0"
+    find-up "^4.1.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 "^4.2.0"
+    which-module "^2.0.0"
+    y18n "^4.0.0"
+    yargs-parser "^18.1.1"
+
 yn@3.1.1:
   version "3.1.1"
   resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50"