From fdef4b6e6a5f3a47effc5c388d3333370c688db1 Mon Sep 17 00:00:00 2001 From: silverpill Date: Fri, 9 Apr 2021 00:22:17 +0000 Subject: [PATCH] Initial commit --- .editorconfig | 16 + .env | 2 + .gitignore | 4 + Cargo.lock | 3883 +++++++++++++++++++++++++++ Cargo.toml | 77 + LICENSE | 661 +++++ README.md | 114 + config.yaml.example | 29 + contracts/Collectible.json | 433 +++ contracts/Minter.json | 151 ++ docker-compose.yaml | 17 + migrations/V0001__create_tables.sql | 62 + migrations/schema.sql | 62 + src/activitypub/activity.rs | 263 ++ src/activitypub/actor.rs | 139 + src/activitypub/constants.rs | 3 + src/activitypub/deliverer.rs | 97 + src/activitypub/fetcher.rs | 120 + src/activitypub/mod.rs | 8 + src/activitypub/receiver.rs | 210 ++ src/activitypub/views.rs | 129 + src/activitypub/vocabulary.rs | 14 + src/bin/mitractl.rs | 87 + src/config.rs | 159 ++ src/database/migrate.rs | 23 + src/database/mod.rs | 26 + src/errors.rs | 100 + src/ethereum/api.rs | 10 + src/ethereum/mod.rs | 3 + src/ethereum/nft.rs | 232 ++ src/ethereum/utils.rs | 59 + src/http_signatures/create.rs | 96 + src/http_signatures/mod.rs | 2 + src/http_signatures/verify.rs | 197 ++ src/ipfs/mod.rs | 2 + src/ipfs/store.rs | 26 + src/ipfs/utils.rs | 20 + src/lib.rs | 14 + src/logger.rs | 17 + src/main.rs | 101 + src/mastodon_api/accounts/mod.rs | 2 + src/mastodon_api/accounts/types.rs | 118 + src/mastodon_api/accounts/views.rs | 207 ++ src/mastodon_api/directory/mod.rs | 1 + src/mastodon_api/directory/views.rs | 24 + src/mastodon_api/instance/mod.rs | 2 + src/mastodon_api/instance/types.rs | 40 + src/mastodon_api/instance/views.rs | 13 + src/mastodon_api/media/mod.rs | 2 + src/mastodon_api/media/types.rs | 42 + src/mastodon_api/media/views.rs | 44 + src/mastodon_api/mod.rs | 8 + src/mastodon_api/search/mod.rs | 3 + src/mastodon_api/search/queries.rs | 47 + src/mastodon_api/search/types.rs | 9 + src/mastodon_api/search/views.rs | 33 + src/mastodon_api/statuses/mod.rs | 2 + src/mastodon_api/statuses/types.rs | 61 + src/mastodon_api/statuses/views.rs | 159 ++ src/mastodon_api/timelines/mod.rs | 1 + src/mastodon_api/timelines/views.rs | 24 + src/mastodon_api/users/auth.rs | 25 + src/mastodon_api/users/mod.rs | 3 + src/mastodon_api/users/types.rs | 24 + src/mastodon_api/users/views.rs | 105 + src/models/attachments/mod.rs | 2 + src/models/attachments/queries.rs | 24 + src/models/attachments/types.rs | 34 + src/models/mod.rs | 5 + src/models/posts/mod.rs | 2 + src/models/posts/queries.rs | 207 ++ src/models/posts/types.rs | 102 + src/models/profiles/mod.rs | 2 + src/models/profiles/queries.rs | 272 ++ src/models/profiles/types.rs | 51 + src/models/relationships/mod.rs | 2 + src/models/relationships/queries.rs | 230 ++ src/models/relationships/types.rs | 78 + src/models/users/mod.rs | 2 + src/models/users/queries.rs | 212 ++ src/models/users/types.rs | 72 + src/nodeinfo/mod.rs | 2 + src/nodeinfo/types.rs | 68 + src/nodeinfo/views.rs | 39 + src/scheduler.rs | 24 + src/utils/crypto.rs | 115 + src/utils/files.rs | 68 + src/utils/html.rs | 27 + src/utils/mod.rs | 3 + src/webfinger/mod.rs | 2 + src/webfinger/types.rs | 27 + src/webfinger/views.rs | 66 + 92 files changed, 10405 insertions(+) create mode 100644 .editorconfig create mode 100644 .env create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 LICENSE create mode 100644 README.md create mode 100644 config.yaml.example create mode 100644 contracts/Collectible.json create mode 100644 contracts/Minter.json create mode 100644 docker-compose.yaml create mode 100644 migrations/V0001__create_tables.sql create mode 100644 migrations/schema.sql create mode 100644 src/activitypub/activity.rs create mode 100644 src/activitypub/actor.rs create mode 100644 src/activitypub/constants.rs create mode 100644 src/activitypub/deliverer.rs create mode 100644 src/activitypub/fetcher.rs create mode 100644 src/activitypub/mod.rs create mode 100644 src/activitypub/receiver.rs create mode 100644 src/activitypub/views.rs create mode 100644 src/activitypub/vocabulary.rs create mode 100644 src/bin/mitractl.rs create mode 100644 src/config.rs create mode 100644 src/database/migrate.rs create mode 100644 src/database/mod.rs create mode 100644 src/errors.rs create mode 100644 src/ethereum/api.rs create mode 100644 src/ethereum/mod.rs create mode 100644 src/ethereum/nft.rs create mode 100644 src/ethereum/utils.rs create mode 100644 src/http_signatures/create.rs create mode 100644 src/http_signatures/mod.rs create mode 100644 src/http_signatures/verify.rs create mode 100644 src/ipfs/mod.rs create mode 100644 src/ipfs/store.rs create mode 100644 src/ipfs/utils.rs create mode 100644 src/lib.rs create mode 100644 src/logger.rs create mode 100644 src/main.rs create mode 100644 src/mastodon_api/accounts/mod.rs create mode 100644 src/mastodon_api/accounts/types.rs create mode 100644 src/mastodon_api/accounts/views.rs create mode 100644 src/mastodon_api/directory/mod.rs create mode 100644 src/mastodon_api/directory/views.rs create mode 100644 src/mastodon_api/instance/mod.rs create mode 100644 src/mastodon_api/instance/types.rs create mode 100644 src/mastodon_api/instance/views.rs create mode 100644 src/mastodon_api/media/mod.rs create mode 100644 src/mastodon_api/media/types.rs create mode 100644 src/mastodon_api/media/views.rs create mode 100644 src/mastodon_api/mod.rs create mode 100644 src/mastodon_api/search/mod.rs create mode 100644 src/mastodon_api/search/queries.rs create mode 100644 src/mastodon_api/search/types.rs create mode 100644 src/mastodon_api/search/views.rs create mode 100644 src/mastodon_api/statuses/mod.rs create mode 100644 src/mastodon_api/statuses/types.rs create mode 100644 src/mastodon_api/statuses/views.rs create mode 100644 src/mastodon_api/timelines/mod.rs create mode 100644 src/mastodon_api/timelines/views.rs create mode 100644 src/mastodon_api/users/auth.rs create mode 100644 src/mastodon_api/users/mod.rs create mode 100644 src/mastodon_api/users/types.rs create mode 100644 src/mastodon_api/users/views.rs create mode 100644 src/models/attachments/mod.rs create mode 100644 src/models/attachments/queries.rs create mode 100644 src/models/attachments/types.rs create mode 100644 src/models/mod.rs create mode 100644 src/models/posts/mod.rs create mode 100644 src/models/posts/queries.rs create mode 100644 src/models/posts/types.rs create mode 100644 src/models/profiles/mod.rs create mode 100644 src/models/profiles/queries.rs create mode 100644 src/models/profiles/types.rs create mode 100644 src/models/relationships/mod.rs create mode 100644 src/models/relationships/queries.rs create mode 100644 src/models/relationships/types.rs create mode 100644 src/models/users/mod.rs create mode 100644 src/models/users/queries.rs create mode 100644 src/models/users/types.rs create mode 100644 src/nodeinfo/mod.rs create mode 100644 src/nodeinfo/types.rs create mode 100644 src/nodeinfo/views.rs create mode 100644 src/scheduler.rs create mode 100644 src/utils/crypto.rs create mode 100644 src/utils/files.rs create mode 100644 src/utils/html.rs create mode 100644 src/utils/mod.rs create mode 100644 src/webfinger/mod.rs create mode 100644 src/webfinger/types.rs create mode 100644 src/webfinger/views.rs diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..ab1f8b8 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,16 @@ +[*] +charset = utf-8 +indent_style = space +insert_final_newline = true +trim_trailing_whitespace = true + +[*.rs] +indent_size = 4 + +[*.yaml] +indent_size = 2 + +[*.md] +indent_size = 2 +max_line_length = off +trim_trailing_whitespace = false diff --git a/.env b/.env new file mode 100644 index 0000000..93ce2c9 --- /dev/null +++ b/.env @@ -0,0 +1,2 @@ +# Allowed values: development, production +ENVIRONMENT=development diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d6bfdaa --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +.env.local +config.yaml +/files +/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..76320b1 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,3883 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "actix-codec" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78d1833b3838dbe990df0f1f87baf640cf6146e898166afe401839d1b001e570" +dependencies = [ + "bitflags", + "bytes 0.5.6", + "futures-core", + "futures-sink", + "log", + "pin-project 0.4.28", + "tokio 0.2.25", + "tokio-util", +] + +[[package]] +name = "actix-connect" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "177837a10863f15ba8d3ae3ec12fac1099099529ed20083a27fdfe247381d0dc" +dependencies = [ + "actix-codec", + "actix-rt", + "actix-service", + "actix-utils", + "derive_more", + "either", + "futures-util", + "http", + "log", + "trust-dns-proto", + "trust-dns-resolver", +] + +[[package]] +name = "actix-cors" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36b133d8026a9f209a9aeeeacd028e7451bcca975f592881b305d37983f303d7" +dependencies = [ + "actix-web", + "derive_more", + "futures-util", + "log", + "once_cell", + "tinyvec", +] + +[[package]] +name = "actix-files" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c51e8a9146c12fce92a6e4c24b8c4d9b05268130bfd8d61bc587e822c32ce689" +dependencies = [ + "actix-service", + "actix-web", + "bitflags", + "bytes 0.5.6", + "derive_more", + "futures-core", + "futures-util", + "log", + "mime", + "mime_guess", + "percent-encoding 2.1.0", + "v_htmlescape", +] + +[[package]] +name = "actix-http" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "452299e87817ae5673910e53c243484ca38be3828db819b6011736fc6982e874" +dependencies = [ + "actix-codec", + "actix-connect", + "actix-rt", + "actix-service", + "actix-threadpool", + "actix-utils", + "base64 0.13.0", + "bitflags", + "brotli2", + "bytes 0.5.6", + "cookie", + "copyless", + "derive_more", + "either", + "encoding_rs", + "flate2", + "futures-channel", + "futures-core", + "futures-util", + "fxhash", + "h2", + "http", + "httparse", + "indexmap", + "itoa", + "language-tags", + "lazy_static", + "log", + "mime", + "percent-encoding 2.1.0", + "pin-project 1.0.6", + "rand 0.7.3", + "regex", + "serde", + "serde_json", + "serde_urlencoded", + "sha-1", + "slab", + "time 0.2.26", +] + +[[package]] +name = "actix-macros" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ca8ce00b267af8ccebbd647de0d61e0674b6e61185cc7a592ff88772bed655" +dependencies = [ + "quote", + "syn", +] + +[[package]] +name = "actix-router" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ad299af73649e1fc893e333ccf86f377751eb95ff875d095131574c6f43452c" +dependencies = [ + "bytestring", + "http", + "log", + "regex", + "serde", +] + +[[package]] +name = "actix-rt" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "143fcc2912e0d1de2bcf4e2f720d2a60c28652ab4179685a1ee159e0fb3db227" +dependencies = [ + "actix-macros", + "actix-threadpool", + "copyless", + "futures-channel", + "futures-util", + "smallvec", + "tokio 0.2.25", +] + +[[package]] +name = "actix-server" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45407e6e672ca24784baa667c5d32ef109ccdd8d5e0b5ebb9ef8a67f4dfb708e" +dependencies = [ + "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.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0052435d581b5be835d11f4eb3bce417c8af18d87ddf8ace99f8e67e595882bb" +dependencies = [ + "futures-util", + "pin-project 0.4.28", +] + +[[package]] +name = "actix-session" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "559b815f2b3ad84f8a17256069d7df16c3ee8069635c86758729521d62ca891d" +dependencies = [ + "actix-service", + "actix-web", + "derive_more", + "futures-util", + "serde", + "serde_json", + "time 0.2.26", +] + +[[package]] +name = "actix-testing" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47239ca38799ab74ee6a8a94d1ce857014b2ac36f242f70f3f75a66f691e791c" +dependencies = [ + "actix-macros", + "actix-rt", + "actix-server", + "actix-service", + "log", + "socket2", +] + +[[package]] +name = "actix-threadpool" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d209f04d002854b9afd3743032a27b066158817965bf5d036824d19ac2cc0e30" +dependencies = [ + "derive_more", + "futures-channel", + "lazy_static", + "log", + "num_cpus", + "parking_lot", + "threadpool", +] + +[[package]] +name = "actix-tls" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24789b7d7361cf5503a504ebe1c10806896f61e96eca9a7350e23001aca715fb" +dependencies = [ + "actix-codec", + "actix-service", + "actix-utils", + "futures-util", +] + +[[package]] +name = "actix-utils" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e9022dec56632d1d7979e59af14f0597a28a830a9c1c7fec8b2327eb9f16b5a" +dependencies = [ + "actix-codec", + "actix-rt", + "actix-service", + "bitflags", + "bytes 0.5.6", + "either", + "futures-channel", + "futures-sink", + "futures-util", + "log", + "pin-project 0.4.28", + "slab", +] + +[[package]] +name = "actix-web" +version = "3.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e641d4a172e7faa0862241a20ff4f1f5ab0ab7c279f00c2d4587b77483477b86" +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 0.5.6", + "derive_more", + "encoding_rs", + "futures-channel", + "futures-core", + "futures-util", + "fxhash", + "log", + "mime", + "pin-project 1.0.6", + "regex", + "serde", + "serde_json", + "serde_urlencoded", + "socket2", + "time 0.2.26", + "tinyvec", + "url 2.2.2", +] + +[[package]] +name = "actix-web-codegen" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad26f77093333e0e7c6ffe54ebe3582d908a104e448723eec6d43d08b07143fb" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "aead" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fc95d1bdb8e6666b2b217308eeeb09f2d6728d104be3e31916cc74d15420331" +dependencies = [ + "generic-array", +] + +[[package]] +name = "aes" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "884391ef1066acaa41e766ba8f596341b96e93ce34f9a43e7d24bf0a0eaf0561" +dependencies = [ + "aes-soft", + "aesni", + "cipher", +] + +[[package]] +name = "aes-gcm" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5278b5fabbb9bd46e24aa69b2fdea62c99088e0a950a9be40e3e0101298f88da" +dependencies = [ + "aead", + "aes", + "cipher", + "ctr", + "ghash", + "subtle", +] + +[[package]] +name = "aes-soft" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be14c7498ea50828a38d0e24a765ed2effe92a705885b57d029cd67d45744072" +dependencies = [ + "cipher", + "opaque-debug", +] + +[[package]] +name = "aesni" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea2e11f5e94c2f7d386164cc2aa1f97823fed6f259e486940a71c174dd01b0ce" +dependencies = [ + "cipher", + "opaque-debug", +] + +[[package]] +name = "aho-corasick" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f" +dependencies = [ + "memchr", +] + +[[package]] +name = "ammonia" +version = "3.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e445c26125ff80316eaea16e812d717b147b82a68682bd4730f74d4845c8b35" +dependencies = [ + "html5ever", + "lazy_static", + "maplit", + "markup5ever_rcdom", + "matches", + "tendril", + "url 2.2.2", +] + +[[package]] +name = "anyhow" +version = "1.0.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "595d3cfa7a60d4555cb5067b99f07142a08ea778de5cf993f7b75c7d8fabc486" + +[[package]] +name = "arrayref" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4c527152e37cf757a3f78aae5a06fbeefdb07ccc535c980a3208ee3060dd544" + +[[package]] +name = "arrayvec" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" + +[[package]] +name = "async-trait" +version = "0.1.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b98e84bbb4cbcdd97da190ba0c58a1bb0de2c1fdf67d159e192ed766aeca722" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[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.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" + +[[package]] +name = "awc" +version = "2.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b381e490e7b0cfc37ebc54079b0413d8093ef43d14a4e4747083f7fa47a9e691" +dependencies = [ + "actix-codec", + "actix-http", + "actix-rt", + "actix-service", + "base64 0.13.0", + "bytes 0.5.6", + "cfg-if 1.0.0", + "derive_more", + "futures-core", + "log", + "mime", + "percent-encoding 2.1.0", + "rand 0.7.3", + "serde", + "serde_json", + "serde_urlencoded", +] + +[[package]] +name = "base-x" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4521f3e3d031370679b3b140beb36dfe4801b09ac77e30c61941f97df3ef28b" + +[[package]] +name = "base64" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b41b7ea54a0c9d92199de89e20e58d49f02f8e699814ef3fdf266f6f748d15c7" + +[[package]] +name = "base64" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" + +[[package]] +name = "base64ct" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0d27fb6b6f1e43147af148af49d49329413ba781aa0d5e10979831c210173b5" + +[[package]] +name = "bitflags" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" + +[[package]] +name = "bitvec" +version = "0.17.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41262f11d771fd4a61aa3ce019fca363b4b6c282fca9da2a31186d3965a47a5c" +dependencies = [ + "either", + "radium", +] + +[[package]] +name = "blake2b_simd" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afa748e348ad3be8263be728124b24a24f268266f6f5d58af9d75f6a40b5c587" +dependencies = [ + "arrayref", + "arrayvec", + "constant_time_eq", +] + +[[package]] +name = "block-buffer" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" +dependencies = [ + "block-padding", + "generic-array", +] + +[[package]] +name = "block-padding" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d696c370c750c948ada61c69a0ee2cbbb9c50b1019ddb86d9317157a99c2cae" + +[[package]] +name = "brotli-sys" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4445dea95f4c2b41cde57cc9fee236ae4dbae88d8fcbdb4750fc1bb5d86aaecd" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "brotli2" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cb036c3eade309815c15ddbacec5b22c4d1f3983a774ab2eac2e3e9ea85568e" +dependencies = [ + "brotli-sys", + "libc", +] + +[[package]] +name = "buf-min" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa17aa1cf56bdd6bb30518767d00e58019d326f3f05d8c3e0730b549d332ea83" +dependencies = [ + "bytes 0.5.6", +] + +[[package]] +name = "bumpalo" +version = "3.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63396b8a4b9de3f4fdfb320ab6080762242f66a8ef174c49d8e19b674db4cdbe" + +[[package]] +name = "byte-slice-cast" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0a5e3906bcbf133e33c1d4d95afc664ad37fbdb9f6568d8043e7ea8c27d93d3" + +[[package]] +name = "byteorder" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" + +[[package]] +name = "bytes" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e4cec68f03f32e44924783795810fa50a7035d8c8ebe78580ad7e6c703fba38" + +[[package]] +name = "bytes" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b700ce4376041dcd0a327fd0097c41095743c4c8af8887265942faf1100bd040" + +[[package]] +name = "bytestring" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90706ba19e97b90786e19dc0d5e2abd80008d99d4c0c5d1ad0b5e72cec7c494d" +dependencies = [ + "bytes 1.0.1", +] + +[[package]] +name = "cc" +version = "1.0.67" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3c69b077ad434294d3ce9f1f6143a2a4b89a8a2d54ef813d85003a4fd1137fd" + +[[package]] +name = "cfg-if" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "chrono" +version = "0.4.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73" +dependencies = [ + "libc", + "num-integer", + "num-traits", + "serde", + "time 0.1.44", + "winapi 0.3.9", +] + +[[package]] +name = "cipher" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12f8e7987cbd042a63249497f41aed09f8e65add917ea6566effbc56578d6801" +dependencies = [ + "generic-array", +] + +[[package]] +name = "clap" +version = "3.0.0-beta.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bd1061998a501ee7d4b6d449020df3266ca3124b941ec56cf2005c3779ca142" +dependencies = [ + "bitflags", + "clap_derive", + "indexmap", + "lazy_static", + "os_str_bytes", + "textwrap", + "unicode-width", + "vec_map", +] + +[[package]] +name = "clap_derive" +version = "3.0.0-beta.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "370f715b81112975b1b69db93e0b56ea4cd4e5002ac43b2da8474106a54096a1" +dependencies = [ + "heck", + "proc-macro-error", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "cloudabi" +version = "0.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddfc5b9aa5d4507acaf872de71051dfd0e309860e88966e1051e462a077aac4f" +dependencies = [ + "bitflags", +] + +[[package]] +name = "config" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19b076e143e1d9538dde65da30f8481c2a6c44040edb8e02b9bf1351edb92ce3" +dependencies = [ + "lazy_static", + "nom 5.1.2", + "serde", +] + +[[package]] +name = "const-oid" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c32f031ea41b4291d695026c023b95d59db2d8a2c7640800ed56bc8f510f22" + +[[package]] +name = "const_fn" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "076a6803b0dacd6a88cfe64deba628b01533ff5ef265687e6938280c1afd0a28" + +[[package]] +name = "constant_time_eq" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" + +[[package]] +name = "convert_case" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" + +[[package]] +name = "cookie" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03a5d7b21829bc7b4bf4754a978a241ae54ea55a40f92bb20216e54096f4b951" +dependencies = [ + "aes-gcm", + "base64 0.13.0", + "hkdf", + "hmac 0.10.1", + "percent-encoding 2.1.0", + "rand 0.8.3", + "sha2", + "time 0.2.26", + "version_check 0.9.3", +] + +[[package]] +name = "copyless" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2df960f5d869b2dd8532793fde43eb5427cceb126c929747a26823ab0eeb536" + +[[package]] +name = "core-foundation" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a89e2ae426ea83155dccf10c0fa6b1463ef6d5fcb44cee0b224a408fa640a62" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea221b5284a47e40033bf9b66f35f984ec0ea2931eb03505246cd27a963f981b" + +[[package]] +name = "cpufeatures" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66c99696f6c9dd7f35d486b9d04d7e6e202aa3e8c40d553f2fdf5e7e0c6a71ef" +dependencies = [ + "libc", +] + +[[package]] +name = "cpuid-bool" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8aebca1129a03dc6dc2b127edd729435bbc4a37e1d5f4d7513165089ceb02634" + +[[package]] +name = "cpuid-bool" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcb25d077389e53838a8158c8e99174c5a9d902dee4904320db714f3c653ffba" + +[[package]] +name = "crc32fast" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81156fece84ab6a9f2afdb109ce3ae577e42b1228441eded99bd77f627953b1a" +dependencies = [ + "cfg-if 1.0.0", +] + +[[package]] +name = "crossbeam-queue" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "774ba60a54c213d409d5353bda12d49cd68d14e45036a285234c8d6f91f92570" +dependencies = [ + "cfg-if 0.1.10", + "crossbeam-utils 0.7.2", + "maybe-uninit", +] + +[[package]] +name = "crossbeam-queue" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f6cb3c7f5b8e51bc3ebb73a2327ad4abdbd119dc13223f14f961d2f38486756" +dependencies = [ + "cfg-if 1.0.0", + "crossbeam-utils 0.8.3", +] + +[[package]] +name = "crossbeam-utils" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3c7c73a2d1e9fc0886a08b93e98eb643461230d5f1925e4036204d5f2e261a8" +dependencies = [ + "autocfg 1.0.1", + "cfg-if 0.1.10", + "lazy_static", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7e9d99fa91428effe99c5c6d4634cdeba32b8cf784fc428a2a687f61a952c49" +dependencies = [ + "autocfg 1.0.1", + "cfg-if 1.0.0", + "lazy_static", +] + +[[package]] +name = "crunchy" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" + +[[package]] +name = "crypto-bigint" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b32a398eb1ccfbe7e4f452bc749c44d38dd732e9a253f19da224c416f00ee7f4" +dependencies = [ + "generic-array", + "rand_core 0.6.2", + "subtle", +] + +[[package]] +name = "crypto-mac" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58bcd97a54c7ca5ce2f6eb16f6bede5b0ab5f0055fedc17d2f0b4466e21671ca" +dependencies = [ + "generic-array", + "subtle", +] + +[[package]] +name = "crypto-mac" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4857fd85a0c34b3c3297875b747c1e02e06b6a0ea32dd892d8192b9ce0813ea6" +dependencies = [ + "generic-array", + "subtle", +] + +[[package]] +name = "ctr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb4a30d54f7443bf3d6191dcd486aca19e67cb3c49fa7a06a319966346707e7f" +dependencies = [ + "cipher", +] + +[[package]] +name = "deadpool" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4aaff9a7a1de9893f4004fa08527b31cb2ae4121c44e053cf53f29203c73bd23" +dependencies = [ + "async-trait", + "crossbeam-queue 0.2.3", + "num_cpus", + "tokio 0.2.25", +] + +[[package]] +name = "deadpool" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d126179d86aee4556e54f5f3c6bf6d9884e7cc52cef82f77ee6f90a7747616d" +dependencies = [ + "async-trait", + "config", + "crossbeam-queue 0.3.1", + "num_cpus", + "serde", + "tokio 1.5.0", +] + +[[package]] +name = "deadpool-postgres" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "faad41e7f93dd682108c72aec029e5bc6238e7df64c9d84832525d4033d2e726" +dependencies = [ + "async-trait", + "deadpool 0.5.2", + "futures", + "log", + "tokio 0.2.25", + "tokio-postgres", +] + +[[package]] +name = "der" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49f215f706081a44cb702c71c39a52c05da637822e9c1645a50b7202689e982d" +dependencies = [ + "const-oid", + "crypto-bigint", +] + +[[package]] +name = "derive_more" +version = "0.99.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f82b1b72f1263f214c0f823371768776c4f5841b942c9883aa8e5ec584fd0ba6" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "digest" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" +dependencies = [ + "generic-array", +] + +[[package]] +name = "discard" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "212d0f5754cb6769937f4501cc0e67f4f4483c8d2c3e1e922ee9edbe4ab4c7c0" + +[[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.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56899898ce76aaf4a0f24d914c97ea6ed976d42fec6ad33fcbb0a1103e07b2b0" + +[[package]] +name = "either" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" + +[[package]] +name = "encoding_rs" +version = "0.8.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80df024fbc5ac80f87dfef0d9f5209a252f2a497f7f42944cff24d8253cac065" +dependencies = [ + "cfg-if 1.0.0", +] + +[[package]] +name = "enum-as-inner" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c5f0096a91d210159eceb2ff5e1c4da18388a170e1e3ce948aac9c8fdbbf595" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "env_logger" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a19187fea3ac7e84da7dacf48de0c45d63c6a76f9490dae389aead16c243fce3" +dependencies = [ + "log", +] + +[[package]] +name = "ethabi" +version = "13.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d4e679d6864bc26210feb5cf044e245741cd9d7701b35c00440a6e84d61399" +dependencies = [ + "anyhow", + "ethereum-types", + "hex", + "serde", + "serde_json", + "sha3", + "thiserror", + "uint", +] + +[[package]] +name = "ethbloom" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22a621dcebea74f2a6f2002d0a885c81ccf6cbdf86760183316a7722b5707ca4" +dependencies = [ + "crunchy", + "fixed-hash", + "impl-rlp", + "impl-serde", + "tiny-keccak", +] + +[[package]] +name = "ethereum-types" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05dc5f0df4915fa6dff7f975a8366ecfaaa8959c74235469495153e7bb1b280e" +dependencies = [ + "ethbloom", + "fixed-hash", + "impl-rlp", + "impl-serde", + "primitive-types", + "uint", +] + +[[package]] +name = "fallible-iterator" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7" + +[[package]] +name = "fixed-hash" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcf0ed7fe52a17a03854ec54a9f76d6d84508d1c0e66bc1793301c73fc8493c" +dependencies = [ + "byteorder", + "rand 0.8.3", + "rustc-hex", + "static_assertions", +] + +[[package]] +name = "flate2" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd3aec53de10fe96d7d8c565eb17f2c687bb5518a2ec453b5b1252964526abe0" +dependencies = [ + "cfg-if 1.0.0", + "crc32fast", + "libc", + "miniz_oxide", +] + +[[package]] +name = "fnv" +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", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "form_urlencoded" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fc25a87fa4fd2094bffb06925852034d90a17f0d1e05197d4956d3555752191" +dependencies = [ + "matches", + "percent-encoding 2.1.0", +] + +[[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", + "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 = "futf" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c9c1ce3fa9336301af935ab852c437817d14cd33690446569392e65170aac3b" +dependencies = [ + "mac", + "new_debug_unreachable", +] + +[[package]] +name = "futures" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f55667319111d593ba876406af7c409c0ebb44dc4be6132a783ccf163ea14c1" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c2dd2df839b57db9ab69c2c9d8f3e8c81984781937fe2807dc6dcf3b2ad2939" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15496a72fabf0e62bdc3df11a59a3787429221dd0710ba8ef163d6f7a9112c94" + +[[package]] +name = "futures-executor" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891a4b7b96d84d5940084b2a37632dd65deeae662c114ceaa2c879629c9c0ad1" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71c2c65c57704c32f5241c1223167c2c3294fd34ac020c807ddbe6db287ba59" + +[[package]] +name = "futures-macro" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea405816a5139fb39af82c2beb921d52143f556038378d6db21183a5c37fbfb7" +dependencies = [ + "proc-macro-hack", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85754d98985841b7d4f5e8e6fbfa4a4ac847916893ec511a2917ccd8525b8bb3" + +[[package]] +name = "futures-task" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa189ef211c15ee602667a6fcfe1c1fd9e07d42250d2156382820fba33c9df80" + +[[package]] +name = "futures-timer" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e64b03909df88034c26dc1547e8970b91f98bdb65165d6a4e9110d94263dbb2c" + +[[package]] +name = "futures-util" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1812c7ab8aedf8d6f2701a43e1243acdbcc2b36ab26e2ad421eb99ac963d96d1" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite 0.2.6", + "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 = "generic-array" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "501466ecc8a30d1d3b7fc9229b122b2ce8ed6e9d9223f1138d4babb253e51817" +dependencies = [ + "typenum", + "version_check 0.9.3", +] + +[[package]] +name = "getrandom" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" +dependencies = [ + "cfg-if 1.0.0", + "libc", + "wasi 0.9.0+wasi-snapshot-preview1", +] + +[[package]] +name = "getrandom" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9495705279e7140bf035dde1f6e750c162df8b625267cd52cc44e0b156732c8" +dependencies = [ + "cfg-if 1.0.0", + "libc", + "wasi 0.10.0+wasi-snapshot-preview1", +] + +[[package]] +name = "ghash" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97304e4cd182c3846f7575ced3890c53012ce534ad9114046b0a9e00bb30a375" +dependencies = [ + "opaque-debug", + "polyval", +] + +[[package]] +name = "h2" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e4728fd124914ad25e99e3d15a9361a879f6620f63cb56bbb08f95abb97a535" +dependencies = [ + "bytes 0.5.6", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http", + "indexmap", + "slab", + "tokio 0.2.25", + "tokio-util", + "tracing", + "tracing-futures", +] + +[[package]] +name = "hashbrown" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7afe4a420e3fe79967a00898cc1f4db7c8a49a9333a29f8a4bd76a253d5cd04" + +[[package]] +name = "heck" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87cbf45460356b7deeb5e3415b5563308c0a9b057c85e12b06ad551f98d0a6ac" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "hermit-abi" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "322f4de77956e22ed0e5032c359a0f1273f1f7f0d79bfa3b8ffbc730d7fbcc5c" +dependencies = [ + "libc", +] + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hkdf" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51ab2f639c231793c5f6114bdb9bbe50a7dbbfcd7c7c6bd8475dec2d991e964f" +dependencies = [ + "digest", + "hmac 0.10.1", +] + +[[package]] +name = "hmac" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "deae6d9dbb35ec2c502d62b8f7b1c000a0822c3b0794ba36b3149c0a1c840dff" +dependencies = [ + "crypto-mac 0.9.1", + "digest", +] + +[[package]] +name = "hmac" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1441c6b1e930e2817404b5046f1f989899143a12bf92de603b69f4e0aee1e15" +dependencies = [ + "crypto-mac 0.10.0", + "digest", +] + +[[package]] +name = "hostname" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c731c3e10504cc8ed35cfe2f1db4c9274c3d35fa486e3b31df46f068ef3e867" +dependencies = [ + "libc", + "match_cfg", + "winapi 0.3.9", +] + +[[package]] +name = "html5ever" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aafcf38a1a36118242d29b92e1b08ef84e67e4a5ed06e0a80be20e6a32bfed6b" +dependencies = [ + "log", + "mac", + "markup5ever", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "http" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "527e8c9ac747e28542699a951517aa9a6945af506cd1f2e1b53a576c17b6cc11" +dependencies = [ + "bytes 1.0.1", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13d5ff830006f7646652e057693569bfe0d51760c0085a071769d142a205111b" +dependencies = [ + "bytes 0.5.6", + "http", +] + +[[package]] +name = "httparse" +version = "1.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc35c995b9d93ec174cf9a27d425c7892722101e14993cd227fdb51d70cf9589" + +[[package]] +name = "httpdate" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "494b4d60369511e7dea41cf646832512a94e542f68bb9c49e54518e0f468eb47" + +[[package]] +name = "hyper" +version = "0.13.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a6f157065790a3ed2f88679250419b5cdd96e714a0d65f7797fd337186e96bb" +dependencies = [ + "bytes 0.5.6", + "futures-channel", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project 1.0.6", + "socket2", + "tokio 0.2.25", + "tower-service", + "tracing", + "want", +] + +[[package]] +name = "hyper-proxy" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f93ec5be69758dfc06b9b29efa9d6e9306e387c85eb362c603912eead2ad98c7" +dependencies = [ + "bytes 0.5.6", + "futures", + "http", + "hyper", + "hyper-tls", + "native-tls", + "tokio 0.2.25", + "tokio-tls", + "tower-service", + "typed-headers", +] + +[[package]] +name = "hyper-tls" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d979acc56dcb5b8dddba3917601745e877576475aa046df3226eabdecef78eed" +dependencies = [ + "bytes 0.5.6", + "hyper", + "native-tls", + "tokio 0.2.25", + "tokio-tls", +] + +[[package]] +name = "idna" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38f09e0f0b1fb55fdee1f17470ad800da77af5186a1a76c026b679358b7e844e" +dependencies = [ + "matches", + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "idna" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89829a5d69c23d348314a7ac337fe39173b61149a9864deabd260983aed48c21" +dependencies = [ + "matches", + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "impl-codec" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1be51a921b067b0eaca2fad532d9400041561aa922221cc65f95a85641c6bf53" +dependencies = [ + "parity-scale-codec", +] + +[[package]] +name = "impl-rlp" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f28220f89297a075ddc7245cd538076ee98b01f2a9c23a53a4f1105d5a322808" +dependencies = [ + "rlp", +] + +[[package]] +name = "impl-serde" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b47ca4d2b6931707a55fce5cf66aff80e2178c8b63bbb4ecb5695cbc870ddf6f" +dependencies = [ + "serde", +] + +[[package]] +name = "indexmap" +version = "1.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824845a0bf897a9042383849b02c1bc219c2383772efcd5c6f9766fa4b81aef3" +dependencies = [ + "autocfg 1.0.1", + "hashbrown", +] + +[[package]] +name = "instant" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61124eeebbd69b8190558df225adf7e4caafce0d743919e5d6b19652314ec5ec" +dependencies = [ + "cfg-if 1.0.0", +] + +[[package]] +name = "iovec" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2b3ea6ff95e175473f8ffe6a7eb7c00d054240321b84c57051175fe3c1e075e" +dependencies = [ + "libc", +] + +[[package]] +name = "ipconfig" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7e2f18aece9709094573a9f24f483c4f65caa4298e2f7ae1b71cc65d853fad7" +dependencies = [ + "socket2", + "widestring", + "winapi 0.3.9", + "winreg 0.6.2", +] + +[[package]] +name = "ipnet" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68f2d64f2edebec4ce84ad108148e67e1064789bee435edc5b60ad398714a3a9" + +[[package]] +name = "itoa" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd25036021b0de88a0aff6b850051563c6516d0bf53f8638938edbb9de732736" + +[[package]] +name = "js-sys" +version = "0.3.51" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83bdfbace3a0e81a4253f73b49e960b053e396a11012cbd49b9b74d6a2b67062" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "jsonrpc-core" +version = "16.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a47c4c3ac843f9a4238943f97620619033dadef4b378cd1e8addd170de396b3" +dependencies = [ + "futures", + "log", + "serde", + "serde_derive", + "serde_json", +] + +[[package]] +name = "keccak" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67c21572b4949434e4fc1e1978b99c5f77064153c59d998bf13ecd96fb5ecba7" + +[[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", + "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" +dependencies = [ + "spin", +] + +[[package]] +name = "lexical-core" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6607c62aa161d23d17a9072cc5da0be67cdfc89d3afb1e8d9c842bebc2525ffe" +dependencies = [ + "arrayvec", + "bitflags", + "cfg-if 1.0.0", + "ryu", + "static_assertions", +] + +[[package]] +name = "libc" +version = "0.2.98" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320cfe77175da3a483efed4bc0adc1968ca050b098ce4f2f1c13a56626128790" + +[[package]] +name = "libm" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7d73b3f436185384286bd8098d17ec07c9a7d2388a6599f824d8502b529702a" + +[[package]] +name = "linked-hash-map" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fb9b38af92608140b86b693604b9ffcc5824240a484d1ecd4795bacb2fe88f3" + +[[package]] +name = "lock_api" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a3c91c24eae6777794bb1997ad98bbb87daf92890acab859f7eaa4320333176" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710" +dependencies = [ + "cfg-if 1.0.0", +] + +[[package]] +name = "lru-cache" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31e24f1ad8321ca0e8a1e0ac13f23cb668e6f5466c2c57319f6a5cf1cc8e3b1c" +dependencies = [ + "linked-hash-map", +] + +[[package]] +name = "mac" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" + +[[package]] +name = "maplit" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d" + +[[package]] +name = "markup5ever" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a24f40fb03852d1cdd84330cddcaf98e9ec08a7b7768e952fad3b4cf048ec8fd" +dependencies = [ + "log", + "phf", + "phf_codegen", + "string_cache", + "string_cache_codegen", + "tendril", +] + +[[package]] +name = "markup5ever_rcdom" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f015da43bcd8d4f144559a3423f4591d69b8ce0652c905374da7205df336ae2b" +dependencies = [ + "html5ever", + "markup5ever", + "tendril", + "xml5ever", +] + +[[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 = "md5" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "490cc448043f947bae3cbee9c203358d62dbee0db12107a74be5c30ccfd09771" + +[[package]] +name = "memchr" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b16bd47d9e329435e309c58469fe0791c2d0d1ba96ec0954152a5ae2b04387dc" + +[[package]] +name = "mime" +version = "0.3.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" + +[[package]] +name = "mime-sniffer" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e98f7cfbbaf64674624e2aa35327d75e3de8e4d1b2555ef70dcf0c107a95490" +dependencies = [ + "mime", + "url 1.7.2", +] + +[[package]] +name = "mime_guess" +version = "2.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2684d4c2e97d99848d30b324b00c8fcc7e5c897b7cbb5819b09e7c90e8baf212" +dependencies = [ + "mime", + "unicase", +] + +[[package]] +name = "miniz_oxide" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a92518e98c078586bc6c934028adcca4c92a53d6a958196de835170a01d84e4b" +dependencies = [ + "adler", + "autocfg 1.0.1", +] + +[[package]] +name = "mio" +version = "0.6.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4afd66f5b91bf2a3bc13fad0e21caedac168ca4c707504e75585648ae80e4cc4" +dependencies = [ + "cfg-if 0.1.10", + "fuchsia-zircon", + "fuchsia-zircon-sys", + "iovec", + "kernel32-sys", + "libc", + "log", + "miow", + "net2", + "slab", + "winapi 0.2.8", +] + +[[package]] +name = "mio-uds" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afcb699eb26d4332647cc848492bbc15eafb26f08d0304550d5aa1f612e066f0" +dependencies = [ + "iovec", + "libc", + "mio", +] + +[[package]] +name = "miow" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebd808424166322d4a38da87083bfddd3ac4c131334ed55856112eb06d46944d" +dependencies = [ + "kernel32-sys", + "net2", + "winapi 0.2.8", + "ws2_32-sys", +] + +[[package]] +name = "mitra" +version = "0.1.0" +dependencies = [ + "actix-cors", + "actix-files", + "actix-rt", + "actix-session", + "actix-web", + "ammonia", + "base64 0.13.0", + "chrono", + "clap", + "deadpool 0.7.0", + "deadpool-postgres", + "dotenv", + "env_logger", + "hex", + "log", + "mime-sniffer", + "mime_guess", + "num_cpus", + "postgres-types", + "rand 0.8.3", + "refinery", + "regex", + "reqwest", + "rsa", + "rust-argon2", + "secp256k1", + "serde", + "serde_json", + "serde_yaml", + "sha2", + "thiserror", + "tokio 0.2.25", + "tokio-postgres", + "url 2.2.2", + "uuid", + "web3", +] + +[[package]] +name = "native-tls" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8d96b2e1c8da3957d58100b09f102c6d9cfdfced01b7ec5a8974044bb09dbd4" +dependencies = [ + "lazy_static", + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + +[[package]] +name = "net2" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "391630d12b68002ae1e25e8f974306474966550ad82dac6886fb8910c19568ae" +dependencies = [ + "cfg-if 0.1.10", + "libc", + "winapi 0.3.9", +] + +[[package]] +name = "new_debug_unreachable" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4a24736216ec316047a1fc4252e27dabb04218aa4a3f37c6e7ddbf1f9782b54" + +[[package]] +name = "nom" +version = "4.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ad2a91a8e869eeb30b9cb3119ae87773a8f4ae617f41b1eb9c154b2905f7bd6" +dependencies = [ + "memchr", + "version_check 0.1.5", +] + +[[package]] +name = "nom" +version = "5.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffb4262d26ed83a1c0a33a38fe2bb15797329c85770da05e6b828ddb782627af" +dependencies = [ + "lexical-core", + "memchr", + "version_check 0.9.3", +] + +[[package]] +name = "num-bigint-dig" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4547ee5541c18742396ae2c895d0717d0f886d8823b8399cdaf7b07d63ad0480" +dependencies = [ + "autocfg 0.1.7", + "byteorder", + "lazy_static", + "libm", + "num-integer", + "num-iter", + "num-traits", + "rand 0.8.3", + "smallvec", + "zeroize", +] + +[[package]] +name = "num-integer" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2cc698a63b549a70bc047073d2949cce27cd1c7b0a4a862d08a8031bc2801db" +dependencies = [ + "autocfg 1.0.1", + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2021c8337a54d21aca0d59a92577a029af9431cb59b909b03252b9c164fad59" +dependencies = [ + "autocfg 1.0.1", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290" +dependencies = [ + "autocfg 1.0.1", + "libm", +] + +[[package]] +name = "num_cpus" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05499f3756671c15885fee9034446956fff3f243d6077b91e5767df161f766b3" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "once_cell" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "692fcb63b64b1758029e0a96ee63e049ce8c5948587f2f7208df04625e5f6b56" + +[[package]] +name = "opaque-debug" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" + +[[package]] +name = "openssl" +version = "0.10.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "549430950c79ae24e6d02e0b7404534ecf311d94cc9f861e9e4020187d13d885" +dependencies = [ + "bitflags", + "cfg-if 1.0.0", + "foreign-types", + "libc", + "once_cell", + "openssl-sys", +] + +[[package]] +name = "openssl-probe" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28988d872ab76095a6e6ac88d99b54fd267702734fd7ffe610ca27f533ddb95a" + +[[package]] +name = "openssl-sys" +version = "0.9.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a7907e3bfa08bb85105209cdfcb6c63d109f8f6c1ed6ca318fff5c1853fbc1d" +dependencies = [ + "autocfg 1.0.1", + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "os_str_bytes" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afb2e1c3ee07430c2cf76151675e583e0f19985fa6efae47d6848a3e2c824f85" + +[[package]] +name = "parity-scale-codec" +version = "1.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4b26b16c7687c3075982af47719e481815df30bc544f7a6690763a25ca16e9d" +dependencies = [ + "arrayvec", + "bitvec", + "byte-slice-cast", + "serde", +] + +[[package]] +name = "parking_lot" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d7744ac029df22dca6284efe4e898991d28e3085c706c972bcd7da4a27a15eb" +dependencies = [ + "instant", + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7a782938e745763fe6907fc6ba86946d72f49fe7e21de074e08128a99fb018" +dependencies = [ + "cfg-if 1.0.0", + "instant", + "libc", + "redox_syscall", + "smallvec", + "winapi 0.3.9", +] + +[[package]] +name = "pem-rfc7468" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8fe90c78c9a17442665a41a1a45dcd24bbab0e1794748edc19b27fffb146c13" +dependencies = [ + "base64ct", +] + +[[package]] +name = "percent-encoding" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31010dd2e1ac33d5b46a5b413495239882813e0369f8ed8a5e266f173602f831" + +[[package]] +name = "percent-encoding" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" + +[[package]] +name = "phf" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3dfb61232e34fcb633f43d12c58f83c1df82962dcdfa565a4e866ffc17dafe12" +dependencies = [ + "phf_shared", +] + +[[package]] +name = "phf_codegen" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbffee61585b0411840d3ece935cce9cb6321f01c45477d30066498cd5e1a815" +dependencies = [ + "phf_generator", + "phf_shared", +] + +[[package]] +name = "phf_generator" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17367f0cc86f2d25802b2c26ee58a7b23faeccf78a396094c13dced0d0182526" +dependencies = [ + "phf_shared", + "rand 0.7.3", +] + +[[package]] +name = "phf_shared" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c00cf8b9eafe68dde5e9eaa2cef8ee84a9336a47d566ec55ca16589633b65af7" +dependencies = [ + "siphasher", +] + +[[package]] +name = "pin-project" +version = "0.4.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "918192b5c59119d51e0cd221f4d49dde9112824ba717369e903c97d076083d0f" +dependencies = [ + "pin-project-internal 0.4.28", +] + +[[package]] +name = "pin-project" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc174859768806e91ae575187ada95c91a29e96a98dc5d2cd9a1fed039501ba6" +dependencies = [ + "pin-project-internal 1.0.6", +] + +[[package]] +name = "pin-project-internal" +version = "0.4.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3be26700300be6d9d23264c73211d8190e755b6b5ca7a1b28230025511b52a5e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pin-project-internal" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a490329918e856ed1b083f244e3bfe2d8c4f336407e4ea9e1a9f479ff09049e5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pin-project-lite" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "257b64915a082f7811703966789728173279bdebb956b143dbcd23f6f970a777" + +[[package]] +name = "pin-project-lite" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc0e1f259c92177c30a4c9d177246edd0a3568b25756a977d0632cf8fa37e905" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkcs1" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "359e7852310174a810f078124edb73c66e88a1a731b2fd586dba34ee32dbe416" +dependencies = [ + "der", + "pem-rfc7468", + "zeroize", +] + +[[package]] +name = "pkcs8" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbee84ed13e44dd82689fa18348a49934fa79cc774a344c42fc9b301c71b140a" +dependencies = [ + "der", + "pem-rfc7468", + "pkcs1", + "spki", + "zeroize", +] + +[[package]] +name = "pkg-config" +version = "0.3.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3831453b3449ceb48b6d9c7ad7c96d5ea673e9b470a1dc578c2ce6521230884c" + +[[package]] +name = "polyval" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eebcc4aa140b9abd2bc40d9c3f7ccec842679cd79045ac3a7ac698c1a064b7cd" +dependencies = [ + "cpuid-bool 0.2.0", + "opaque-debug", + "universal-hash", +] + +[[package]] +name = "postgres-derive" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c857dd221cb0e7d8414b894a0ce29eae44d453dda0baa132447878e75e701477" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "postgres-protocol" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4888a0e36637ab38d76cace88c1476937d617ad015f07f6b669cec11beacc019" +dependencies = [ + "base64 0.13.0", + "byteorder", + "bytes 0.5.6", + "fallible-iterator", + "hmac 0.9.0", + "md5", + "memchr", + "rand 0.7.3", + "sha2", + "stringprep", +] + +[[package]] +name = "postgres-types" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfc08a7d94a80665de4a83942fa8db2fdeaf2f123fc0535e384dc4fff251efae" +dependencies = [ + "bytes 0.5.6", + "chrono", + "fallible-iterator", + "postgres-derive", + "postgres-protocol", + "serde", + "serde_json", + "uuid", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac74c624d6b2d21f425f752262f42188365d7b8ff1aff74c82e45136510a4857" + +[[package]] +name = "precomputed-hash" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" + +[[package]] +name = "primitive-types" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3824ae2c5e27160113b9e029a10ec9e3f0237bad8029f69c7724393c9fdefd8" +dependencies = [ + "fixed-hash", + "impl-codec", + "impl-rlp", + "impl-serde", + "uint", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn", + "version_check 0.9.3", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check 0.9.3", +] + +[[package]] +name = "proc-macro-hack" +version = "0.5.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbf0c48bc1d91375ae5c3cd81e3722dff1abcf81a30960240640d223f59fe0e5" + +[[package]] +name = "proc-macro-nested" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc881b2c22681370c6a780e47af9840ef841837bc98118431d4e1868bd0c1086" + +[[package]] +name = "proc-macro2" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a152013215dca273577e18d2bf00fa862b89b24169fb78c4c95aeb07992c9cec" +dependencies = [ + "unicode-xid", +] + +[[package]] +name = "quick-error" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" + +[[package]] +name = "quote" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d0b9745dc2debf507c8422de05d7226cc1f0644216dfdfead988f9b1ab32a7" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "radium" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "def50a86306165861203e7f84ecffbbdfdea79f0e51039b33de1e952358c47ac" + +[[package]] +name = "rand" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d71dacdc3c88c1fde3885a3be3fbab9f35724e6ce99467f7d9c5026132184ca" +dependencies = [ + "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 0.1.2", + "rand_xorshift", + "winapi 0.3.9", +] + +[[package]] +name = "rand" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" +dependencies = [ + "getrandom 0.1.16", + "libc", + "rand_chacha 0.2.2", + "rand_core 0.5.1", + "rand_hc 0.2.0", + "rand_pcg 0.2.1", +] + +[[package]] +name = "rand" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ef9e7e66b4468674bfcb0c81af8b7fa0bb154fa9f28eb840da5c447baeb8d7e" +dependencies = [ + "libc", + "rand_chacha 0.3.0", + "rand_core 0.6.2", + "rand_hc 0.3.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", + "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", + "rand_core 0.5.1", +] + +[[package]] +name = "rand_chacha" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e12735cf05c9e10bf21534da50a147b924d555dc7a547c42e6bb2d5b6017ae0d" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.2", +] + +[[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", +] + +[[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.16", +] + +[[package]] +name = "rand_core" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34cf66eb183df1c5876e2dcf6b13d57340741e8dc255b48e40a26de954d06ae7" +dependencies = [ + "getrandom 0.2.2", +] + +[[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", +] + +[[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", +] + +[[package]] +name = "rand_hc" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3190ef7066a446f2e7f42e239d161e905420ccab01eb967c9eb27d21b2322a73" +dependencies = [ + "rand_core 0.6.2", +] + +[[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", +] + +[[package]] +name = "rand_jitter" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1166d5c91dc97b88d1decc3285bb0a99ed84b05cfd0bc2341bdf2d43fc41e39b" +dependencies = [ + "libc", + "rand_core 0.4.2", + "winapi 0.3.9", +] + +[[package]] +name = "rand_os" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b75f676a1e053fc562eafbb47838d67c84801e38fc1ba459e8f180deabd5071" +dependencies = [ + "cloudabi", + "fuchsia-cprng", + "libc", + "rand_core 0.4.2", + "rdrand", + "winapi 0.3.9", +] + +[[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", + "rand_core 0.4.2", +] + +[[package]] +name = "rand_pcg" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16abd0c1b639e9eb4d7c50c0b8100b0d0f849be2349829c740fe8e6eb4816429" +dependencies = [ + "rand_core 0.5.1", +] + +[[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", +] + +[[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", +] + +[[package]] +name = "redox_syscall" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94341e4e44e24f6b591b59e47a8a027df12e008d73fd5672dbea9cc22f4507d9" +dependencies = [ + "bitflags", +] + +[[package]] +name = "refinery" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d34d324a0627219489ebdda4ddad5397902fb9d38777bf499223373aeaaba91" +dependencies = [ + "refinery-core", + "refinery-macros", +] + +[[package]] +name = "refinery-core" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eed7c0812dfc4ec2596a474e175002cc99576e8c78dd0a2c41aea5a451ad0216" +dependencies = [ + "async-trait", + "cfg-if 0.1.10", + "chrono", + "lazy_static", + "log", + "regex", + "serde", + "siphasher", + "thiserror", + "tokio-postgres", + "toml", + "url 2.2.2", + "walkdir", +] + +[[package]] +name = "refinery-macros" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a80f45b9c4c99b4268ce14305f16b129f6c40b392c9fcaa350da5e805ad7c64" +dependencies = [ + "proc-macro2", + "quote", + "refinery-core", + "regex", + "syn", +] + +[[package]] +name = "regex" +version = "1.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d07a8629359eb56f1e2fb1652bb04212c072a87ba68546a04065d525673ac461" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.6.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b" + +[[package]] +name = "remove_dir_all" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" +dependencies = [ + "winapi 0.3.9", +] + +[[package]] +name = "reqwest" +version = "0.10.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0718f81a8e14c4dbb3b34cf23dc6aaf9ab8a0dfec160c534b3dbca1aaa21f47c" +dependencies = [ + "base64 0.13.0", + "bytes 0.5.6", + "encoding_rs", + "futures-core", + "futures-util", + "http", + "http-body", + "hyper", + "hyper-tls", + "ipnet", + "js-sys", + "lazy_static", + "log", + "mime", + "mime_guess", + "native-tls", + "percent-encoding 2.1.0", + "pin-project-lite 0.2.6", + "serde", + "serde_json", + "serde_urlencoded", + "tokio 0.2.25", + "tokio-tls", + "url 2.2.2", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "winreg 0.7.0", +] + +[[package]] +name = "resolv-conf" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52e44394d2086d010551b14b53b1f24e31647570cd1deb0379e2c21b329aba00" +dependencies = [ + "hostname", + "quick-error", +] + +[[package]] +name = "rlp" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "999508abb0ae792aabed2460c45b89106d97fe4adac593bdaef433c2605847b5" +dependencies = [ + "bytes 1.0.1", + "rustc-hex", +] + +[[package]] +name = "rsa" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e05c2603e2823634ab331437001b411b9ed11660fbc4066f3908c84a9439260d" +dependencies = [ + "byteorder", + "digest", + "lazy_static", + "num-bigint-dig", + "num-integer", + "num-iter", + "num-traits", + "pkcs1", + "pkcs8", + "rand 0.8.3", + "subtle", + "zeroize", +] + +[[package]] +name = "rust-argon2" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b18820d944b33caa75a71378964ac46f58517c92b6ae5f762636247c09e78fb" +dependencies = [ + "base64 0.13.0", + "blake2b_simd", + "constant_time_eq", + "crossbeam-utils 0.8.3", +] + +[[package]] +name = "rustc-hex" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e75f6a532d0fd9f7f13144f392b6ad56a32696bfcd9c78f797f16bbb6f072d6" + +[[package]] +name = "rustc_version" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "138e3e0acb6c9fb258b19b67cb8abd63c00679d2851805ea151465464fe9030a" +dependencies = [ + "semver", +] + +[[package]] +name = "ryu" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schannel" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f05ba609c234e60bee0d547fe94a4c7e9da733d1c962cf6e59efa4cd9c8bc75" +dependencies = [ + "lazy_static", + "winapi 0.3.9", +] + +[[package]] +name = "scopeguard" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" + +[[package]] +name = "secp256k1" +version = "0.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97d03ceae636d0fed5bae6a7f4f664354c5f4fcedf6eef053fef17e49f837d0a" +dependencies = [ + "rand 0.6.5", + "secp256k1-sys", +] + +[[package]] +name = "secp256k1-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "827cb7cce42533829c792fc51b82fbf18b125b45a702ef2c8be77fce65463a7b" +dependencies = [ + "cc", +] + +[[package]] +name = "security-framework" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23a2ac85147a3a11d77ecf1bc7166ec0b92febfa4461c37944e180f319ece467" +dependencies = [ + "bitflags", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e4effb91b4b8b6fb7732e670b6cee160278ff8e6bf485c7805d9e319d76e284" +dependencies = [ + "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", +] + +[[package]] +name = "semver-parser" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" + +[[package]] +name = "serde" +version = "1.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b88fa983de7720629c9387e9f517353ed404164b1e482c970a90c1a4aaf7dc1a" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbd1ae72adb44aab48f325a02444a5fc079349a8d804c1fc922aed3f7454c74e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799e97dc9fdae36a5c8b8f2cae9ce2ee9fdce2058c57a93e6099d919fd982f79" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edfa57a7f8d9c1d260a549e7224100f6c43d43f9103e06dd8b4095a9b2b43ce9" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_yaml" +version = "0.8.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15654ed4ab61726bf918a39cb8d98a2e2995b002387807fa6ba58fdf7f59bb23" +dependencies = [ + "dtoa", + "linked-hash-map", + "serde", + "yaml-rust", +] + +[[package]] +name = "sha-1" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfebf75d25bd900fd1e7d11501efab59bc846dbc76196839663e6637bba9f25f" +dependencies = [ + "block-buffer", + "cfg-if 1.0.0", + "cpuid-bool 0.1.2", + "digest", + "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.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b362ae5752fd2137731f9fa25fd4d9058af34666ca1966fb969119cc35719f12" +dependencies = [ + "block-buffer", + "cfg-if 1.0.0", + "cpufeatures", + "digest", + "opaque-debug", +] + +[[package]] +name = "sha3" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f81199417d4e5de3f04b1e871023acea7389672c4135918f05aa9cbf2f2fa809" +dependencies = [ + "block-buffer", + "digest", + "keccak", + "opaque-debug", +] + +[[package]] +name = "signal-hook-registry" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16f1d0fef1604ba8f7a073c7e701f213e056707210e9020af4528e0101ce11a6" +dependencies = [ + "libc", +] + +[[package]] +name = "siphasher" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbce6d4507c7e4a3962091436e56e95290cb71fa302d0d270e32130b75fbff27" + +[[package]] +name = "slab" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c111b5bd5695e56cffe5129854aa230b39c93a305372fdbb2668ca2394eea9f8" + +[[package]] +name = "smallvec" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe0f37c9e8f3c5a4a66ad655a93c74daac4ad00c441533bf5c6e7990bb42604e" + +[[package]] +name = "socket2" +version = "0.3.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "122e570113d28d773067fab24266b66753f6ea915758651696b6e35e49f88d6e" +dependencies = [ + "cfg-if 1.0.0", + "libc", + "winapi 0.3.9", +] + +[[package]] +name = "spin" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" + +[[package]] +name = "spki" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "987637c5ae6b3121aba9d513f869bd2bff11c4cc086c22473befd6649c0bd521" +dependencies = [ + "der", +] + +[[package]] +name = "standback" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e113fb6f3de07a243d434a56ec6f186dfd51cb08448239fe7bcae73f87ff28ff" +dependencies = [ + "version_check 0.9.3", +] + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "stdweb" +version = "0.4.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d022496b16281348b52d0e30ae99e01a73d737b2f45d38fed4edf79f9325a1d5" +dependencies = [ + "discard", + "rustc_version", + "stdweb-derive", + "stdweb-internal-macros", + "stdweb-internal-runtime", + "wasm-bindgen", +] + +[[package]] +name = "stdweb-derive" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c87a60a40fccc84bef0652345bbbbbe20a605bf5d0ce81719fc476f5c03b50ef" +dependencies = [ + "proc-macro2", + "quote", + "serde", + "serde_derive", + "syn", +] + +[[package]] +name = "stdweb-internal-macros" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58fa5ff6ad0d98d1ffa8cb115892b6e69d67799f6763e162a1c9db421dc22e11" +dependencies = [ + "base-x", + "proc-macro2", + "quote", + "serde", + "serde_derive", + "serde_json", + "sha1", + "syn", +] + +[[package]] +name = "stdweb-internal-runtime" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "213701ba3370744dcd1a12960caa4843b3d68b4d1c0a5d575e0d65b2ee9d16c0" + +[[package]] +name = "string_cache" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ddb1139b5353f96e429e1a5e19fbaf663bddedaa06d1dbd49f82e352601209a" +dependencies = [ + "lazy_static", + "new_debug_unreachable", + "phf_shared", + "precomputed-hash", + "serde", +] + +[[package]] +name = "string_cache_codegen" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f24c8e5e19d22a726626f1a5e16fe15b132dcf21d10177fa5a45ce7962996b97" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", +] + +[[package]] +name = "stringprep" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ee348cb74b87454fff4b551cbf727025810a004f88aeacae7f85b87f4e9a1c1" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "subtle" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e81da0851ada1f3e9d4312c704aa4f8806f0f9d69faaf8df2f3464b4a9437c2" + +[[package]] +name = "syn" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48fe99c6bd8b1cc636890bcc071842de909d902c81ac7dab53ba33c421ab8ffb" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + +[[package]] +name = "synstructure" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "474aaa926faa1603c40b7885a9eaea29b444d1cb2850cb7c0e37bb1a4182f4fa" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "unicode-xid", +] + +[[package]] +name = "tempfile" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dac1c663cfc93810f88aed9b8941d48cabf856a1b111c29a40439018d870eb22" +dependencies = [ + "cfg-if 1.0.0", + "libc", + "rand 0.8.3", + "redox_syscall", + "remove_dir_all", + "winapi 0.3.9", +] + +[[package]] +name = "tendril" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9ef557cb397a4f0a5a3a628f06515f78563f2209e64d47055d9dc6052bf5e33" +dependencies = [ + "futf", + "mac", + "utf-8", +] + +[[package]] +name = "textwrap" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "203008d98caf094106cfaba70acfed15e18ed3ddb7d94e49baec153a2b462789" +dependencies = [ + "unicode-width", +] + +[[package]] +name = "thiserror" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0f4a65597094d4483ddaed134f409b2cb7c1beccf25201a9f73c719254fa98e" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7765189610d8241a44529806d6fd1f2e0a08734313a35d5b3a556f92b381f3c0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "threadpool" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d050e60b33d41c19108b32cea32164033a9013fe3b46cbd4457559bfbf77afaa" +dependencies = [ + "num_cpus", +] + +[[package]] +name = "time" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db9e6914ab8b1ae1c260a4ae7a49b6c5611b40328a735b21862567685e73255" +dependencies = [ + "libc", + "wasi 0.10.0+wasi-snapshot-preview1", + "winapi 0.3.9", +] + +[[package]] +name = "time" +version = "0.2.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08a8cbfbf47955132d0202d1662f49b2423ae35862aee471f3ba4b133358f372" +dependencies = [ + "const_fn", + "libc", + "standback", + "stdweb", + "time-macros", + "version_check 0.9.3", + "winapi 0.3.9", +] + +[[package]] +name = "time-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "957e9c6e26f12cb6d0dd7fc776bb67a706312e7299aed74c8dd5b17ebb27e2f1" +dependencies = [ + "proc-macro-hack", + "time-macros-impl", +] + +[[package]] +name = "time-macros-impl" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5c3be1edfad6027c69f5491cf4cb310d1a71ecd6af742788c6ff8bced86b8fa" +dependencies = [ + "proc-macro-hack", + "proc-macro2", + "quote", + "standback", + "syn", +] + +[[package]] +name = "tiny-keccak" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" +dependencies = [ + "crunchy", +] + +[[package]] +name = "tinyvec" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b5220f05bb7de7f3f53c7c065e1199b3172696fe2db9f9c4d8ad9b4ee74c342" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" + +[[package]] +name = "tokio" +version = "0.2.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6703a273949a90131b290be1fe7b039d0fc884aa1935860dfcbe056f28cd8092" +dependencies = [ + "bytes 0.5.6", + "fnv", + "futures-core", + "iovec", + "lazy_static", + "libc", + "memchr", + "mio", + "mio-uds", + "pin-project-lite 0.1.12", + "signal-hook-registry", + "slab", + "tokio-macros", + "winapi 0.3.9", +] + +[[package]] +name = "tokio" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83f0c8e7c0addab50b663055baf787d0af7f413a46e6e7fb9559a4e4db7137a5" +dependencies = [ + "autocfg 1.0.1", + "pin-project-lite 0.2.6", +] + +[[package]] +name = "tokio-macros" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e44da00bfc73a25f814cd8d7e57a68a5c31b74b3152a0a1d1f590c97ed06265a" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio-postgres" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55a2482c9fe4dd481723cf5c0616f34afc710e55dcda0944e12e7b3316117892" +dependencies = [ + "async-trait", + "byteorder", + "bytes 0.5.6", + "fallible-iterator", + "futures", + "log", + "parking_lot", + "percent-encoding 2.1.0", + "phf", + "pin-project-lite 0.1.12", + "postgres-protocol", + "postgres-types", + "tokio 0.2.25", + "tokio-util", +] + +[[package]] +name = "tokio-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a70f4fcd7b3b24fb194f837560168208f669ca8cb70d0c4b862944452396343" +dependencies = [ + "native-tls", + "tokio 0.2.25", +] + +[[package]] +name = "tokio-util" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be8242891f2b6cbef26a2d7e8605133c2c554cd35b3e4948ea892d6d68436499" +dependencies = [ + "bytes 0.5.6", + "futures-core", + "futures-sink", + "log", + "pin-project-lite 0.1.12", + "tokio 0.2.25", +] + +[[package]] +name = "toml" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a31142970826733df8241ef35dc040ef98c679ab14d7c3e54d827099b3acecaa" +dependencies = [ + "serde", +] + +[[package]] +name = "tower-service" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "360dfd1d6d30e05fda32ace2c8c70e9c0a9da713275777f5a4dbb8a1893930c6" + +[[package]] +name = "tracing" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01ebdc2bb4498ab1ab5f5b73c5803825e60199229ccba0698170e3be0e7f959f" +dependencies = [ + "cfg-if 1.0.0", + "log", + "pin-project-lite 0.2.6", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f50de3927f93d202783f4513cda820ab47ef17f624b03c096e86ef00c67e6b5f" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "tracing-futures" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97d095ae15e245a057c8e8451bab9b3ee1e1f68e9ba2b4fbc18d0ac5237835f2" +dependencies = [ + "pin-project 1.0.6", + "tracing", +] + +[[package]] +name = "trust-dns-proto" +version = "0.19.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cad71a0c0d68ab9941d2fb6e82f8fb2e86d9945b94e1661dd0aaea2b88215a9" +dependencies = [ + "async-trait", + "cfg-if 1.0.0", + "enum-as-inner", + "futures", + "idna 0.2.2", + "lazy_static", + "log", + "rand 0.7.3", + "smallvec", + "thiserror", + "tokio 0.2.25", + "url 2.2.2", +] + +[[package]] +name = "trust-dns-resolver" +version = "0.19.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "710f593b371175db53a26d0b38ed2978fafb9e9e8d3868b1acd753ea18df0ceb" +dependencies = [ + "cfg-if 0.1.10", + "futures", + "ipconfig", + "lazy_static", + "log", + "lru-cache", + "resolv-conf", + "smallvec", + "thiserror", + "tokio 0.2.25", + "trust-dns-proto", +] + +[[package]] +name = "try-lock" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642" + +[[package]] +name = "typed-headers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3179a61e9eccceead5f1574fd173cf2e162ac42638b9bf214c6ad0baf7efa24a" +dependencies = [ + "base64 0.11.0", + "bytes 0.5.6", + "chrono", + "http", + "mime", +] + +[[package]] +name = "typenum" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f6906492a7cd215bfa4cf595b600146ccfac0c79bcbd1f3000162af5e8b06" + +[[package]] +name = "uint" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6470ab50f482bde894a037a57064480a246dbfdd5960bd65a44824693f08da5f" +dependencies = [ + "byteorder", + "crunchy", + "hex", + "static_assertions", +] + +[[package]] +name = "unicase" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50f37be617794602aabbeee0be4f259dc1778fabe05e2d67ee8f79326d5cb4f6" +dependencies = [ + "version_check 0.9.3", +] + +[[package]] +name = "unicode-bidi" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eeb8be209bb1c96b7c177c7420d26e04eccacb0eeae6b980e35fcb74678107e0" +dependencies = [ + "matches", +] + +[[package]] +name = "unicode-normalization" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07fbfce1c8a97d547e8b5334978438d9d6ec8c20e38f56d4a4374d181493eaef" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-segmentation" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0d2e7be6ae3a5fa87eed5fb451aff96f2573d2694942e40543ae0bbe19c796" + +[[package]] +name = "unicode-width" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9337591893a19b88d8d87f2cec1e73fad5cdfd10e5a6f349f498ad6ea2ffb1e3" + +[[package]] +name = "unicode-xid" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564" + +[[package]] +name = "universal-hash" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8326b2c654932e3e4f9196e69d08fdf7cfd718e1dc6f66b347e6024a0c961402" +dependencies = [ + "generic-array", + "subtle", +] + +[[package]] +name = "url" +version = "1.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd4e7c0d531266369519a4aa4f399d748bd37043b00bde1e4ff1f60a120b355a" +dependencies = [ + "idna 0.1.5", + "matches", + "percent-encoding 1.0.1", +] + +[[package]] +name = "url" +version = "2.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a507c383b2d33b5fc35d1861e77e6b383d158b2da5e14fe51b83dfedf6fd578c" +dependencies = [ + "form_urlencoded", + "idna 0.2.2", + "matches", + "percent-encoding 2.1.0", +] + +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + +[[package]] +name = "uuid" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7" +dependencies = [ + "getrandom 0.2.2", + "serde", +] + +[[package]] +name = "v_escape" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3e0ab5fab1db278a9413d2ea794cb66f471f898c5b020c3c394f6447625d9d4" +dependencies = [ + "buf-min", + "v_escape_derive", +] + +[[package]] +name = "v_escape_derive" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c860ad1273f4eee7006cee05db20c9e60e5d24cba024a32e1094aa8e574f3668" +dependencies = [ + "nom 4.2.3", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "v_htmlescape" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f9a8af610ad6f7fc9989c9d2590d9764bc61f294884e9ee93baa58795174572" +dependencies = [ + "cfg-if 1.0.0", + "v_escape", +] + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "vec_map" +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.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe" + +[[package]] +name = "walkdir" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "808cf2735cd4b6866113f648b791c6adc5714537bc222d9347bb203386ffda56" +dependencies = [ + "same-file", + "winapi 0.3.9", + "winapi-util", +] + +[[package]] +name = "want" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ce8a968cb1cd110d136ff8b819a556d6fb6d919363c61534f6860c7eb172ba0" +dependencies = [ + "log", + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.9.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" + +[[package]] +name = "wasi" +version = "0.10.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" + +[[package]] +name = "wasm-bindgen" +version = "0.2.74" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d54ee1d4ed486f78874278e63e4069fc1ab9f6a18ca492076ffb90c5eb2997fd" +dependencies = [ + "cfg-if 1.0.0", + "serde", + "serde_json", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.74" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b33f6a0694ccfea53d94db8b2ed1c3a8a4c86dd936b13b9f0a15ec4a451b900" +dependencies = [ + "bumpalo", + "lazy_static", + "log", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fba7978c679d53ce2d0ac80c8c175840feb849a161664365d1287b41f2e67f1" +dependencies = [ + "cfg-if 1.0.0", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.74" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "088169ca61430fe1e58b8096c24975251700e7b1f6fd91cc9d59b04fb9b18bd4" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.74" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be2241542ff3d9f241f5e2cb6dd09b37efe786df8851c54957683a49f0987a97" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.74" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7cff876b8f18eed75a66cf49b65e7f967cb354a7aa16003fb55dbfd25b44b4f" + +[[package]] +name = "web-sys" +version = "0.3.51" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e828417b379f3df7111d3a2a9e5753706cae29c41f7c4029ee9fd77f3e09e582" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web3" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4080a844bbb41437f0d432138f0a7543780a40b43345b76dc0338c59bdfd1336" +dependencies = [ + "arrayvec", + "base64 0.13.0", + "derive_more", + "ethabi", + "ethereum-types", + "futures", + "futures-timer", + "hex", + "hyper", + "hyper-proxy", + "hyper-tls", + "jsonrpc-core", + "log", + "native-tls", + "parking_lot", + "pin-project 1.0.6", + "rlp", + "secp256k1", + "serde", + "serde_json", + "tiny-keccak", + "typed-headers", + "url 2.2.2", +] + +[[package]] +name = "widestring" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c168940144dd21fd8046987c16a46a33d5fc84eec29ef9dcddc2ac9e31526b7c" + +[[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.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "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.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +dependencies = [ + "winapi 0.3.9", +] + +[[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.9", +] + +[[package]] +name = "winreg" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0120db82e8a1e0b9fb3345a539c478767c0048d842860994d96113d5b667bd69" +dependencies = [ + "winapi 0.3.9", +] + +[[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", + "winapi-build", +] + +[[package]] +name = "xml5ever" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b1b52e6e8614d4a58b8e70cf51ec0cc21b256ad8206708bcff8139b5bbd6a59" +dependencies = [ + "log", + "mac", + "markup5ever", + "time 0.1.44", +] + +[[package]] +name = "yaml-rust" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" +dependencies = [ + "linked-hash-map", +] + +[[package]] +name = "zeroize" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4756f7db3f7b5574938c3eb1c117038b8e07f95ee6718c0efad4ac21508f1efd" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2c1e130bebaeab2f23886bf9acbaca14b092408c452543c857f66399cd6dab1" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..904629c --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,77 @@ +[package] +name = "mitra" +version = "0.1.0" +description = "Mitra backend" +license = "AGPL-3.0" + +edition = "2018" +publish = false +default-run = "mitra" + +[dependencies] +# Used to handle incoming HTTP requests +actix-cors = "0.5.4" +actix-files = "0.5.0" +actix-session = "0.4.1" +actix-web = "3.3.2" +# Used for managing async tasks +actix-rt = "1.1.1" +# Used for HTML sanitization +ammonia = "3.1.2" +# Used for working with RSA keys, HTTP signatures and file uploads +base64 = "0.13.0" +# Used for working with dates +chrono = { version = "0.4.19", features = ["serde"] } +# Used to build admin CLI tool +clap = { version = "3.0.0-beta.2", default-features = false, features = ["std", "derive"] } +# Used for pooling database connections +deadpool = "0.7.0" +deadpool-postgres = { version = "0.5.6", default-features = false } +# Used to read .env files +dotenv = "0.15.0" +# Used to work with hexadecimal strings +hex = "0.4.3" +# Used for logging +log = "0.4.14" +env_logger = { version = "0.8.4", default-features = false } +# Used to guess media type of a file +mime_guess = "2.0.3" +mime-sniffer = "0.1.2" +# Used to determine the number of CPUs on the system +num_cpus = "1.13.0" +# Used to map postgres types to rust types +postgres-types = { version = "0.1.2", features = ["derive", "with-chrono-0_4", "with-uuid-0_8", "with-serde_json-1"] } +# Used for working with regular expressions +regex = "1.5.4" +# Used to generate random numbers +rand = "0.8.3" +# Used for managing database migrations +refinery = { version = "0.4.0", features = ["tokio-postgres"] } +# Used for making async HTTP requests +reqwest = { version = "0.10.10", features = ["json"] } +# Used for working with RSA keys +rsa = "0.5.0" +# Used for hashing passwords +rust-argon2 = "0.8.3" +# Used for working with ethereum keys +secp256k1 = { version = "0.20.3", features = ["rand", "rand-std"] } +# Used for serialization/deserialization +# https://github.com/rust-db/refinery/issues/160 +serde = { version = "=1.0.117", features = ["derive"] } +serde_json = "1.0" +# Used to parse config file +serde_yaml = "0.8.17" +# Used to calculate SHA2 hashes +sha2 = "0.9.5" +# Used for creating error types +thiserror = "1.0.24" +# Async runtime ( required for #[tokio::main] ) +tokio = { version = "0.2.25", features = ["macros"] } +# Used for working with Postgresql database (compatible with tokio 0.2) +tokio-postgres = { version = "0.5.5", features = ["with-chrono-0_4", "with-uuid-0_8", "with-serde_json-1"] } +# Used to work with URLs +url = "2.2.2" +# Used to work with UUIDs +uuid = { version = "0.8.2", features = ["serde", "v4"] } +# Used to query ethereum node +web3 = { version = "0.15.0", default-features = false, features = ["http", "http-tls", "signing"] } diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..be3f7b2 --- /dev/null +++ b/LICENSE @@ -0,0 +1,661 @@ + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU Affero General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU AGPL, see +. diff --git a/README.md b/README.md new file mode 100644 index 0000000..9a48676 --- /dev/null +++ b/README.md @@ -0,0 +1,114 @@ +# Mitra + +Federated social network with smart contracts. + +- Built on [ActivityPub](https://activitypub.rocks/) protocol. +- Lightweight. +- Sign-in with Ethereum. +- Converting posts into NFTs. +- More crypto features in the future. + +**WIP: Mitra is not ready for production yet.** + +Demo instance: https://test.mitra.pm/ (invite-only) + +## Requirements + +- Rust 1.51+ +- Postgresql +- IPFS node (optional) +- Ethereum node (optional) + +## Development + +### Create database + +``` +docker-compose up +``` + +Test connection: + +``` +psql -h localhost -p 5432 -U mitra mitra +``` + +### Run web service + +Create config file: + +``` +cp config.yaml.example config.yaml +``` + +Compile and run service: + +``` +cargo run +``` + +### Run CLI + +``` +cargo run --bin mitractl +``` + +### Build for production + +``` +cargo build --release +``` + +## API + +### Mastodon API + +Endpoints are similar to Mastodon API: + +``` +GET /api/v1/accounts/{account_id} +PATCH /api/v1/accounts/update_credentials +GET /api/v1/accounts/relationships +POST /api/v1/accounts/{account_id}/follow +POST /api/v1/accounts/{account_id}/unfollow +GET /api/v1/directory +GET /api/v1/instance +POST /api/v1/media +GET /api/v2/search +POST /api/v1/statuses +GET /api/v1/statuses/{status_id} +GET /api/v1/timelines/home +``` + +Extra APIs: + +``` +POST /api/v1/statuses/{status_id}/make_permanent +GET /api/v1/statuses/{status_id}/signature +``` + +## CLI commands + +Delete profile: + +``` +mitractl delete-profile -i 55a3005f-f293-4168-ab70-6ab09a879679 +``` + +Generate invite code: + +``` +mitractl generate-invite-code +``` + +List generated invites: + +``` +mitractl list-invite-codes +``` + +Generate ethereum address: + +``` +mitractl generate-ethereum-address +``` diff --git a/config.yaml.example b/config.yaml.example new file mode 100644 index 0000000..83977b3 --- /dev/null +++ b/config.yaml.example @@ -0,0 +1,29 @@ +database_url: postgres://mitra:mitra@127.0.0.1:5432/mitra + +http_host: '127.0.0.1' +http_port: 8380 + +# 32 symbols or more +cookie_secret_key: null + +# domain name +instance_uri: myserver.net +instance_title: myserver +instance_short_description: myserver is a federated social network +# Long description can contain markdown syntax +instance_description: myserver is a federated social network +registrations_open: false +# Login message must contain instance URL +login_message: 'Sign this message to log in to https://myserver.net. Do not sign this message on other sites!' + +ethereum_json_rpc_url: 'http://127.0.0.1:8545' +# Block explorer base URL (must be compatible with https://eips.ethereum.org/EIPS/eip-3091) +ethereum_explorer_url: null +ethereum_contract: + address: '0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512' + chain_id: 31337 + signing_key: null + +ipfs_api_url: 'http://127.0.0.1:5001' +# IPFS gateway for clients +ipfs_gateway_url: 'https://ipfs.io' diff --git a/contracts/Collectible.json b/contracts/Collectible.json new file mode 100644 index 0000000..eebb10a --- /dev/null +++ b/contracts/Collectible.json @@ -0,0 +1,433 @@ +{ + "_format": "hh-sol-artifact-1", + "contractName": "Collectible", + "sourceName": "contracts/Collectible.sol", + "abi": [ + { + "inputs": [ + { + "internalType": "string", + "name": "name", + "type": "string" + }, + { + "internalType": "string", + "name": "symbol", + "type": "string" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "approved", + "type": "address" + }, + { + "indexed": true, + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + } + ], + "name": "Approval", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "operator", + "type": "address" + }, + { + "indexed": false, + "internalType": "bool", + "name": "approved", + "type": "bool" + } + ], + "name": "ApprovalForAll", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "previousOwner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "OwnershipTransferred", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "indexed": true, + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + } + ], + "name": "Transfer", + "type": "event" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + } + ], + "name": "approve", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "owner", + "type": "address" + } + ], + "name": "balanceOf", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + } + ], + "name": "getApproved", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "internalType": "address", + "name": "operator", + "type": "address" + } + ], + "name": "isApprovedForAll", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "string", + "name": "tokenURI", + "type": "string" + } + ], + "name": "mint", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "name", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "owner", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + } + ], + "name": "ownerOf", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "renounceOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + } + ], + "name": "safeTransferFrom", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "_data", + "type": "bytes" + } + ], + "name": "safeTransferFrom", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "operator", + "type": "address" + }, + { + "internalType": "bool", + "name": "approved", + "type": "bool" + } + ], + "name": "setApprovalForAll", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes4", + "name": "interfaceId", + "type": "bytes4" + } + ], + "name": "supportsInterface", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "symbol", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + } + ], + "name": "tokenURI", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + } + ], + "name": "transferFrom", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "transferOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + } + ], + "bytecode": "0x60806040523480156200001157600080fd5b50604051620034ac380380620034ac833981810160405281019062000037919062000285565b818181600090805190602001906200005192919062000163565b5080600190805190602001906200006a92919062000163565b5050506200008d620000816200009560201b60201c565b6200009d60201b60201c565b505062000468565b600033905090565b6000600760009054906101000a900473ffffffffffffffffffffffffffffffffffffffff16905081600760006101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff1602179055508173ffffffffffffffffffffffffffffffffffffffff168173ffffffffffffffffffffffffffffffffffffffff167f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e060405160405180910390a35050565b82805462000171906200038d565b90600052602060002090601f016020900481019282620001955760008555620001e1565b82601f10620001b057805160ff1916838001178555620001e1565b82800160010185558215620001e1579182015b82811115620001e0578251825591602001919060010190620001c3565b5b509050620001f09190620001f4565b5090565b5b808211156200020f576000816000905550600101620001f5565b5090565b60006200022a620002248462000321565b620002f8565b9050828152602081018484840111156200024357600080fd5b6200025084828562000357565b509392505050565b600082601f8301126200026a57600080fd5b81516200027c84826020860162000213565b91505092915050565b600080604083850312156200029957600080fd5b600083015167ffffffffffffffff811115620002b457600080fd5b620002c28582860162000258565b925050602083015167ffffffffffffffff811115620002e057600080fd5b620002ee8582860162000258565b9150509250929050565b60006200030462000317565b9050620003128282620003c3565b919050565b6000604051905090565b600067ffffffffffffffff8211156200033f576200033e62000428565b5b6200034a8262000457565b9050602081019050919050565b60005b83811015620003775780820151818401526020810190506200035a565b8381111562000387576000848401525b50505050565b60006002820490506001821680620003a657607f821691505b60208210811415620003bd57620003bc620003f9565b5b50919050565b620003ce8262000457565b810181811067ffffffffffffffff82111715620003f057620003ef62000428565b5b80604052505050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052602260045260246000fd5b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b6000601f19601f8301169050919050565b61303480620004786000396000f3fe608060405234801561001057600080fd5b506004361061010b5760003560e01c8063715018a6116100a2578063b88d4fde11610071578063b88d4fde146102a4578063c87b56dd146102c0578063d0def521146102f0578063e985e9c514610320578063f2fde38b146103505761010b565b8063715018a6146102425780638da5cb5b1461024c57806395d89b411461026a578063a22cb465146102885761010b565b806323b872dd116100de57806323b872dd146101aa57806342842e0e146101c65780636352211e146101e257806370a08231146102125761010b565b806301ffc9a71461011057806306fdde0314610140578063081812fc1461015e578063095ea7b31461018e575b600080fd5b61012a60048036038101906101259190611fe3565b61036c565b604051610137919061242f565b60405180910390f35b61014861044e565b604051610155919061244a565b60405180910390f35b61017860048036038101906101739190612035565b6104e0565b60405161018591906123c8565b60405180910390f35b6101a860048036038101906101a39190611fa7565b610565565b005b6101c460048036038101906101bf9190611e4d565b61067d565b005b6101e060048036038101906101db9190611e4d565b6106dd565b005b6101fc60048036038101906101f79190612035565b6106fd565b60405161020991906123c8565b60405180910390f35b61022c60048036038101906102279190611de8565b6107af565b60405161023991906126ac565b60405180910390f35b61024a610867565b005b6102546108ef565b60405161026191906123c8565b60405180910390f35b610272610919565b60405161027f919061244a565b60405180910390f35b6102a2600480360381019061029d9190611f17565b6109ab565b005b6102be60048036038101906102b99190611e9c565b610b2c565b005b6102da60048036038101906102d59190612035565b610b8e565b6040516102e7919061244a565b60405180910390f35b61030a60048036038101906103059190611f53565b610ce0565b60405161031791906126ac565b60405180910390f35b61033a60048036038101906103359190611e11565b610d94565b604051610347919061242f565b60405180910390f35b61036a60048036038101906103659190611de8565b610e28565b005b60007f80ac58cd000000000000000000000000000000000000000000000000000000007bffffffffffffffffffffffffffffffffffffffffffffffffffffffff1916827bffffffffffffffffffffffffffffffffffffffffffffffffffffffff1916148061043757507f5b5e139f000000000000000000000000000000000000000000000000000000007bffffffffffffffffffffffffffffffffffffffffffffffffffffffff1916827bffffffffffffffffffffffffffffffffffffffffffffffffffffffff1916145b80610447575061044682610f20565b5b9050919050565b60606000805461045d90612902565b80601f016020809104026020016040519081016040528092919081815260200182805461048990612902565b80156104d65780601f106104ab576101008083540402835291602001916104d6565b820191906000526020600020905b8154815290600101906020018083116104b957829003601f168201915b5050505050905090565b60006104eb82610f8a565b61052a576040517f08c379a0000000000000000000000000000000000000000000000000000000008152600401610521906125ec565b60405180910390fd5b6004600083815260200190815260200160002060009054906101000a900473ffffffffffffffffffffffffffffffffffffffff169050919050565b6000610570826106fd565b90508073ffffffffffffffffffffffffffffffffffffffff168373ffffffffffffffffffffffffffffffffffffffff1614156105e1576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004016105d89061266c565b60405180910390fd5b8073ffffffffffffffffffffffffffffffffffffffff16610600610ff6565b73ffffffffffffffffffffffffffffffffffffffff16148061062f575061062e81610629610ff6565b610d94565b5b61066e576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004016106659061252c565b60405180910390fd5b6106788383610ffe565b505050565b61068e610688610ff6565b826110b7565b6106cd576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004016106c49061268c565b60405180910390fd5b6106d8838383611195565b505050565b6106f883838360405180602001604052806000815250610b2c565b505050565b6000806002600084815260200190815260200160002060009054906101000a900473ffffffffffffffffffffffffffffffffffffffff169050600073ffffffffffffffffffffffffffffffffffffffff168173ffffffffffffffffffffffffffffffffffffffff1614156107a6576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040161079d9061256c565b60405180910390fd5b80915050919050565b60008073ffffffffffffffffffffffffffffffffffffffff168273ffffffffffffffffffffffffffffffffffffffff161415610820576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004016108179061254c565b60405180910390fd5b600360008373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020549050919050565b61086f610ff6565b73ffffffffffffffffffffffffffffffffffffffff1661088d6108ef565b73ffffffffffffffffffffffffffffffffffffffff16146108e3576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004016108da9061260c565b60405180910390fd5b6108ed60006113f1565b565b6000600760009054906101000a900473ffffffffffffffffffffffffffffffffffffffff16905090565b60606001805461092890612902565b80601f016020809104026020016040519081016040528092919081815260200182805461095490612902565b80156109a15780601f10610976576101008083540402835291602001916109a1565b820191906000526020600020905b81548152906001019060200180831161098457829003601f168201915b5050505050905090565b6109b3610ff6565b73ffffffffffffffffffffffffffffffffffffffff168273ffffffffffffffffffffffffffffffffffffffff161415610a21576040517f08c379a0000000000000000000000000000000000000000000000000000000008152600401610a18906124ec565b60405180910390fd5b8060056000610a2e610ff6565b73ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060008473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060006101000a81548160ff0219169083151502179055508173ffffffffffffffffffffffffffffffffffffffff16610adb610ff6565b73ffffffffffffffffffffffffffffffffffffffff167f17307eab39ab6107e8899845ad3d59bd9653f200f220920489ca2b5937696c3183604051610b20919061242f565b60405180910390a35050565b610b3d610b37610ff6565b836110b7565b610b7c576040517f08c379a0000000000000000000000000000000000000000000000000000000008152600401610b739061268c565b60405180910390fd5b610b88848484846114b7565b50505050565b6060610b9982610f8a565b610bd8576040517f08c379a0000000000000000000000000000000000000000000000000000000008152600401610bcf906125cc565b60405180910390fd5b6000600660008481526020019081526020016000208054610bf890612902565b80601f0160208091040260200160405190810160405280929190818152602001828054610c2490612902565b8015610c715780601f10610c4657610100808354040283529160200191610c71565b820191906000526020600020905b815481529060010190602001808311610c5457829003601f168201915b505050505090506000610c82611513565b9050600081511415610c98578192505050610cdb565b600082511115610ccd578082604051602001610cb59291906123a4565b60405160208183030381529060405292505050610cdb565b610cd68461152a565b925050505b919050565b6000610cea610ff6565b73ffffffffffffffffffffffffffffffffffffffff16610d086108ef565b73ffffffffffffffffffffffffffffffffffffffff1614610d5e576040517f08c379a0000000000000000000000000000000000000000000000000000000008152600401610d559061260c565b60405180910390fd5b610d6860086115d1565b6000610d7460086115e7565b9050610d8084826115f5565b610d8a8184611613565b8091505092915050565b6000600560008473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060008373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060009054906101000a900460ff16905092915050565b610e30610ff6565b73ffffffffffffffffffffffffffffffffffffffff16610e4e6108ef565b73ffffffffffffffffffffffffffffffffffffffff1614610ea4576040517f08c379a0000000000000000000000000000000000000000000000000000000008152600401610e9b9061260c565b60405180910390fd5b600073ffffffffffffffffffffffffffffffffffffffff168173ffffffffffffffffffffffffffffffffffffffff161415610f14576040517f08c379a0000000000000000000000000000000000000000000000000000000008152600401610f0b9061248c565b60405180910390fd5b610f1d816113f1565b50565b60007f01ffc9a7000000000000000000000000000000000000000000000000000000007bffffffffffffffffffffffffffffffffffffffffffffffffffffffff1916827bffffffffffffffffffffffffffffffffffffffffffffffffffffffff1916149050919050565b60008073ffffffffffffffffffffffffffffffffffffffff166002600084815260200190815260200160002060009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1614159050919050565b600033905090565b816004600083815260200190815260200160002060006101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff160217905550808273ffffffffffffffffffffffffffffffffffffffff16611071836106fd565b73ffffffffffffffffffffffffffffffffffffffff167f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b92560405160405180910390a45050565b60006110c282610f8a565b611101576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004016110f89061250c565b60405180910390fd5b600061110c836106fd565b90508073ffffffffffffffffffffffffffffffffffffffff168473ffffffffffffffffffffffffffffffffffffffff16148061117b57508373ffffffffffffffffffffffffffffffffffffffff16611163846104e0565b73ffffffffffffffffffffffffffffffffffffffff16145b8061118c575061118b8185610d94565b5b91505092915050565b8273ffffffffffffffffffffffffffffffffffffffff166111b5826106fd565b73ffffffffffffffffffffffffffffffffffffffff161461120b576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004016112029061262c565b60405180910390fd5b600073ffffffffffffffffffffffffffffffffffffffff168273ffffffffffffffffffffffffffffffffffffffff16141561127b576040517f08c379a0000000000000000000000000000000000000000000000000000000008152600401611272906124cc565b60405180910390fd5b611286838383611687565b611291600082610ffe565b6001600360008573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060008282546112e19190612818565b925050819055506001600360008473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060008282546113389190612791565b92505081905550816002600083815260200190815260200160002060006101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff160217905550808273ffffffffffffffffffffffffffffffffffffffff168473ffffffffffffffffffffffffffffffffffffffff167fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef60405160405180910390a4505050565b6000600760009054906101000a900473ffffffffffffffffffffffffffffffffffffffff16905081600760006101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff1602179055508173ffffffffffffffffffffffffffffffffffffffff168173ffffffffffffffffffffffffffffffffffffffff167f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e060405160405180910390a35050565b6114c2848484611195565b6114ce8484848461168c565b61150d576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004016115049061246c565b60405180910390fd5b50505050565b606060405180602001604052806000815250905090565b606061153582610f8a565b611574576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040161156b9061264c565b60405180910390fd5b600061157e611513565b9050600081511161159e57604051806020016040528060008152506115c9565b806115a884611823565b6040516020016115b99291906123a4565b6040516020818303038152906040525b915050919050565b6001816000016000828254019250508190555050565b600081600001549050919050565b61160f8282604051806020016040528060008152506119d0565b5050565b61161c82610f8a565b61165b576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004016116529061258c565b60405180910390fd5b80600660008481526020019081526020016000209080519060200190611682929190611c0c565b505050565b505050565b60006116ad8473ffffffffffffffffffffffffffffffffffffffff16611a2b565b15611816578373ffffffffffffffffffffffffffffffffffffffff1663150b7a026116d6610ff6565b8786866040518563ffffffff1660e01b81526004016116f894939291906123e3565b602060405180830381600087803b15801561171257600080fd5b505af192505050801561174357506040513d601f19601f82011682018060405250810190611740919061200c565b60015b6117c6573d8060008114611773576040519150601f19603f3d011682016040523d82523d6000602084013e611778565b606091505b506000815114156117be576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004016117b59061246c565b60405180910390fd5b805181602001fd5b63150b7a0260e01b7bffffffffffffffffffffffffffffffffffffffffffffffffffffffff1916817bffffffffffffffffffffffffffffffffffffffffffffffffffffffff19161491505061181b565b600190505b949350505050565b6060600082141561186b576040518060400160405280600181526020017f300000000000000000000000000000000000000000000000000000000000000081525090506119cb565b600082905060005b6000821461189d57808061188690612965565b915050600a8261189691906127e7565b9150611873565b60008167ffffffffffffffff8111156118df577f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b6040519080825280601f01601f1916602001820160405280156119115781602001600182028036833780820191505090505b5090505b600085146119c45760018261192a9190612818565b9150600a8561193991906129ae565b60306119459190612791565b60f81b818381518110611981577f4e487b7100000000000000000000000000000000000000000000000000000000600052603260045260246000fd5b60200101907effffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff1916908160001a905350600a856119bd91906127e7565b9450611915565b8093505050505b919050565b6119da8383611a3e565b6119e7600084848461168c565b611a26576040517f08c379a0000000000000000000000000000000000000000000000000000000008152600401611a1d9061246c565b60405180910390fd5b505050565b600080823b905060008111915050919050565b600073ffffffffffffffffffffffffffffffffffffffff168273ffffffffffffffffffffffffffffffffffffffff161415611aae576040517f08c379a0000000000000000000000000000000000000000000000000000000008152600401611aa5906125ac565b60405180910390fd5b611ab781610f8a565b15611af7576040517f08c379a0000000000000000000000000000000000000000000000000000000008152600401611aee906124ac565b60405180910390fd5b611b0360008383611687565b6001600360008473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020016000206000828254611b539190612791565b92505081905550816002600083815260200190815260200160002060006101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff160217905550808273ffffffffffffffffffffffffffffffffffffffff16600073ffffffffffffffffffffffffffffffffffffffff167fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef60405160405180910390a45050565b828054611c1890612902565b90600052602060002090601f016020900481019282611c3a5760008555611c81565b82601f10611c5357805160ff1916838001178555611c81565b82800160010185558215611c81579182015b82811115611c80578251825591602001919060010190611c65565b5b509050611c8e9190611c92565b5090565b5b80821115611cab576000816000905550600101611c93565b5090565b6000611cc2611cbd846126ec565b6126c7565b905082815260208101848484011115611cda57600080fd5b611ce58482856128c0565b509392505050565b6000611d00611cfb8461271d565b6126c7565b905082815260208101848484011115611d1857600080fd5b611d238482856128c0565b509392505050565b600081359050611d3a81612fa2565b92915050565b600081359050611d4f81612fb9565b92915050565b600081359050611d6481612fd0565b92915050565b600081519050611d7981612fd0565b92915050565b600082601f830112611d9057600080fd5b8135611da0848260208601611caf565b91505092915050565b600082601f830112611dba57600080fd5b8135611dca848260208601611ced565b91505092915050565b600081359050611de281612fe7565b92915050565b600060208284031215611dfa57600080fd5b6000611e0884828501611d2b565b91505092915050565b60008060408385031215611e2457600080fd5b6000611e3285828601611d2b565b9250506020611e4385828601611d2b565b9150509250929050565b600080600060608486031215611e6257600080fd5b6000611e7086828701611d2b565b9350506020611e8186828701611d2b565b9250506040611e9286828701611dd3565b9150509250925092565b60008060008060808587031215611eb257600080fd5b6000611ec087828801611d2b565b9450506020611ed187828801611d2b565b9350506040611ee287828801611dd3565b925050606085013567ffffffffffffffff811115611eff57600080fd5b611f0b87828801611d7f565b91505092959194509250565b60008060408385031215611f2a57600080fd5b6000611f3885828601611d2b565b9250506020611f4985828601611d40565b9150509250929050565b60008060408385031215611f6657600080fd5b6000611f7485828601611d2b565b925050602083013567ffffffffffffffff811115611f9157600080fd5b611f9d85828601611da9565b9150509250929050565b60008060408385031215611fba57600080fd5b6000611fc885828601611d2b565b9250506020611fd985828601611dd3565b9150509250929050565b600060208284031215611ff557600080fd5b600061200384828501611d55565b91505092915050565b60006020828403121561201e57600080fd5b600061202c84828501611d6a565b91505092915050565b60006020828403121561204757600080fd5b600061205584828501611dd3565b91505092915050565b6120678161284c565b82525050565b6120768161285e565b82525050565b60006120878261274e565b6120918185612764565b93506120a18185602086016128cf565b6120aa81612a9b565b840191505092915050565b60006120c082612759565b6120ca8185612775565b93506120da8185602086016128cf565b6120e381612a9b565b840191505092915050565b60006120f982612759565b6121038185612786565b93506121138185602086016128cf565b80840191505092915050565b600061212c603283612775565b915061213782612aac565b604082019050919050565b600061214f602683612775565b915061215a82612afb565b604082019050919050565b6000612172601c83612775565b915061217d82612b4a565b602082019050919050565b6000612195602483612775565b91506121a082612b73565b604082019050919050565b60006121b8601983612775565b91506121c382612bc2565b602082019050919050565b60006121db602c83612775565b91506121e682612beb565b604082019050919050565b60006121fe603883612775565b915061220982612c3a565b604082019050919050565b6000612221602a83612775565b915061222c82612c89565b604082019050919050565b6000612244602983612775565b915061224f82612cd8565b604082019050919050565b6000612267602e83612775565b915061227282612d27565b604082019050919050565b600061228a602083612775565b915061229582612d76565b602082019050919050565b60006122ad603183612775565b91506122b882612d9f565b604082019050919050565b60006122d0602c83612775565b91506122db82612dee565b604082019050919050565b60006122f3602083612775565b91506122fe82612e3d565b602082019050919050565b6000612316602983612775565b915061232182612e66565b604082019050919050565b6000612339602f83612775565b915061234482612eb5565b604082019050919050565b600061235c602183612775565b915061236782612f04565b604082019050919050565b600061237f603183612775565b915061238a82612f53565b604082019050919050565b61239e816128b6565b82525050565b60006123b082856120ee565b91506123bc82846120ee565b91508190509392505050565b60006020820190506123dd600083018461205e565b92915050565b60006080820190506123f8600083018761205e565b612405602083018661205e565b6124126040830185612395565b8181036060830152612424818461207c565b905095945050505050565b6000602082019050612444600083018461206d565b92915050565b6000602082019050818103600083015261246481846120b5565b905092915050565b600060208201905081810360008301526124858161211f565b9050919050565b600060208201905081810360008301526124a581612142565b9050919050565b600060208201905081810360008301526124c581612165565b9050919050565b600060208201905081810360008301526124e581612188565b9050919050565b60006020820190508181036000830152612505816121ab565b9050919050565b60006020820190508181036000830152612525816121ce565b9050919050565b60006020820190508181036000830152612545816121f1565b9050919050565b6000602082019050818103600083015261256581612214565b9050919050565b6000602082019050818103600083015261258581612237565b9050919050565b600060208201905081810360008301526125a58161225a565b9050919050565b600060208201905081810360008301526125c58161227d565b9050919050565b600060208201905081810360008301526125e5816122a0565b9050919050565b60006020820190508181036000830152612605816122c3565b9050919050565b60006020820190508181036000830152612625816122e6565b9050919050565b6000602082019050818103600083015261264581612309565b9050919050565b600060208201905081810360008301526126658161232c565b9050919050565b600060208201905081810360008301526126858161234f565b9050919050565b600060208201905081810360008301526126a581612372565b9050919050565b60006020820190506126c16000830184612395565b92915050565b60006126d16126e2565b90506126dd8282612934565b919050565b6000604051905090565b600067ffffffffffffffff82111561270757612706612a6c565b5b61271082612a9b565b9050602081019050919050565b600067ffffffffffffffff82111561273857612737612a6c565b5b61274182612a9b565b9050602081019050919050565b600081519050919050565b600081519050919050565b600082825260208201905092915050565b600082825260208201905092915050565b600081905092915050565b600061279c826128b6565b91506127a7836128b6565b9250827fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff038211156127dc576127db6129df565b5b828201905092915050565b60006127f2826128b6565b91506127fd836128b6565b92508261280d5761280c612a0e565b5b828204905092915050565b6000612823826128b6565b915061282e836128b6565b925082821015612841576128406129df565b5b828203905092915050565b600061285782612896565b9050919050565b60008115159050919050565b60007fffffffff0000000000000000000000000000000000000000000000000000000082169050919050565b600073ffffffffffffffffffffffffffffffffffffffff82169050919050565b6000819050919050565b82818337600083830152505050565b60005b838110156128ed5780820151818401526020810190506128d2565b838111156128fc576000848401525b50505050565b6000600282049050600182168061291a57607f821691505b6020821081141561292e5761292d612a3d565b5b50919050565b61293d82612a9b565b810181811067ffffffffffffffff8211171561295c5761295b612a6c565b5b80604052505050565b6000612970826128b6565b91507fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8214156129a3576129a26129df565b5b600182019050919050565b60006129b9826128b6565b91506129c4836128b6565b9250826129d4576129d3612a0e565b5b828206905092915050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601260045260246000fd5b7f4e487b7100000000000000000000000000000000000000000000000000000000600052602260045260246000fd5b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b6000601f19601f8301169050919050565b7f4552433732313a207472616e7366657220746f206e6f6e20455243373231526560008201527f63656976657220696d706c656d656e7465720000000000000000000000000000602082015250565b7f4f776e61626c653a206e6577206f776e657220697320746865207a65726f206160008201527f6464726573730000000000000000000000000000000000000000000000000000602082015250565b7f4552433732313a20746f6b656e20616c7265616479206d696e74656400000000600082015250565b7f4552433732313a207472616e7366657220746f20746865207a65726f2061646460008201527f7265737300000000000000000000000000000000000000000000000000000000602082015250565b7f4552433732313a20617070726f766520746f2063616c6c657200000000000000600082015250565b7f4552433732313a206f70657261746f7220717565727920666f72206e6f6e657860008201527f697374656e7420746f6b656e0000000000000000000000000000000000000000602082015250565b7f4552433732313a20617070726f76652063616c6c6572206973206e6f74206f7760008201527f6e6572206e6f7220617070726f76656420666f7220616c6c0000000000000000602082015250565b7f4552433732313a2062616c616e636520717565727920666f7220746865207a6560008201527f726f206164647265737300000000000000000000000000000000000000000000602082015250565b7f4552433732313a206f776e657220717565727920666f72206e6f6e657869737460008201527f656e7420746f6b656e0000000000000000000000000000000000000000000000602082015250565b7f45524337323155524953746f726167653a2055524920736574206f66206e6f6e60008201527f6578697374656e7420746f6b656e000000000000000000000000000000000000602082015250565b7f4552433732313a206d696e7420746f20746865207a65726f2061646472657373600082015250565b7f45524337323155524953746f726167653a2055524920717565727920666f722060008201527f6e6f6e6578697374656e7420746f6b656e000000000000000000000000000000602082015250565b7f4552433732313a20617070726f76656420717565727920666f72206e6f6e657860008201527f697374656e7420746f6b656e0000000000000000000000000000000000000000602082015250565b7f4f776e61626c653a2063616c6c6572206973206e6f7420746865206f776e6572600082015250565b7f4552433732313a207472616e73666572206f6620746f6b656e2074686174206960008201527f73206e6f74206f776e0000000000000000000000000000000000000000000000602082015250565b7f4552433732314d657461646174613a2055524920717565727920666f72206e6f60008201527f6e6578697374656e7420746f6b656e0000000000000000000000000000000000602082015250565b7f4552433732313a20617070726f76616c20746f2063757272656e74206f776e6560008201527f7200000000000000000000000000000000000000000000000000000000000000602082015250565b7f4552433732313a207472616e736665722063616c6c6572206973206e6f74206f60008201527f776e6572206e6f7220617070726f766564000000000000000000000000000000602082015250565b612fab8161284c565b8114612fb657600080fd5b50565b612fc28161285e565b8114612fcd57600080fd5b50565b612fd98161286a565b8114612fe457600080fd5b50565b612ff0816128b6565b8114612ffb57600080fd5b5056fea264697066735822122040b960152888b1f29af47e31e81f007992dea5a4cd4a6983e1a6318737bc3db564736f6c63430008040033", + "deployedBytecode": "0x608060405234801561001057600080fd5b506004361061010b5760003560e01c8063715018a6116100a2578063b88d4fde11610071578063b88d4fde146102a4578063c87b56dd146102c0578063d0def521146102f0578063e985e9c514610320578063f2fde38b146103505761010b565b8063715018a6146102425780638da5cb5b1461024c57806395d89b411461026a578063a22cb465146102885761010b565b806323b872dd116100de57806323b872dd146101aa57806342842e0e146101c65780636352211e146101e257806370a08231146102125761010b565b806301ffc9a71461011057806306fdde0314610140578063081812fc1461015e578063095ea7b31461018e575b600080fd5b61012a60048036038101906101259190611fe3565b61036c565b604051610137919061242f565b60405180910390f35b61014861044e565b604051610155919061244a565b60405180910390f35b61017860048036038101906101739190612035565b6104e0565b60405161018591906123c8565b60405180910390f35b6101a860048036038101906101a39190611fa7565b610565565b005b6101c460048036038101906101bf9190611e4d565b61067d565b005b6101e060048036038101906101db9190611e4d565b6106dd565b005b6101fc60048036038101906101f79190612035565b6106fd565b60405161020991906123c8565b60405180910390f35b61022c60048036038101906102279190611de8565b6107af565b60405161023991906126ac565b60405180910390f35b61024a610867565b005b6102546108ef565b60405161026191906123c8565b60405180910390f35b610272610919565b60405161027f919061244a565b60405180910390f35b6102a2600480360381019061029d9190611f17565b6109ab565b005b6102be60048036038101906102b99190611e9c565b610b2c565b005b6102da60048036038101906102d59190612035565b610b8e565b6040516102e7919061244a565b60405180910390f35b61030a60048036038101906103059190611f53565b610ce0565b60405161031791906126ac565b60405180910390f35b61033a60048036038101906103359190611e11565b610d94565b604051610347919061242f565b60405180910390f35b61036a60048036038101906103659190611de8565b610e28565b005b60007f80ac58cd000000000000000000000000000000000000000000000000000000007bffffffffffffffffffffffffffffffffffffffffffffffffffffffff1916827bffffffffffffffffffffffffffffffffffffffffffffffffffffffff1916148061043757507f5b5e139f000000000000000000000000000000000000000000000000000000007bffffffffffffffffffffffffffffffffffffffffffffffffffffffff1916827bffffffffffffffffffffffffffffffffffffffffffffffffffffffff1916145b80610447575061044682610f20565b5b9050919050565b60606000805461045d90612902565b80601f016020809104026020016040519081016040528092919081815260200182805461048990612902565b80156104d65780601f106104ab576101008083540402835291602001916104d6565b820191906000526020600020905b8154815290600101906020018083116104b957829003601f168201915b5050505050905090565b60006104eb82610f8a565b61052a576040517f08c379a0000000000000000000000000000000000000000000000000000000008152600401610521906125ec565b60405180910390fd5b6004600083815260200190815260200160002060009054906101000a900473ffffffffffffffffffffffffffffffffffffffff169050919050565b6000610570826106fd565b90508073ffffffffffffffffffffffffffffffffffffffff168373ffffffffffffffffffffffffffffffffffffffff1614156105e1576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004016105d89061266c565b60405180910390fd5b8073ffffffffffffffffffffffffffffffffffffffff16610600610ff6565b73ffffffffffffffffffffffffffffffffffffffff16148061062f575061062e81610629610ff6565b610d94565b5b61066e576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004016106659061252c565b60405180910390fd5b6106788383610ffe565b505050565b61068e610688610ff6565b826110b7565b6106cd576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004016106c49061268c565b60405180910390fd5b6106d8838383611195565b505050565b6106f883838360405180602001604052806000815250610b2c565b505050565b6000806002600084815260200190815260200160002060009054906101000a900473ffffffffffffffffffffffffffffffffffffffff169050600073ffffffffffffffffffffffffffffffffffffffff168173ffffffffffffffffffffffffffffffffffffffff1614156107a6576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040161079d9061256c565b60405180910390fd5b80915050919050565b60008073ffffffffffffffffffffffffffffffffffffffff168273ffffffffffffffffffffffffffffffffffffffff161415610820576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004016108179061254c565b60405180910390fd5b600360008373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020549050919050565b61086f610ff6565b73ffffffffffffffffffffffffffffffffffffffff1661088d6108ef565b73ffffffffffffffffffffffffffffffffffffffff16146108e3576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004016108da9061260c565b60405180910390fd5b6108ed60006113f1565b565b6000600760009054906101000a900473ffffffffffffffffffffffffffffffffffffffff16905090565b60606001805461092890612902565b80601f016020809104026020016040519081016040528092919081815260200182805461095490612902565b80156109a15780601f10610976576101008083540402835291602001916109a1565b820191906000526020600020905b81548152906001019060200180831161098457829003601f168201915b5050505050905090565b6109b3610ff6565b73ffffffffffffffffffffffffffffffffffffffff168273ffffffffffffffffffffffffffffffffffffffff161415610a21576040517f08c379a0000000000000000000000000000000000000000000000000000000008152600401610a18906124ec565b60405180910390fd5b8060056000610a2e610ff6565b73ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060008473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060006101000a81548160ff0219169083151502179055508173ffffffffffffffffffffffffffffffffffffffff16610adb610ff6565b73ffffffffffffffffffffffffffffffffffffffff167f17307eab39ab6107e8899845ad3d59bd9653f200f220920489ca2b5937696c3183604051610b20919061242f565b60405180910390a35050565b610b3d610b37610ff6565b836110b7565b610b7c576040517f08c379a0000000000000000000000000000000000000000000000000000000008152600401610b739061268c565b60405180910390fd5b610b88848484846114b7565b50505050565b6060610b9982610f8a565b610bd8576040517f08c379a0000000000000000000000000000000000000000000000000000000008152600401610bcf906125cc565b60405180910390fd5b6000600660008481526020019081526020016000208054610bf890612902565b80601f0160208091040260200160405190810160405280929190818152602001828054610c2490612902565b8015610c715780601f10610c4657610100808354040283529160200191610c71565b820191906000526020600020905b815481529060010190602001808311610c5457829003601f168201915b505050505090506000610c82611513565b9050600081511415610c98578192505050610cdb565b600082511115610ccd578082604051602001610cb59291906123a4565b60405160208183030381529060405292505050610cdb565b610cd68461152a565b925050505b919050565b6000610cea610ff6565b73ffffffffffffffffffffffffffffffffffffffff16610d086108ef565b73ffffffffffffffffffffffffffffffffffffffff1614610d5e576040517f08c379a0000000000000000000000000000000000000000000000000000000008152600401610d559061260c565b60405180910390fd5b610d6860086115d1565b6000610d7460086115e7565b9050610d8084826115f5565b610d8a8184611613565b8091505092915050565b6000600560008473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060008373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060009054906101000a900460ff16905092915050565b610e30610ff6565b73ffffffffffffffffffffffffffffffffffffffff16610e4e6108ef565b73ffffffffffffffffffffffffffffffffffffffff1614610ea4576040517f08c379a0000000000000000000000000000000000000000000000000000000008152600401610e9b9061260c565b60405180910390fd5b600073ffffffffffffffffffffffffffffffffffffffff168173ffffffffffffffffffffffffffffffffffffffff161415610f14576040517f08c379a0000000000000000000000000000000000000000000000000000000008152600401610f0b9061248c565b60405180910390fd5b610f1d816113f1565b50565b60007f01ffc9a7000000000000000000000000000000000000000000000000000000007bffffffffffffffffffffffffffffffffffffffffffffffffffffffff1916827bffffffffffffffffffffffffffffffffffffffffffffffffffffffff1916149050919050565b60008073ffffffffffffffffffffffffffffffffffffffff166002600084815260200190815260200160002060009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1614159050919050565b600033905090565b816004600083815260200190815260200160002060006101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff160217905550808273ffffffffffffffffffffffffffffffffffffffff16611071836106fd565b73ffffffffffffffffffffffffffffffffffffffff167f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b92560405160405180910390a45050565b60006110c282610f8a565b611101576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004016110f89061250c565b60405180910390fd5b600061110c836106fd565b90508073ffffffffffffffffffffffffffffffffffffffff168473ffffffffffffffffffffffffffffffffffffffff16148061117b57508373ffffffffffffffffffffffffffffffffffffffff16611163846104e0565b73ffffffffffffffffffffffffffffffffffffffff16145b8061118c575061118b8185610d94565b5b91505092915050565b8273ffffffffffffffffffffffffffffffffffffffff166111b5826106fd565b73ffffffffffffffffffffffffffffffffffffffff161461120b576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004016112029061262c565b60405180910390fd5b600073ffffffffffffffffffffffffffffffffffffffff168273ffffffffffffffffffffffffffffffffffffffff16141561127b576040517f08c379a0000000000000000000000000000000000000000000000000000000008152600401611272906124cc565b60405180910390fd5b611286838383611687565b611291600082610ffe565b6001600360008573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060008282546112e19190612818565b925050819055506001600360008473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060008282546113389190612791565b92505081905550816002600083815260200190815260200160002060006101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff160217905550808273ffffffffffffffffffffffffffffffffffffffff168473ffffffffffffffffffffffffffffffffffffffff167fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef60405160405180910390a4505050565b6000600760009054906101000a900473ffffffffffffffffffffffffffffffffffffffff16905081600760006101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff1602179055508173ffffffffffffffffffffffffffffffffffffffff168173ffffffffffffffffffffffffffffffffffffffff167f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e060405160405180910390a35050565b6114c2848484611195565b6114ce8484848461168c565b61150d576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004016115049061246c565b60405180910390fd5b50505050565b606060405180602001604052806000815250905090565b606061153582610f8a565b611574576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040161156b9061264c565b60405180910390fd5b600061157e611513565b9050600081511161159e57604051806020016040528060008152506115c9565b806115a884611823565b6040516020016115b99291906123a4565b6040516020818303038152906040525b915050919050565b6001816000016000828254019250508190555050565b600081600001549050919050565b61160f8282604051806020016040528060008152506119d0565b5050565b61161c82610f8a565b61165b576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004016116529061258c565b60405180910390fd5b80600660008481526020019081526020016000209080519060200190611682929190611c0c565b505050565b505050565b60006116ad8473ffffffffffffffffffffffffffffffffffffffff16611a2b565b15611816578373ffffffffffffffffffffffffffffffffffffffff1663150b7a026116d6610ff6565b8786866040518563ffffffff1660e01b81526004016116f894939291906123e3565b602060405180830381600087803b15801561171257600080fd5b505af192505050801561174357506040513d601f19601f82011682018060405250810190611740919061200c565b60015b6117c6573d8060008114611773576040519150601f19603f3d011682016040523d82523d6000602084013e611778565b606091505b506000815114156117be576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004016117b59061246c565b60405180910390fd5b805181602001fd5b63150b7a0260e01b7bffffffffffffffffffffffffffffffffffffffffffffffffffffffff1916817bffffffffffffffffffffffffffffffffffffffffffffffffffffffff19161491505061181b565b600190505b949350505050565b6060600082141561186b576040518060400160405280600181526020017f300000000000000000000000000000000000000000000000000000000000000081525090506119cb565b600082905060005b6000821461189d57808061188690612965565b915050600a8261189691906127e7565b9150611873565b60008167ffffffffffffffff8111156118df577f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b6040519080825280601f01601f1916602001820160405280156119115781602001600182028036833780820191505090505b5090505b600085146119c45760018261192a9190612818565b9150600a8561193991906129ae565b60306119459190612791565b60f81b818381518110611981577f4e487b7100000000000000000000000000000000000000000000000000000000600052603260045260246000fd5b60200101907effffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff1916908160001a905350600a856119bd91906127e7565b9450611915565b8093505050505b919050565b6119da8383611a3e565b6119e7600084848461168c565b611a26576040517f08c379a0000000000000000000000000000000000000000000000000000000008152600401611a1d9061246c565b60405180910390fd5b505050565b600080823b905060008111915050919050565b600073ffffffffffffffffffffffffffffffffffffffff168273ffffffffffffffffffffffffffffffffffffffff161415611aae576040517f08c379a0000000000000000000000000000000000000000000000000000000008152600401611aa5906125ac565b60405180910390fd5b611ab781610f8a565b15611af7576040517f08c379a0000000000000000000000000000000000000000000000000000000008152600401611aee906124ac565b60405180910390fd5b611b0360008383611687565b6001600360008473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020016000206000828254611b539190612791565b92505081905550816002600083815260200190815260200160002060006101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff160217905550808273ffffffffffffffffffffffffffffffffffffffff16600073ffffffffffffffffffffffffffffffffffffffff167fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef60405160405180910390a45050565b828054611c1890612902565b90600052602060002090601f016020900481019282611c3a5760008555611c81565b82601f10611c5357805160ff1916838001178555611c81565b82800160010185558215611c81579182015b82811115611c80578251825591602001919060010190611c65565b5b509050611c8e9190611c92565b5090565b5b80821115611cab576000816000905550600101611c93565b5090565b6000611cc2611cbd846126ec565b6126c7565b905082815260208101848484011115611cda57600080fd5b611ce58482856128c0565b509392505050565b6000611d00611cfb8461271d565b6126c7565b905082815260208101848484011115611d1857600080fd5b611d238482856128c0565b509392505050565b600081359050611d3a81612fa2565b92915050565b600081359050611d4f81612fb9565b92915050565b600081359050611d6481612fd0565b92915050565b600081519050611d7981612fd0565b92915050565b600082601f830112611d9057600080fd5b8135611da0848260208601611caf565b91505092915050565b600082601f830112611dba57600080fd5b8135611dca848260208601611ced565b91505092915050565b600081359050611de281612fe7565b92915050565b600060208284031215611dfa57600080fd5b6000611e0884828501611d2b565b91505092915050565b60008060408385031215611e2457600080fd5b6000611e3285828601611d2b565b9250506020611e4385828601611d2b565b9150509250929050565b600080600060608486031215611e6257600080fd5b6000611e7086828701611d2b565b9350506020611e8186828701611d2b565b9250506040611e9286828701611dd3565b9150509250925092565b60008060008060808587031215611eb257600080fd5b6000611ec087828801611d2b565b9450506020611ed187828801611d2b565b9350506040611ee287828801611dd3565b925050606085013567ffffffffffffffff811115611eff57600080fd5b611f0b87828801611d7f565b91505092959194509250565b60008060408385031215611f2a57600080fd5b6000611f3885828601611d2b565b9250506020611f4985828601611d40565b9150509250929050565b60008060408385031215611f6657600080fd5b6000611f7485828601611d2b565b925050602083013567ffffffffffffffff811115611f9157600080fd5b611f9d85828601611da9565b9150509250929050565b60008060408385031215611fba57600080fd5b6000611fc885828601611d2b565b9250506020611fd985828601611dd3565b9150509250929050565b600060208284031215611ff557600080fd5b600061200384828501611d55565b91505092915050565b60006020828403121561201e57600080fd5b600061202c84828501611d6a565b91505092915050565b60006020828403121561204757600080fd5b600061205584828501611dd3565b91505092915050565b6120678161284c565b82525050565b6120768161285e565b82525050565b60006120878261274e565b6120918185612764565b93506120a18185602086016128cf565b6120aa81612a9b565b840191505092915050565b60006120c082612759565b6120ca8185612775565b93506120da8185602086016128cf565b6120e381612a9b565b840191505092915050565b60006120f982612759565b6121038185612786565b93506121138185602086016128cf565b80840191505092915050565b600061212c603283612775565b915061213782612aac565b604082019050919050565b600061214f602683612775565b915061215a82612afb565b604082019050919050565b6000612172601c83612775565b915061217d82612b4a565b602082019050919050565b6000612195602483612775565b91506121a082612b73565b604082019050919050565b60006121b8601983612775565b91506121c382612bc2565b602082019050919050565b60006121db602c83612775565b91506121e682612beb565b604082019050919050565b60006121fe603883612775565b915061220982612c3a565b604082019050919050565b6000612221602a83612775565b915061222c82612c89565b604082019050919050565b6000612244602983612775565b915061224f82612cd8565b604082019050919050565b6000612267602e83612775565b915061227282612d27565b604082019050919050565b600061228a602083612775565b915061229582612d76565b602082019050919050565b60006122ad603183612775565b91506122b882612d9f565b604082019050919050565b60006122d0602c83612775565b91506122db82612dee565b604082019050919050565b60006122f3602083612775565b91506122fe82612e3d565b602082019050919050565b6000612316602983612775565b915061232182612e66565b604082019050919050565b6000612339602f83612775565b915061234482612eb5565b604082019050919050565b600061235c602183612775565b915061236782612f04565b604082019050919050565b600061237f603183612775565b915061238a82612f53565b604082019050919050565b61239e816128b6565b82525050565b60006123b082856120ee565b91506123bc82846120ee565b91508190509392505050565b60006020820190506123dd600083018461205e565b92915050565b60006080820190506123f8600083018761205e565b612405602083018661205e565b6124126040830185612395565b8181036060830152612424818461207c565b905095945050505050565b6000602082019050612444600083018461206d565b92915050565b6000602082019050818103600083015261246481846120b5565b905092915050565b600060208201905081810360008301526124858161211f565b9050919050565b600060208201905081810360008301526124a581612142565b9050919050565b600060208201905081810360008301526124c581612165565b9050919050565b600060208201905081810360008301526124e581612188565b9050919050565b60006020820190508181036000830152612505816121ab565b9050919050565b60006020820190508181036000830152612525816121ce565b9050919050565b60006020820190508181036000830152612545816121f1565b9050919050565b6000602082019050818103600083015261256581612214565b9050919050565b6000602082019050818103600083015261258581612237565b9050919050565b600060208201905081810360008301526125a58161225a565b9050919050565b600060208201905081810360008301526125c58161227d565b9050919050565b600060208201905081810360008301526125e5816122a0565b9050919050565b60006020820190508181036000830152612605816122c3565b9050919050565b60006020820190508181036000830152612625816122e6565b9050919050565b6000602082019050818103600083015261264581612309565b9050919050565b600060208201905081810360008301526126658161232c565b9050919050565b600060208201905081810360008301526126858161234f565b9050919050565b600060208201905081810360008301526126a581612372565b9050919050565b60006020820190506126c16000830184612395565b92915050565b60006126d16126e2565b90506126dd8282612934565b919050565b6000604051905090565b600067ffffffffffffffff82111561270757612706612a6c565b5b61271082612a9b565b9050602081019050919050565b600067ffffffffffffffff82111561273857612737612a6c565b5b61274182612a9b565b9050602081019050919050565b600081519050919050565b600081519050919050565b600082825260208201905092915050565b600082825260208201905092915050565b600081905092915050565b600061279c826128b6565b91506127a7836128b6565b9250827fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff038211156127dc576127db6129df565b5b828201905092915050565b60006127f2826128b6565b91506127fd836128b6565b92508261280d5761280c612a0e565b5b828204905092915050565b6000612823826128b6565b915061282e836128b6565b925082821015612841576128406129df565b5b828203905092915050565b600061285782612896565b9050919050565b60008115159050919050565b60007fffffffff0000000000000000000000000000000000000000000000000000000082169050919050565b600073ffffffffffffffffffffffffffffffffffffffff82169050919050565b6000819050919050565b82818337600083830152505050565b60005b838110156128ed5780820151818401526020810190506128d2565b838111156128fc576000848401525b50505050565b6000600282049050600182168061291a57607f821691505b6020821081141561292e5761292d612a3d565b5b50919050565b61293d82612a9b565b810181811067ffffffffffffffff8211171561295c5761295b612a6c565b5b80604052505050565b6000612970826128b6565b91507fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8214156129a3576129a26129df565b5b600182019050919050565b60006129b9826128b6565b91506129c4836128b6565b9250826129d4576129d3612a0e565b5b828206905092915050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601260045260246000fd5b7f4e487b7100000000000000000000000000000000000000000000000000000000600052602260045260246000fd5b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b6000601f19601f8301169050919050565b7f4552433732313a207472616e7366657220746f206e6f6e20455243373231526560008201527f63656976657220696d706c656d656e7465720000000000000000000000000000602082015250565b7f4f776e61626c653a206e6577206f776e657220697320746865207a65726f206160008201527f6464726573730000000000000000000000000000000000000000000000000000602082015250565b7f4552433732313a20746f6b656e20616c7265616479206d696e74656400000000600082015250565b7f4552433732313a207472616e7366657220746f20746865207a65726f2061646460008201527f7265737300000000000000000000000000000000000000000000000000000000602082015250565b7f4552433732313a20617070726f766520746f2063616c6c657200000000000000600082015250565b7f4552433732313a206f70657261746f7220717565727920666f72206e6f6e657860008201527f697374656e7420746f6b656e0000000000000000000000000000000000000000602082015250565b7f4552433732313a20617070726f76652063616c6c6572206973206e6f74206f7760008201527f6e6572206e6f7220617070726f76656420666f7220616c6c0000000000000000602082015250565b7f4552433732313a2062616c616e636520717565727920666f7220746865207a6560008201527f726f206164647265737300000000000000000000000000000000000000000000602082015250565b7f4552433732313a206f776e657220717565727920666f72206e6f6e657869737460008201527f656e7420746f6b656e0000000000000000000000000000000000000000000000602082015250565b7f45524337323155524953746f726167653a2055524920736574206f66206e6f6e60008201527f6578697374656e7420746f6b656e000000000000000000000000000000000000602082015250565b7f4552433732313a206d696e7420746f20746865207a65726f2061646472657373600082015250565b7f45524337323155524953746f726167653a2055524920717565727920666f722060008201527f6e6f6e6578697374656e7420746f6b656e000000000000000000000000000000602082015250565b7f4552433732313a20617070726f76656420717565727920666f72206e6f6e657860008201527f697374656e7420746f6b656e0000000000000000000000000000000000000000602082015250565b7f4f776e61626c653a2063616c6c6572206973206e6f7420746865206f776e6572600082015250565b7f4552433732313a207472616e73666572206f6620746f6b656e2074686174206960008201527f73206e6f74206f776e0000000000000000000000000000000000000000000000602082015250565b7f4552433732314d657461646174613a2055524920717565727920666f72206e6f60008201527f6e6578697374656e7420746f6b656e0000000000000000000000000000000000602082015250565b7f4552433732313a20617070726f76616c20746f2063757272656e74206f776e6560008201527f7200000000000000000000000000000000000000000000000000000000000000602082015250565b7f4552433732313a207472616e736665722063616c6c6572206973206e6f74206f60008201527f776e6572206e6f7220617070726f766564000000000000000000000000000000602082015250565b612fab8161284c565b8114612fb657600080fd5b50565b612fc28161285e565b8114612fcd57600080fd5b50565b612fd98161286a565b8114612fe457600080fd5b50565b612ff0816128b6565b8114612ffb57600080fd5b5056fea264697066735822122040b960152888b1f29af47e31e81f007992dea5a4cd4a6983e1a6318737bc3db564736f6c63430008040033", + "linkReferences": {}, + "deployedLinkReferences": {} +} diff --git a/contracts/Minter.json b/contracts/Minter.json new file mode 100644 index 0000000..06acc74 --- /dev/null +++ b/contracts/Minter.json @@ -0,0 +1,151 @@ +{ + "_format": "hh-sol-artifact-1", + "contractName": "Minter", + "sourceName": "contracts/Minter.sol", + "abi": [ + { + "inputs": [ + { + "internalType": "contract Collectible", + "name": "token_", + "type": "address" + }, + { + "internalType": "address", + "name": "authority_", + "type": "address" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "previousOwner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "OwnershipTransferred", + "type": "event" + }, + { + "inputs": [], + "name": "authority", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "user", + "type": "address" + }, + { + "internalType": "string", + "name": "tokenURI", + "type": "string" + }, + { + "internalType": "uint8", + "name": "v", + "type": "uint8" + }, + { + "internalType": "bytes32", + "name": "r", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "s", + "type": "bytes32" + } + ], + "name": "mint", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "owner", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "renounceOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "token", + "outputs": [ + { + "internalType": "contract Collectible", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "transferOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "to", + "type": "address" + } + ], + "name": "transferTokenOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + } + ], + "bytecode": "0x60c06040523480156200001157600080fd5b5060405162001323380380620013238339818101604052810190620000379190620001c7565b620000576200004b620000cd60201b60201c565b620000d560201b60201c565b8173ffffffffffffffffffffffffffffffffffffffff1660808173ffffffffffffffffffffffffffffffffffffffff1660601b815250508073ffffffffffffffffffffffffffffffffffffffff1660a08173ffffffffffffffffffffffffffffffffffffffff1660601b81525050505062000284565b600033905090565b60008060009054906101000a900473ffffffffffffffffffffffffffffffffffffffff169050816000806101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff1602179055508173ffffffffffffffffffffffffffffffffffffffff168173ffffffffffffffffffffffffffffffffffffffff167f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e060405160405180910390a35050565b600081519050620001aa8162000250565b92915050565b600081519050620001c1816200026a565b92915050565b60008060408385031215620001db57600080fd5b6000620001eb85828601620001b0565b9250506020620001fe8582860162000199565b9150509250929050565b6000620002158262000230565b9050919050565b6000620002298262000208565b9050919050565b600073ffffffffffffffffffffffffffffffffffffffff82169050919050565b6200025b8162000208565b81146200026757600080fd5b50565b62000275816200021c565b81146200028157600080fd5b50565b60805160601c60a05160601c61105e620002c56000396000818161035d015261057f0152600081816101b801528181610416015261069b015261105e6000f3fe608060405234801561001057600080fd5b506004361061007d5760003560e01c80638da5cb5b1161005b5780638da5cb5b146100c4578063bf7e214f146100e2578063f2fde38b14610100578063fc0c546a1461011c5761007d565b806321e6b53d146100825780635e5b7c181461009e578063715018a6146100ba575b600080fd5b61009c60048036038101906100979190610845565b61013a565b005b6100b860048036038101906100b3919061086e565b610244565b005b6100c26104cc565b005b6100cc610554565b6040516100d99190610b75565b60405180910390f35b6100ea61057d565b6040516100f79190610b75565b60405180910390f35b61011a60048036038101906101159190610845565b6105a1565b005b610124610699565b6040516101319190610c05565b60405180910390f35b6101426106bd565b73ffffffffffffffffffffffffffffffffffffffff16610160610554565b73ffffffffffffffffffffffffffffffffffffffff16146101b6576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004016101ad90610c80565b60405180910390fd5b7f000000000000000000000000000000000000000000000000000000000000000073ffffffffffffffffffffffffffffffffffffffff1663f2fde38b826040518263ffffffff1660e01b815260040161020f9190610b75565b600060405180830381600087803b15801561022957600080fd5b505af115801561023d573d6000803e3d6000fd5b5050505050565b60004630878760405160200161025d9493929190610b20565b6040516020818303038152906040528051906020012090506000816040516020016102889190610afa565b604051602081830303815290604052805190602001209050600115156001600083815260200190815260200160002060009054906101000a900460ff1615151415610308576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004016102ff90610c60565b60405180910390fd5b60006001828787876040516000815260200160405260405161032d9493929190610bc0565b6020604051602081039080840390855afa15801561034f573d6000803e3d6000fd5b5050506020604051035190507f000000000000000000000000000000000000000000000000000000000000000073ffffffffffffffffffffffffffffffffffffffff168173ffffffffffffffffffffffffffffffffffffffff16146103e9576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004016103e090610c20565b60405180910390fd5b600180600084815260200190815260200160002060006101000a81548160ff0219169083151502179055507f000000000000000000000000000000000000000000000000000000000000000073ffffffffffffffffffffffffffffffffffffffff1663d0def52189896040518363ffffffff1660e01b815260040161046f929190610b90565b602060405180830381600087803b15801561048957600080fd5b505af115801561049d573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906104c191906108fd565b505050505050505050565b6104d46106bd565b73ffffffffffffffffffffffffffffffffffffffff166104f2610554565b73ffffffffffffffffffffffffffffffffffffffff1614610548576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040161053f90610c80565b60405180910390fd5b61055260006106c5565b565b60008060009054906101000a900473ffffffffffffffffffffffffffffffffffffffff16905090565b7f000000000000000000000000000000000000000000000000000000000000000081565b6105a96106bd565b73ffffffffffffffffffffffffffffffffffffffff166105c7610554565b73ffffffffffffffffffffffffffffffffffffffff161461061d576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040161061490610c80565b60405180910390fd5b600073ffffffffffffffffffffffffffffffffffffffff168173ffffffffffffffffffffffffffffffffffffffff16141561068d576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040161068490610c40565b60405180910390fd5b610696816106c5565b50565b7f000000000000000000000000000000000000000000000000000000000000000081565b600033905090565b60008060009054906101000a900473ffffffffffffffffffffffffffffffffffffffff169050816000806101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff1602179055508173ffffffffffffffffffffffffffffffffffffffff168173ffffffffffffffffffffffffffffffffffffffff167f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e060405160405180910390a35050565b600061079c61079784610cc5565b610ca0565b9050828152602081018484840111156107b457600080fd5b6107bf848285610db8565b509392505050565b6000813590506107d681610fcc565b92915050565b6000813590506107eb81610fe3565b92915050565b600082601f83011261080257600080fd5b8135610812848260208601610789565b91505092915050565b60008151905061082a81610ffa565b92915050565b60008135905061083f81611011565b92915050565b60006020828403121561085757600080fd5b6000610865848285016107c7565b91505092915050565b600080600080600060a0868803121561088657600080fd5b6000610894888289016107c7565b955050602086013567ffffffffffffffff8111156108b157600080fd5b6108bd888289016107f1565b94505060406108ce88828901610830565b93505060606108df888289016107dc565b92505060806108f0888289016107dc565b9150509295509295909350565b60006020828403121561090f57600080fd5b600061091d8482850161081b565b91505092915050565b61092f81610d1d565b82525050565b61094661094182610d1d565b610e2b565b82525050565b61095581610d2f565b82525050565b61096c61096782610d2f565b610e3d565b82525050565b61097b81610d70565b82525050565b61099261098d82610d94565b610e2b565b82525050565b60006109a382610cf6565b6109ad8185610d01565b93506109bd818560208601610dc7565b6109c681610e92565b840191505092915050565b60006109dc82610cf6565b6109e68185610d12565b93506109f6818560208601610dc7565b80840191505092915050565b6000610a0f601c83610d12565b9150610a1a82610eb0565b601c82019050919050565b6000610a32601983610d01565b9150610a3d82610ed9565b602082019050919050565b6000610a55602683610d01565b9150610a6082610f02565b604082019050919050565b6000610a78601683610d01565b9150610a8382610f51565b602082019050919050565b6000610a9b602083610d01565b9150610aa682610f7a565b602082019050919050565b6000610abe600483610d12565b9150610ac982610fa3565b600482019050919050565b610ae5610ae082610d59565b610e59565b82525050565b610af481610d63565b82525050565b6000610b0582610a02565b9150610b11828461095b565b60208201915081905092915050565b6000610b2c8287610ad4565b602082019150610b3c8286610981565b601482019150610b4b82610ab1565b9150610b578285610935565b601482019150610b6782846109d1565b915081905095945050505050565b6000602082019050610b8a6000830184610926565b92915050565b6000604082019050610ba56000830185610926565b8181036020830152610bb78184610998565b90509392505050565b6000608082019050610bd5600083018761094c565b610be26020830186610aeb565b610bef604083018561094c565b610bfc606083018461094c565b95945050505050565b6000602082019050610c1a6000830184610972565b92915050565b60006020820190508181036000830152610c3981610a25565b9050919050565b60006020820190508181036000830152610c5981610a48565b9050919050565b60006020820190508181036000830152610c7981610a6b565b9050919050565b60006020820190508181036000830152610c9981610a8e565b9050919050565b6000610caa610cbb565b9050610cb68282610dfa565b919050565b6000604051905090565b600067ffffffffffffffff821115610ce057610cdf610e63565b5b610ce982610e92565b9050602081019050919050565b600081519050919050565b600082825260208201905092915050565b600081905092915050565b6000610d2882610d39565b9050919050565b6000819050919050565b600073ffffffffffffffffffffffffffffffffffffffff82169050919050565b6000819050919050565b600060ff82169050919050565b6000610d7b82610d82565b9050919050565b6000610d8d82610d39565b9050919050565b6000610d9f82610da6565b9050919050565b6000610db182610d39565b9050919050565b82818337600083830152505050565b60005b83811015610de5578082015181840152602081019050610dca565b83811115610df4576000848401525b50505050565b610e0382610e92565b810181811067ffffffffffffffff82111715610e2257610e21610e63565b5b80604052505050565b6000610e3682610e47565b9050919050565b6000819050919050565b6000610e5282610ea3565b9050919050565b6000819050919050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b6000601f19601f8301169050919050565b60008160601b9050919050565b7f19457468657265756d205369676e6564204d6573736167653a0a333200000000600082015250565b7f4d696e7465723a20696e76616c6964207369676e617475726500000000000000600082015250565b7f4f776e61626c653a206e6577206f776e657220697320746865207a65726f206160008201527f6464726573730000000000000000000000000000000000000000000000000000602082015250565b7f4d696e7465723a20616c7265616479206d696e74656400000000000000000000600082015250565b7f4f776e61626c653a2063616c6c6572206973206e6f7420746865206f776e6572600082015250565b7f6d696e7400000000000000000000000000000000000000000000000000000000600082015250565b610fd581610d1d565b8114610fe057600080fd5b50565b610fec81610d2f565b8114610ff757600080fd5b50565b61100381610d59565b811461100e57600080fd5b50565b61101a81610d63565b811461102557600080fd5b5056fea2646970667358221220dc905fe48fdfa15ef2f34b33a242665d78145271992215e171731c7f80edbf4f64736f6c63430008040033", + "deployedBytecode": "0x608060405234801561001057600080fd5b506004361061007d5760003560e01c80638da5cb5b1161005b5780638da5cb5b146100c4578063bf7e214f146100e2578063f2fde38b14610100578063fc0c546a1461011c5761007d565b806321e6b53d146100825780635e5b7c181461009e578063715018a6146100ba575b600080fd5b61009c60048036038101906100979190610845565b61013a565b005b6100b860048036038101906100b3919061086e565b610244565b005b6100c26104cc565b005b6100cc610554565b6040516100d99190610b75565b60405180910390f35b6100ea61057d565b6040516100f79190610b75565b60405180910390f35b61011a60048036038101906101159190610845565b6105a1565b005b610124610699565b6040516101319190610c05565b60405180910390f35b6101426106bd565b73ffffffffffffffffffffffffffffffffffffffff16610160610554565b73ffffffffffffffffffffffffffffffffffffffff16146101b6576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004016101ad90610c80565b60405180910390fd5b7f000000000000000000000000000000000000000000000000000000000000000073ffffffffffffffffffffffffffffffffffffffff1663f2fde38b826040518263ffffffff1660e01b815260040161020f9190610b75565b600060405180830381600087803b15801561022957600080fd5b505af115801561023d573d6000803e3d6000fd5b5050505050565b60004630878760405160200161025d9493929190610b20565b6040516020818303038152906040528051906020012090506000816040516020016102889190610afa565b604051602081830303815290604052805190602001209050600115156001600083815260200190815260200160002060009054906101000a900460ff1615151415610308576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004016102ff90610c60565b60405180910390fd5b60006001828787876040516000815260200160405260405161032d9493929190610bc0565b6020604051602081039080840390855afa15801561034f573d6000803e3d6000fd5b5050506020604051035190507f000000000000000000000000000000000000000000000000000000000000000073ffffffffffffffffffffffffffffffffffffffff168173ffffffffffffffffffffffffffffffffffffffff16146103e9576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004016103e090610c20565b60405180910390fd5b600180600084815260200190815260200160002060006101000a81548160ff0219169083151502179055507f000000000000000000000000000000000000000000000000000000000000000073ffffffffffffffffffffffffffffffffffffffff1663d0def52189896040518363ffffffff1660e01b815260040161046f929190610b90565b602060405180830381600087803b15801561048957600080fd5b505af115801561049d573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906104c191906108fd565b505050505050505050565b6104d46106bd565b73ffffffffffffffffffffffffffffffffffffffff166104f2610554565b73ffffffffffffffffffffffffffffffffffffffff1614610548576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040161053f90610c80565b60405180910390fd5b61055260006106c5565b565b60008060009054906101000a900473ffffffffffffffffffffffffffffffffffffffff16905090565b7f000000000000000000000000000000000000000000000000000000000000000081565b6105a96106bd565b73ffffffffffffffffffffffffffffffffffffffff166105c7610554565b73ffffffffffffffffffffffffffffffffffffffff161461061d576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040161061490610c80565b60405180910390fd5b600073ffffffffffffffffffffffffffffffffffffffff168173ffffffffffffffffffffffffffffffffffffffff16141561068d576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040161068490610c40565b60405180910390fd5b610696816106c5565b50565b7f000000000000000000000000000000000000000000000000000000000000000081565b600033905090565b60008060009054906101000a900473ffffffffffffffffffffffffffffffffffffffff169050816000806101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff1602179055508173ffffffffffffffffffffffffffffffffffffffff168173ffffffffffffffffffffffffffffffffffffffff167f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e060405160405180910390a35050565b600061079c61079784610cc5565b610ca0565b9050828152602081018484840111156107b457600080fd5b6107bf848285610db8565b509392505050565b6000813590506107d681610fcc565b92915050565b6000813590506107eb81610fe3565b92915050565b600082601f83011261080257600080fd5b8135610812848260208601610789565b91505092915050565b60008151905061082a81610ffa565b92915050565b60008135905061083f81611011565b92915050565b60006020828403121561085757600080fd5b6000610865848285016107c7565b91505092915050565b600080600080600060a0868803121561088657600080fd5b6000610894888289016107c7565b955050602086013567ffffffffffffffff8111156108b157600080fd5b6108bd888289016107f1565b94505060406108ce88828901610830565b93505060606108df888289016107dc565b92505060806108f0888289016107dc565b9150509295509295909350565b60006020828403121561090f57600080fd5b600061091d8482850161081b565b91505092915050565b61092f81610d1d565b82525050565b61094661094182610d1d565b610e2b565b82525050565b61095581610d2f565b82525050565b61096c61096782610d2f565b610e3d565b82525050565b61097b81610d70565b82525050565b61099261098d82610d94565b610e2b565b82525050565b60006109a382610cf6565b6109ad8185610d01565b93506109bd818560208601610dc7565b6109c681610e92565b840191505092915050565b60006109dc82610cf6565b6109e68185610d12565b93506109f6818560208601610dc7565b80840191505092915050565b6000610a0f601c83610d12565b9150610a1a82610eb0565b601c82019050919050565b6000610a32601983610d01565b9150610a3d82610ed9565b602082019050919050565b6000610a55602683610d01565b9150610a6082610f02565b604082019050919050565b6000610a78601683610d01565b9150610a8382610f51565b602082019050919050565b6000610a9b602083610d01565b9150610aa682610f7a565b602082019050919050565b6000610abe600483610d12565b9150610ac982610fa3565b600482019050919050565b610ae5610ae082610d59565b610e59565b82525050565b610af481610d63565b82525050565b6000610b0582610a02565b9150610b11828461095b565b60208201915081905092915050565b6000610b2c8287610ad4565b602082019150610b3c8286610981565b601482019150610b4b82610ab1565b9150610b578285610935565b601482019150610b6782846109d1565b915081905095945050505050565b6000602082019050610b8a6000830184610926565b92915050565b6000604082019050610ba56000830185610926565b8181036020830152610bb78184610998565b90509392505050565b6000608082019050610bd5600083018761094c565b610be26020830186610aeb565b610bef604083018561094c565b610bfc606083018461094c565b95945050505050565b6000602082019050610c1a6000830184610972565b92915050565b60006020820190508181036000830152610c3981610a25565b9050919050565b60006020820190508181036000830152610c5981610a48565b9050919050565b60006020820190508181036000830152610c7981610a6b565b9050919050565b60006020820190508181036000830152610c9981610a8e565b9050919050565b6000610caa610cbb565b9050610cb68282610dfa565b919050565b6000604051905090565b600067ffffffffffffffff821115610ce057610cdf610e63565b5b610ce982610e92565b9050602081019050919050565b600081519050919050565b600082825260208201905092915050565b600081905092915050565b6000610d2882610d39565b9050919050565b6000819050919050565b600073ffffffffffffffffffffffffffffffffffffffff82169050919050565b6000819050919050565b600060ff82169050919050565b6000610d7b82610d82565b9050919050565b6000610d8d82610d39565b9050919050565b6000610d9f82610da6565b9050919050565b6000610db182610d39565b9050919050565b82818337600083830152505050565b60005b83811015610de5578082015181840152602081019050610dca565b83811115610df4576000848401525b50505050565b610e0382610e92565b810181811067ffffffffffffffff82111715610e2257610e21610e63565b5b80604052505050565b6000610e3682610e47565b9050919050565b6000819050919050565b6000610e5282610ea3565b9050919050565b6000819050919050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b6000601f19601f8301169050919050565b60008160601b9050919050565b7f19457468657265756d205369676e6564204d6573736167653a0a333200000000600082015250565b7f4d696e7465723a20696e76616c6964207369676e617475726500000000000000600082015250565b7f4f776e61626c653a206e6577206f776e657220697320746865207a65726f206160008201527f6464726573730000000000000000000000000000000000000000000000000000602082015250565b7f4d696e7465723a20616c7265616479206d696e74656400000000000000000000600082015250565b7f4f776e61626c653a2063616c6c6572206973206e6f7420746865206f776e6572600082015250565b7f6d696e7400000000000000000000000000000000000000000000000000000000600082015250565b610fd581610d1d565b8114610fe057600080fd5b50565b610fec81610d2f565b8114610ff757600080fd5b50565b61100381610d59565b811461100e57600080fd5b50565b61101a81610d63565b811461102557600080fd5b5056fea2646970667358221220dc905fe48fdfa15ef2f34b33a242665d78145271992215e171731c7f80edbf4f64736f6c63430008040033", + "linkReferences": {}, + "deployedLinkReferences": {} +} diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 0000000..c77852f --- /dev/null +++ b/docker-compose.yaml @@ -0,0 +1,17 @@ +version: "3" + +services: + database: + image: postgres:latest + restart: always + environment: + POSTGRES_PASSWORD: mitra + POSTGRES_USER: mitra + POSTGRES_DB: mitra + ports: + - "5432:5432" + volumes: + - mitra_postgres:/var/lib/postgresql/data + +volumes: + mitra_postgres: diff --git a/migrations/V0001__create_tables.sql b/migrations/V0001__create_tables.sql new file mode 100644 index 0000000..66ac352 --- /dev/null +++ b/migrations/V0001__create_tables.sql @@ -0,0 +1,62 @@ +CREATE TABLE actor_profile ( + id UUID PRIMARY KEY, + username VARCHAR(100) NOT NULL, + display_name VARCHAR(100), + acct VARCHAR(200) UNIQUE NOT NULL, + bio TEXT, + bio_source TEXT, + avatar_file_name VARCHAR(100), + banner_file_name VARCHAR(100), + follower_count INTEGER NOT NULL CHECK (follower_count >= 0) DEFAULT 0, + following_count INTEGER NOT NULL CHECK (following_count >= 0) DEFAULT 0, + post_count INTEGER NOT NULL CHECK (post_count >= 0) DEFAULT 0, + actor_json JSONB, + created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now() +); + +CREATE TABLE user_invite_code ( + code VARCHAR(100) PRIMARY KEY, + used BOOLEAN NOT NULL DEFAULT FALSE +); + +CREATE TABLE user_account ( + id UUID PRIMARY KEY REFERENCES actor_profile (id) ON DELETE CASCADE, + wallet_address VARCHAR(100) UNIQUE NOT NULL, + password_hash VARCHAR(200) NOT NULL, + private_key TEXT NOT NULL, + invite_code VARCHAR(100) UNIQUE REFERENCES user_invite_code (code) ON DELETE SET NULL, + created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now() +); + +CREATE TABLE post ( + id UUID PRIMARY KEY, + author_id UUID NOT NULL REFERENCES actor_profile (id) ON DELETE CASCADE, + content TEXT NOT NULL, + ipfs_cid VARCHAR(200), + token_id INTEGER, + token_tx_id VARCHAR(200), + created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now() +); + +CREATE TABLE relationship ( + source_id UUID NOT NULL REFERENCES actor_profile (id) ON DELETE CASCADE, + target_id UUID NOT NULL REFERENCES actor_profile (id) ON DELETE CASCADE, + PRIMARY KEY (source_id, target_id) +); + +CREATE TABLE follow_request ( + id UUID PRIMARY KEY, + source_id UUID NOT NULL REFERENCES actor_profile (id) ON DELETE CASCADE, + target_id UUID NOT NULL REFERENCES actor_profile (id) ON DELETE CASCADE, + request_status SMALLINT NOT NULL, + UNIQUE (source_id, target_id) +); + +CREATE TABLE media_attachment ( + id UUID PRIMARY KEY, + owner_id UUID NOT NULL REFERENCES actor_profile (id) ON DELETE CASCADE, + media_type VARCHAR(50), + file_name VARCHAR(200) NOT NULL, + post_id UUID REFERENCES post (id) ON DELETE CASCADE, + created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now() +) diff --git a/migrations/schema.sql b/migrations/schema.sql new file mode 100644 index 0000000..66ac352 --- /dev/null +++ b/migrations/schema.sql @@ -0,0 +1,62 @@ +CREATE TABLE actor_profile ( + id UUID PRIMARY KEY, + username VARCHAR(100) NOT NULL, + display_name VARCHAR(100), + acct VARCHAR(200) UNIQUE NOT NULL, + bio TEXT, + bio_source TEXT, + avatar_file_name VARCHAR(100), + banner_file_name VARCHAR(100), + follower_count INTEGER NOT NULL CHECK (follower_count >= 0) DEFAULT 0, + following_count INTEGER NOT NULL CHECK (following_count >= 0) DEFAULT 0, + post_count INTEGER NOT NULL CHECK (post_count >= 0) DEFAULT 0, + actor_json JSONB, + created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now() +); + +CREATE TABLE user_invite_code ( + code VARCHAR(100) PRIMARY KEY, + used BOOLEAN NOT NULL DEFAULT FALSE +); + +CREATE TABLE user_account ( + id UUID PRIMARY KEY REFERENCES actor_profile (id) ON DELETE CASCADE, + wallet_address VARCHAR(100) UNIQUE NOT NULL, + password_hash VARCHAR(200) NOT NULL, + private_key TEXT NOT NULL, + invite_code VARCHAR(100) UNIQUE REFERENCES user_invite_code (code) ON DELETE SET NULL, + created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now() +); + +CREATE TABLE post ( + id UUID PRIMARY KEY, + author_id UUID NOT NULL REFERENCES actor_profile (id) ON DELETE CASCADE, + content TEXT NOT NULL, + ipfs_cid VARCHAR(200), + token_id INTEGER, + token_tx_id VARCHAR(200), + created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now() +); + +CREATE TABLE relationship ( + source_id UUID NOT NULL REFERENCES actor_profile (id) ON DELETE CASCADE, + target_id UUID NOT NULL REFERENCES actor_profile (id) ON DELETE CASCADE, + PRIMARY KEY (source_id, target_id) +); + +CREATE TABLE follow_request ( + id UUID PRIMARY KEY, + source_id UUID NOT NULL REFERENCES actor_profile (id) ON DELETE CASCADE, + target_id UUID NOT NULL REFERENCES actor_profile (id) ON DELETE CASCADE, + request_status SMALLINT NOT NULL, + UNIQUE (source_id, target_id) +); + +CREATE TABLE media_attachment ( + id UUID PRIMARY KEY, + owner_id UUID NOT NULL REFERENCES actor_profile (id) ON DELETE CASCADE, + media_type VARCHAR(50), + file_name VARCHAR(200) NOT NULL, + post_id UUID REFERENCES post (id) ON DELETE CASCADE, + created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now() +) diff --git a/src/activitypub/activity.rs b/src/activitypub/activity.rs new file mode 100644 index 0000000..fc97158 --- /dev/null +++ b/src/activitypub/activity.rs @@ -0,0 +1,263 @@ +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; +use serde_json::{json, Value}; +use uuid::Uuid; + +use crate::config::Config; +use crate::models::posts::types::Post; +use crate::models::profiles::types::DbActorProfile; +use crate::utils::files::get_file_url; +use super::constants::{AP_CONTEXT, AP_PUBLIC}; +use super::views::{get_actor_url, get_object_url}; +use super::vocabulary::*; + +#[derive(Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct Attachment { + pub name: String, + + #[serde(rename = "type")] + pub attachment_type: String, + + pub media_type: String, + pub url: String, +} + +#[derive(Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct Object { + #[serde(rename = "@context")] + pub context: Option, + + pub id: String, + + #[serde(rename = "type")] + pub object_type: String, + + #[serde(skip_serializing_if = "Option::is_none")] + pub actor: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub attachment: Option>, + + #[serde(skip_serializing_if = "Option::is_none")] + pub object: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub published: Option>, + + #[serde(skip_serializing_if = "Option::is_none")] + pub attributed_to: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub in_reply_to: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub content: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub to: Option, +} + +#[derive(Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct Activity { + #[serde(rename = "@context")] + pub context: Value, + + pub id: String, + + #[serde(rename = "type")] + pub activity_type: String, + + pub actor: String, + pub object: Value, +} + +fn create_activity( + instance_url: &str, + actor_name: &str, + activity_type: &str, + activity_uuid: Option, + object: Value, +) -> Activity { + let actor_id = get_actor_url( + instance_url, + &actor_name, + ); + let activity_id = get_object_url( + instance_url, + &activity_uuid.unwrap_or(Uuid::new_v4()), + ); + let activity = Activity { + context: json!(AP_CONTEXT), + id: activity_id, + activity_type: activity_type.to_string(), + actor: actor_id, + object: object, + }; + activity +} + +pub fn create_activity_note( + config: &Config, + post: &Post, +) -> Activity { + let object_id = get_object_url( + &config.instance_url(), + &post.id, + ); + let actor_id = get_actor_url( + &config.instance_url(), + &post.author.username, + ); + let attachments: Vec = post.attachments.iter().map(|db_item| { + let url = get_file_url(&config.instance_url(), &db_item.file_name); + let media_type = db_item.media_type.clone().unwrap_or("".to_string()); + Attachment { + name: "".to_string(), + attachment_type: DOCUMENT.to_string(), + media_type, + url, + } + }).collect(); + let object = Object { + context: Some(json!(AP_CONTEXT)), + id: object_id, + object_type: NOTE.to_string(), + actor: None, + attachment: Some(attachments), + object: None, + published: Some(post.created_at), + attributed_to: Some(actor_id.clone()), + in_reply_to: None, + content: Some(post.content.clone()), + to: Some(json!(AP_PUBLIC)), + }; + let activity = create_activity( + &config.instance_url(), + &post.author.username, + CREATE, + None, + serde_json::to_value(object).unwrap(), + ); + activity +} + +pub fn create_activity_follow( + config: &Config, + actor_profile: &DbActorProfile, + follow_request_id: &Uuid, + target_id: &str, +) -> Activity { + let object = Object { + context: Some(json!(AP_CONTEXT)), + id: target_id.to_owned(), + object_type: PERSON.to_string(), + actor: None, + attachment: None, + object: None, + published: None, + attributed_to: None, + in_reply_to: None, + content: None, + to: None, + }; + let activity = create_activity( + &config.instance_url(), + &actor_profile.username, + FOLLOW, + Some(*follow_request_id), + serde_json::to_value(object).unwrap(), + ); + activity +} + +pub fn create_activity_accept_follow( + config: &Config, + actor_profile: &DbActorProfile, + follow_activity_id: &str, +) -> Activity { + // TODO: use received activity as object + let object = Object { + context: Some(json!(AP_CONTEXT)), + id: follow_activity_id.to_string(), + object_type: FOLLOW.to_string(), + actor: None, + attachment: None, + object: None, + published: None, + attributed_to: None, + in_reply_to: None, + content: None, + to: None, + }; + let activity = create_activity( + &config.instance_url(), + &actor_profile.username, + ACCEPT, + None, + serde_json::to_value(object).unwrap(), + ); + activity +} + +pub fn create_activity_undo_follow( + config: &Config, + actor_profile: &DbActorProfile, + follow_request_id: &Uuid, + target_id: &str, +) -> Activity { + // TODO: retrieve 'Follow' activity from database + let follow_activity_id = get_object_url( + &config.instance_url(), + follow_request_id, + ); + let follow_actor_id = get_actor_url( + &config.instance_url(), + &actor_profile.username, + ); + let object = Object { + context: Some(json!(AP_CONTEXT)), + id: follow_activity_id, + object_type: FOLLOW.to_string(), + actor: Some(follow_actor_id), + attachment: None, + object: Some(target_id.to_owned()), + published: None, + attributed_to: None, + in_reply_to: None, + content: None, + to: None, + }; + let activity = create_activity( + &config.instance_url(), + &actor_profile.username, + UNDO, + None, + serde_json::to_value(object).unwrap(), + ); + activity +} + +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +pub struct OrderedCollection { + #[serde(rename = "@context")] + pub context: Value, + + pub id: String, + + #[serde(rename = "type")] + pub object_type: String, +} + +impl OrderedCollection { + pub fn new(collection_url: String) -> Self { + Self { + context: json!(AP_CONTEXT), + id: collection_url, + object_type: "OrderedCollection".to_string(), + } + } +} diff --git a/src/activitypub/actor.rs b/src/activitypub/actor.rs new file mode 100644 index 0000000..1fa786f --- /dev/null +++ b/src/activitypub/actor.rs @@ -0,0 +1,139 @@ +use serde::{Deserialize, Serialize}; +use serde_json::{json, Value}; + +use crate::config::Config; +use crate::errors::HttpError; +use crate::models::users::types::User; +use crate::utils::crypto::{deserialize_private_key, get_public_key_pem}; +use crate::utils::files::get_file_url; +use super::constants::AP_CONTEXT; +use super::views::{ + get_actor_url, + get_inbox_url, + get_outbox_url, + get_followers_url, + get_following_url, +}; +use super::vocabulary::{PERSON, IMAGE}; + +const W3ID_CONTEXT: &str = "https://w3id.org/security/v1"; + +#[derive(Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct PublicKey { + id: String, + owner: String, + pub public_key_pem: String, +} + +#[derive(Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct Image { + #[serde(rename = "type")] + object_type: String, + pub url: String, +} + +#[derive(Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ActorCapabilities { + accepts_chat_messages: Option, +} + +#[derive(Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct Actor { + #[serde(rename = "@context")] + context: Option, + + pub id: String, + + #[serde(rename = "type")] + object_type: String, + + pub name: String, + pub preferred_username: String, + pub inbox: String, + pub outbox: String, + pub followers: String, + pub following: String, + + pub public_key: PublicKey, + + #[serde(skip_serializing_if = "Option::is_none")] + pub capabilities: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub icon: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub image: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub summary: Option, +} + +pub fn get_actor_object( + config: &Config, + user: &User, +) -> Result { + let username = &user.profile.username; + let id = get_actor_url(&config.instance_url(), &username); + let inbox = get_inbox_url(&config.instance_url(), &username); + let outbox = get_outbox_url(&config.instance_url(), &username); + let followers = get_followers_url(&config.instance_url(), &username); + let following = get_following_url(&config.instance_url(), &username); + + let private_key = deserialize_private_key(&user.private_key) + .map_err(|_| HttpError::InternalError)?; + let public_key_pem = get_public_key_pem(&private_key) + .map_err(|_| HttpError::InternalError)?; + let public_key = PublicKey { + id: format!("{}#main-key", id), + owner: id.clone(), + public_key_pem: public_key_pem, + }; + let avatar = match &user.profile.avatar_file_name { + Some(file_name) => { + let image = Image { + object_type: IMAGE.to_string(), + url: get_file_url(&config.instance_url(), file_name), + }; + Some(image) + }, + None => None, + }; + let banner = match &user.profile.banner_file_name { + Some(file_name) => { + let image = Image { + object_type: IMAGE.to_string(), + url: get_file_url(&config.instance_url(), file_name), + }; + Some(image) + }, + None => None, + }; + let capabilities = ActorCapabilities { + accepts_chat_messages: Some(false), + }; + let actor = Actor { + context: Some(json!([ + AP_CONTEXT.to_string(), + W3ID_CONTEXT.to_string(), + ])), + id, + object_type: PERSON.to_string(), + name: username.to_string(), + preferred_username: username.to_string(), + inbox, + outbox, + followers, + following, + public_key, + capabilities: Some(capabilities), + icon: avatar, + image: banner, + summary: None, + }; + Ok(actor) +} diff --git a/src/activitypub/constants.rs b/src/activitypub/constants.rs new file mode 100644 index 0000000..d94d885 --- /dev/null +++ b/src/activitypub/constants.rs @@ -0,0 +1,3 @@ +pub const ACTIVITY_CONTENT_TYPE: &str = "application/activity+json"; +pub const AP_CONTEXT: &str = "https://www.w3.org/ns/activitystreams"; +pub const AP_PUBLIC: &str = "https://www.w3.org/ns/activitystreams#Public"; diff --git a/src/activitypub/deliverer.rs b/src/activitypub/deliverer.rs new file mode 100644 index 0000000..165a8ce --- /dev/null +++ b/src/activitypub/deliverer.rs @@ -0,0 +1,97 @@ +use crate::config::{Environment, Config}; +use crate::http_signatures::create::{create_http_signature, SignatureError}; +use crate::models::users::types::User; +use crate::utils::crypto::deserialize_private_key; +use super::activity::Activity; +use super::actor::Actor; +use super::constants::ACTIVITY_CONTENT_TYPE; +use super::views::get_actor_url; + +#[derive(thiserror::Error, Debug)] +pub enum DelivererError { + #[error("key error")] + KeyDeserializationError(#[from] rsa::pkcs8::Error), + + #[error(transparent)] + SignatureError(#[from] SignatureError), + + #[error("activity serialization error")] + SerializationError(#[from] serde_json::Error), + + #[error(transparent)] + RequestError(#[from] reqwest::Error), + + #[error("http error {0:?}")] + HttpError(reqwest::StatusCode), +} + +async fn send_activity( + config: &Config, + sender: &User, + activity: &Activity, + inbox_url: &str, +) -> Result<(), DelivererError> { + let activity_json = serde_json::to_string(&activity)?; + log::info!("sending activity: {}", activity_json); + let actor_key = deserialize_private_key(&sender.private_key)?; + let actor_key_id = format!( + "{}#main-key", + get_actor_url( + &config.instance_url(), + &sender.profile.username, + ), + ); + let headers = create_http_signature( + &inbox_url, + &activity_json, + actor_key, + actor_key_id, + )?; + + // Send + match config.environment { + Environment::Development => { + log::info!( + "development mode: not sending activity to {}", + inbox_url, + ); + }, + Environment::Production => { + let client = reqwest::Client::new(); + // Default timeout is 30s + let response = client.post(inbox_url) + .header("Host", headers.host) + .header("Date", headers.date) + .header("Digest", headers.digest) + .header("Signature", headers.signature) + .header("Content-Type", ACTIVITY_CONTENT_TYPE) + .body(activity_json) + .send() + .await?; + let response_status = response.status(); + let response_text = response.text().await?; + log::info!( + "remote server response: {}", + response_text, + ); + if response_status.is_client_error() || response_status.is_server_error() { + return Err(DelivererError::HttpError(response_status)); + } + }, + }; + Ok(()) +} + +pub async fn deliver_activity( + config: &Config, + sender: &User, + activity: Activity, + recipients: Vec, +) -> () { + for actor in recipients { + // TODO: retry on error + if let Err(err) = send_activity(&config, &sender, &activity, &actor.inbox).await { + log::error!("{}", err); + } + }; +} diff --git a/src/activitypub/fetcher.rs b/src/activitypub/fetcher.rs new file mode 100644 index 0000000..b5fae86 --- /dev/null +++ b/src/activitypub/fetcher.rs @@ -0,0 +1,120 @@ +use std::path::PathBuf; + +use serde_json::Value; + +use crate::models::profiles::types::ProfileCreateData; +use crate::utils::files::{save_file, FileError}; +use crate::webfinger::types::JsonResourceDescriptor; +use super::actor::Actor; +use super::constants::ACTIVITY_CONTENT_TYPE; + +#[derive(thiserror::Error, Debug)] +pub enum FetchError { + #[error("invalid URL")] + UrlError(#[from] url::ParseError), + + #[error(transparent)] + RequestError(#[from] reqwest::Error), + + #[error("json parse error")] + JsonParseError(#[from] serde_json::Error), + + #[error("file error")] + FileError(#[from] FileError), + + #[error("{0}")] + OtherError(&'static str), +} + +pub async fn fetch_avatar_and_banner( + actor: &Actor, + media_dir: &PathBuf, +) -> Result<(Option, Option), FetchError> { + let avatar = match &actor.icon { + Some(icon) => { + let file_name = fetch_attachment( + &icon.url, + media_dir, + ).await?; + Some(file_name) + }, + None => None, + }; + let banner = match &actor.image { + Some(image) => { + let file_name = fetch_attachment( + &image.url, + media_dir, + ).await?; + Some(file_name) + }, + None => None, + }; + Ok((avatar, banner)) +} + +pub async fn fetch_profile( + username: &str, + instance_uri: &str, + media_dir: &PathBuf, +) -> Result { + let actor_address = format!("{}@{}", &username, &instance_uri); + let webfinger_account_uri = format!("acct:{}", actor_address); + // TOOD: support http + let webfinger_url = format!("https://{}/.well-known/webfinger", instance_uri); + let client = reqwest::Client::new(); + let webfinger_data = client.get(&webfinger_url) + .query(&[("resource", webfinger_account_uri)]) + .send().await? + .text().await?; + let jrd: JsonResourceDescriptor = serde_json::from_str(&webfinger_data)?; + let link = jrd.links.iter() + .find(|link| link.rel == "self") + .ok_or(FetchError::OtherError("self link not found"))?; + let actor_url = link.href.as_ref() + .ok_or(FetchError::OtherError("account href not found"))?; + fetch_profile_by_actor_id(actor_url, media_dir).await +} + +pub async fn fetch_profile_by_actor_id( + actor_url: &str, + media_dir: &PathBuf, +) -> Result { + let actor_host = url::Url::parse(actor_url)? + .host_str() + .ok_or(FetchError::OtherError("invalid URL"))? + .to_owned(); + let client = reqwest::Client::new(); + let actor_json = client.get(actor_url) + .header(reqwest::header::ACCEPT, ACTIVITY_CONTENT_TYPE) + .send().await? + .text().await?; + let actor_value: Value = serde_json::from_str(&actor_json)?; + let actor: Actor = serde_json::from_value(actor_value.clone())?; + let (avatar, banner) = fetch_avatar_and_banner(&actor, media_dir).await?; + let actor_address = format!( + "{}@{}", + actor.preferred_username, + actor_host, + ); + let profile_data = ProfileCreateData { + username: actor.preferred_username, + display_name: Some(actor.name), + acct: actor_address, + bio: actor.summary, + avatar: avatar, + banner: banner, + actor: Some(actor_value), + }; + Ok(profile_data) +} + +pub async fn fetch_attachment( + url: &str, + output_dir: &PathBuf, +) -> Result { + let response = reqwest::get(url).await?; + let file_data = response.bytes().await?; + let file_name = save_file(file_data.to_vec(), output_dir)?; + Ok(file_name) +} diff --git a/src/activitypub/mod.rs b/src/activitypub/mod.rs new file mode 100644 index 0000000..cabadb7 --- /dev/null +++ b/src/activitypub/mod.rs @@ -0,0 +1,8 @@ +pub mod activity; +pub mod actor; +pub mod constants; +pub mod deliverer; +pub mod fetcher; +mod receiver; +pub mod views; +mod vocabulary; diff --git a/src/activitypub/receiver.rs b/src/activitypub/receiver.rs new file mode 100644 index 0000000..3809b98 --- /dev/null +++ b/src/activitypub/receiver.rs @@ -0,0 +1,210 @@ +use regex::Regex; +use serde_json::Value; +use uuid::Uuid; + +use crate::config::Config; +use crate::database::{Pool, get_database_client}; +use crate::errors::{HttpError, ValidationError}; +use crate::models::attachments::queries::create_attachment; +use crate::models::posts::types::PostCreateData; +use crate::models::posts::queries::create_post; +use crate::models::profiles::queries::{ + get_profile_by_actor_id, + get_profile_by_acct, + update_profile, +}; +use crate::models::profiles::types::ProfileUpdateData; +use crate::models::relationships::queries::{accept_follow_request, follow, unfollow}; +use crate::models::users::queries::get_user_by_id; +use super::activity::{Object, Activity, create_activity_accept_follow}; +use super::actor::Actor; +use super::deliverer::deliver_activity; +use super::fetcher::{fetch_avatar_and_banner, fetch_attachment}; +use super::vocabulary::*; + +fn parse_actor_id(actor_id: &str) -> Result { + let url_regexp = Regex::new(r"^https?://.+/users/(?P[0-9a-z_]+)$").unwrap(); + let url_caps = url_regexp.captures(&actor_id) + .ok_or(ValidationError("invalid actor ID"))?; + let username = url_caps.name("username") + .ok_or(ValidationError("invalid actor ID"))? + .as_str() + .to_owned(); + Ok(username) +} + +fn parse_object_id(object_id: &str) -> Result { + let url_regexp = Regex::new(r"^https?://.+/objects/(?P[0-9a-f-]+)$").unwrap(); + let url_caps = url_regexp.captures(&object_id) + .ok_or(ValidationError("invalid object ID"))?; + let object_uuid: Uuid = url_caps.name("uuid") + .ok_or(ValidationError("invalid object ID"))? + .as_str().parse() + .map_err(|_| ValidationError("invalid object ID"))?; + Ok(object_uuid) +} + +pub async fn receive_activity( + config: &Config, + db_pool: &Pool, + _username: String, + activity_raw: Value, +) -> Result<(), HttpError> { + let activity: Activity = serde_json::from_value(activity_raw) + .map_err(|_| ValidationError("invalid activity"))?; + let activity_type = activity.activity_type; + let object_type = activity.object.get("type") + .and_then(|val| val.as_str()) + .unwrap_or("Unknown") + .to_owned(); + let db_client = &mut **get_database_client(&db_pool).await?; + match (activity_type.as_str(), object_type.as_str()) { + (ACCEPT, FOLLOW) => { + let object: Object = serde_json::from_value(activity.object) + .map_err(|_| ValidationError("invalid object"))?; + // TODO: reject if object ID contains wrong instance URI + let follow_request_id = parse_object_id(&object.id)?; + accept_follow_request(db_client, &follow_request_id).await?; + }, + (CREATE, NOTE) => { + let object: Object = serde_json::from_value(activity.object) + .map_err(|_| ValidationError("invalid object"))?; + let attributed_to = object.attributed_to + .ok_or(ValidationError("unattributed note"))?; + let author = get_profile_by_actor_id(db_client, &attributed_to).await?; + let content = object.content + .ok_or(ValidationError("no content"))?; + let mut attachments: Vec = Vec::new(); + if let Some(list) = object.attachment { + let mut downloaded: Vec<(String, String)> = Vec::new(); + let output_dir = config.media_dir(); + for attachment in list { + let file_name = fetch_attachment(&attachment.url, &output_dir).await + .map_err(|_| ValidationError("failed to fetch attachment"))?; + log::info!("downloaded attachment {}", attachment.url); + downloaded.push((file_name, attachment.media_type)); + } + for (file_name, media_type) in downloaded { + let db_attachment = create_attachment( + db_client, + &author.id, + Some(media_type), + file_name, + ).await?; + attachments.push(db_attachment.id); + } + } + let post_data = PostCreateData { + content, + attachments: attachments, + created_at: object.published, + }; + create_post(db_client, &author.id, post_data).await?; + }, + (FOLLOW, _) => { + let source_profile = get_profile_by_actor_id(db_client, &activity.actor).await?; + let source_actor_value = source_profile.actor_json.ok_or(HttpError::InternalError)?; + let source_actor: Actor = serde_json::from_value(source_actor_value) + .map_err(|_| HttpError::InternalError)?; + let target_actor_id = activity.object.as_str() + .ok_or(ValidationError("invalid object"))?; + // TODO: reject if object ID contains wrong instance URI + let target_username = parse_actor_id(&target_actor_id)?; + let target_profile = get_profile_by_acct(db_client, &target_username).await?; + // Create and send 'Accept' activity + let target_user = get_user_by_id(db_client, &target_profile.id).await?; + let new_activity = create_activity_accept_follow(&config, &target_profile, &activity.id); + // Save relationship + follow(db_client, &source_profile.id, &target_profile.id).await?; + + // Send activity + let recipients = vec![source_actor]; + let config_clone = config.clone(); + actix_rt::spawn(async move { + deliver_activity( + &config_clone, + &target_user, + new_activity, + recipients, + ).await; + }); + }, + (UNDO, FOLLOW) => { + let object: Object = serde_json::from_value(activity.object) + .map_err(|_| ValidationError("invalid object"))?; + let source_profile = get_profile_by_actor_id(db_client, &activity.actor).await?; + let target_actor_id = object.object + .ok_or(ValidationError("invalid object"))?; + // TODO: reject if actor ID contains wrong instance URI + let target_username = parse_actor_id(&target_actor_id)?; + let target_profile = get_profile_by_acct(db_client, &target_username).await?; + unfollow(db_client, &source_profile.id, &target_profile.id).await?; + }, + (UPDATE, PERSON) => { + let actor: Actor = serde_json::from_value(activity.object) + .map_err(|_| ValidationError("invalid actor data"))?; + let profile = get_profile_by_actor_id(db_client, &actor.id).await?; + let (avatar, banner) = fetch_avatar_and_banner(&actor, &config.media_dir()).await + .map_err(|_| ValidationError("failed to fetch image"))?; + let mut profile_data = ProfileUpdateData { + display_name: Some(actor.name), + bio: actor.summary.clone(), + bio_source: actor.summary, + avatar, + banner, + }; + profile_data.clean()?; + update_profile(db_client, &profile.id, profile_data).await?; + }, + _ => { + return Err(HttpError::ValidationError("activity type is not supported".into())); + }, + }; + log::info!( + "processed {}({}) from {}", + activity_type, + object_type, + activity.actor, + ); + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_parse_actor_id() { + let username = parse_actor_id("https://example.org/users/test").unwrap(); + assert_eq!(username, "test".to_string()); + } + + #[test] + fn test_parse_actor_id_wrong_path() { + let error = parse_actor_id("https://example.org/user/test").unwrap_err(); + assert_eq!(error.to_string(), "invalid actor ID"); + } + + #[test] + fn test_parse_actor_id_invalid_username() { + let error = parse_actor_id("https://example.org/users/tes-t").unwrap_err(); + assert_eq!(error.to_string(), "invalid actor ID"); + } + + #[test] + fn test_parse_object_id() { + let expected_uuid = Uuid::new_v4(); + let object_id = format!( + "https://example.org/objects/{}", + expected_uuid, + ); + let object_uuid = parse_object_id(&object_id).unwrap(); + assert_eq!(object_uuid, expected_uuid); + } + + #[test] + fn test_parse_object_id_invalid_uuid() { + let error = parse_object_id("https://example.org/objects/1234").unwrap_err(); + assert_eq!(error.to_string(), "invalid object ID"); + } +} diff --git a/src/activitypub/views.rs b/src/activitypub/views.rs new file mode 100644 index 0000000..d2a1f9c --- /dev/null +++ b/src/activitypub/views.rs @@ -0,0 +1,129 @@ +use actix_web::{ + get, post, web, + HttpRequest, HttpResponse, Scope, +}; +use serde::Deserialize; +use uuid::Uuid; + +use crate::config::Config; +use crate::database::{Pool, get_database_client}; +use crate::errors::HttpError; +use crate::http_signatures::verify::verify_http_signature; +use crate::models::users::queries::get_user_by_name; +use super::activity::OrderedCollection; +use super::actor::get_actor_object; +use super::constants::ACTIVITY_CONTENT_TYPE; +use super::receiver::receive_activity; + +pub fn get_actor_url(instance_url: &str, username: &str) -> String { + format!("{}/users/{}", instance_url, username) +} + +pub fn get_inbox_url(instance_url: &str, username: &str) -> String { + format!("{}/users/{}/inbox", instance_url, username) +} + +pub fn get_outbox_url(instance_url: &str, username: &str) -> String { + format!("{}/users/{}/outbox", instance_url, username) +} + +pub fn get_followers_url(instance_url: &str, username: &str) -> String { + format!("{}/users/{}/followers", instance_url, username) +} + +pub fn get_following_url(instance_url: &str, username: &str) -> String { + format!("{}/users/{}/following", instance_url, username) +} + +pub fn get_object_url(instance_url: &str, object_uuid: &Uuid) -> String { + format!("{}/objects/{}", instance_url, object_uuid) +} + +#[get("")] +async fn get_actor( + config: web::Data, + db_pool: web::Data, + web::Path(username): web::Path, +) -> Result { + let db_client = &**get_database_client(&db_pool).await?; + let user = get_user_by_name(db_client, &username).await?; + let actor = get_actor_object(&config, &user)?; + let response = HttpResponse::Ok() + .content_type(ACTIVITY_CONTENT_TYPE) + .json(actor); + Ok(response) +} + +#[post("/inbox")] +async fn inbox( + config: web::Data, + db_pool: web::Data, + request: HttpRequest, + web::Path(username): web::Path, + activity: web::Json, +) -> Result { + log::info!("received to '{}' inbox: {}", username, activity); + if let Err(err) = verify_http_signature(&config, &db_pool, &request).await { + log::warn!("invalid signature: {}", err); + } + receive_activity(&config, &db_pool, username, activity.into_inner()).await?; + Ok(HttpResponse::Ok().body("success")) +} + +#[derive(Deserialize)] +struct CollectionQueryParams { + page: Option, +} + +#[get("/followers")] +async fn followers_collection( + config: web::Data, + web::Path(username): web::Path, + query_params: web::Query, +) -> Result { + if query_params.page.is_some() { + // Social graph is not available + return Err(HttpError::PermissionError); + } + let collection_url = get_followers_url(&config.instance_url(), &username); + let collection = OrderedCollection::new(collection_url); + let response = HttpResponse::Ok() + .content_type(ACTIVITY_CONTENT_TYPE) + .json(collection); + Ok(response) +} + +#[get("/following")] +async fn following_collection( + config: web::Data, + web::Path(username): web::Path, + query_params: web::Query, +) -> Result { + if query_params.page.is_some() { + // Social graph is not available + return Err(HttpError::PermissionError); + } + let collection_url = get_following_url(&config.instance_url(), &username); + let collection = OrderedCollection::new(collection_url); + let response = HttpResponse::Ok() + .content_type(ACTIVITY_CONTENT_TYPE) + .json(collection); + Ok(response) +} + +pub fn activitypub_scope() -> Scope { + web::scope("/users/{username}") + .service(get_actor) + .service(inbox) + .service(followers_collection) + .service(following_collection) +} + +#[get("/objects/{object_id}")] +pub async fn get_object( + web::Path(_object_id): web::Path, +) -> Result { + // WARNING: activities/objects are not stored + let response = HttpResponse::Gone().body(""); + Ok(response) +} diff --git a/src/activitypub/vocabulary.rs b/src/activitypub/vocabulary.rs new file mode 100644 index 0000000..a2409a6 --- /dev/null +++ b/src/activitypub/vocabulary.rs @@ -0,0 +1,14 @@ +// Activity types +pub const ACCEPT: &str = "Accept"; +pub const CREATE: &str = "Create"; +pub const FOLLOW: &str = "Follow"; +pub const UNDO: &str = "Undo"; +pub const UPDATE: &str = "Update"; + +// Actor types +pub const PERSON: &str = "Person"; + +// Object types +pub const DOCUMENT: &str = "Document"; +pub const IMAGE: &str = "Image"; +pub const NOTE: &str = "Note"; diff --git a/src/bin/mitractl.rs b/src/bin/mitractl.rs new file mode 100644 index 0000000..0a45c7c --- /dev/null +++ b/src/bin/mitractl.rs @@ -0,0 +1,87 @@ +use clap::Clap; +use tokio; +use uuid::Uuid; + +use mitra::config; +use mitra::database::{create_pool, get_database_client}; +use mitra::database::migrate::apply_migrations; +use mitra::ethereum::utils::generate_ethereum_address; +use mitra::logger::configure_logger; +use mitra::models::profiles::queries as profiles; +use mitra::models::users::queries::{ + generate_invite_code, + get_invite_codes, +}; + +/// Admin CLI tool +#[derive(Clap)] +struct Opts { + #[clap(subcommand)] + subcmd: SubCommand, +} + +#[derive(Clap)] +enum SubCommand { + DeleteProfile(DeleteProfile), + GenerateInviteCode(GenerateInviteCode), + ListInviteCodes(ListInviteCodes), + GenerateEthereumAddress(GenerateEthereumAddress), +} + +/// Delete profile +#[derive(Clap)] +struct DeleteProfile { + /// Print debug info + #[clap(short)] + id: Uuid, +} + +/// Generate invite code +#[derive(Clap)] +struct GenerateInviteCode { } + +/// List invite codes +#[derive(Clap)] +struct ListInviteCodes { } + +/// Generate ethereum address +#[derive(Clap)] +struct GenerateEthereumAddress { } + +#[tokio::main] +async fn main() { + let config = config::parse_config(); + configure_logger(); + let db_pool = create_pool(&config.database_url); + apply_migrations(&db_pool).await; + let db_client = get_database_client(&db_pool).await.unwrap(); + let opts: Opts = Opts::parse(); + + match opts.subcmd { + SubCommand::DeleteProfile(subopts) => { + profiles::delete_profile(&**db_client, &subopts.id).await.unwrap(); + println!("profile deleted"); + }, + SubCommand::GenerateInviteCode(_) => { + let invite_code = generate_invite_code(&**db_client).await.unwrap(); + println!("generated invite code: {}", invite_code); + }, + SubCommand::ListInviteCodes(_) => { + let invite_codes = get_invite_codes(&**db_client).await.unwrap(); + if invite_codes.len() == 0 { + println!("no invite codes found"); + return; + } + for code in invite_codes { + println!("{}", code); + } + }, + SubCommand::GenerateEthereumAddress(_) => { + let (private_key, address) = generate_ethereum_address(); + println!( + "address {:?}; private key {}", + address, private_key, + ); + }, + }; +} diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..8e24d40 --- /dev/null +++ b/src/config.rs @@ -0,0 +1,159 @@ +use std::path::PathBuf; +use std::str::FromStr; + +use serde::{de, Deserialize, Deserializer}; +use url::{Url, ParseError as UrlParseError}; + +use crate::errors::ConversionError; + +#[derive(Clone, Debug)] +pub enum Environment { + Development, + Production, +} + +impl FromStr for Environment { + type Err = ConversionError; + + fn from_str(val: &str) -> Result { + let environment = match val { + "development" => Environment::Development, + "production" => Environment::Production, + _ => return Err(ConversionError), + }; + Ok(environment) + } +} + +fn environment_from_str<'de, D>(deserializer: D) -> Result +where + D: Deserializer<'de>, +{ + let s: String = Deserialize::deserialize(deserializer)?; + Environment::from_str(&s).map_err(de::Error::custom) +} + +#[derive(Clone)] +pub struct EnvConfig { + pub environment: Option, + pub config_path: String, + pub crate_version: String, +} + +fn parse_env() -> EnvConfig { + dotenv::from_filename(".env.local").ok(); + dotenv::dotenv().ok(); + let environment_str = std::env::var("ENVIRONMENT").ok(); + let environment = environment_str + .map(|val| Environment::from_str(&val).expect("invalid environment type")); + let config_path = std::env::var("CONFIG_PATH") + .unwrap_or("config.yaml".to_string()); + let crate_version = env!("CARGO_PKG_VERSION").to_string(); + EnvConfig { + environment, + config_path, + crate_version, + } +} + +fn default_environment() -> Environment { Environment::Development } + +fn default_storage_dir() -> PathBuf { PathBuf::from("files") } + +fn default_contract_dir() -> PathBuf { PathBuf::from("contracts") } + +fn default_cookie_name() -> String { "session".to_string() } + +#[derive(Clone, Deserialize)] +pub struct EthereumContract { + pub address: String, + pub chain_id: u32, + pub signing_key: String, +} + +#[derive(Clone, Deserialize)] +pub struct Config { + #[serde(default = "default_environment")] + #[serde(deserialize_with = "environment_from_str")] + pub environment: Environment, + + #[serde(skip)] + pub version: String, + + // Core settings + pub database_url: String, + + #[serde(default = "default_storage_dir")] + pub storage_dir: PathBuf, + + pub http_host: String, + pub http_port: u32, + + #[serde(default = "default_cookie_name")] + pub cookie_name: String, + + pub cookie_secret_key: String, + + // Instance info + pub instance_uri: String, + pub instance_title: String, + pub instance_short_description: String, + pub instance_description: String, + + #[serde(default)] + pub registrations_open: bool, // default is false + + pub login_message: String, + + // Ethereum & IPFS + #[serde(default = "default_contract_dir")] + pub contract_dir: PathBuf, + + pub ethereum_json_rpc_url: Option, + pub ethereum_explorer_url: Option, + pub ethereum_contract: Option, + pub ipfs_api_url: Option, + pub ipfs_gateway_url: Option, +} + +impl Config { + fn try_instance_url(&self) -> Result { + // TODO: allow http in production + let scheme = match self.environment { + Environment::Development => "http", + Environment::Production => "https", + }; + let url_str = format!("{}://{}", scheme, self.instance_uri); + Url::parse(&url_str) + } + + pub fn instance_url(&self) -> String { + self.try_instance_url().unwrap().origin().ascii_serialization() + } + + pub fn media_dir(&self) -> PathBuf { + self.storage_dir.join("media") + } +} + +pub fn parse_config() -> Config { + let env = parse_env(); + let config_yaml = std::fs::read_to_string(env.config_path) + .expect("failed to load config file"); + let mut config = serde_yaml::from_str::(&config_yaml) + .expect("invalid yaml data"); + // Override environment parameter in config if env variable is set + config.environment = env.environment.unwrap_or(config.environment); + // Set_version + config.version = env.crate_version; + // Validate config + if !config.storage_dir.exists() { + panic!("storage_dir does not exist"); + }; + if !config.contract_dir.exists() { + panic!("contract directory does not exist"); + }; + config.try_instance_url().expect("invalid instance URI"); + + config +} diff --git a/src/database/migrate.rs b/src/database/migrate.rs new file mode 100644 index 0000000..49a803b --- /dev/null +++ b/src/database/migrate.rs @@ -0,0 +1,23 @@ +use crate::database::Pool; + +mod embedded { + use refinery::embed_migrations; + embed_migrations!("migrations"); +} + +pub async fn apply_migrations(pool: &Pool) { + // https://github.com/rust-db/refinery/issues/105 + let mut client_object = pool.get().await.unwrap(); + let client = &mut *(*client_object); + let migration_report = embedded::migrations::runner() + .run_async(client) + .await.unwrap(); + + for migration in migration_report.applied_migrations() { + log::info!( + "Migration Applied - Name: {}, Version: {}", + migration.name(), + migration.version(), + ); + } +} diff --git a/src/database/mod.rs b/src/database/mod.rs new file mode 100644 index 0000000..888779b --- /dev/null +++ b/src/database/mod.rs @@ -0,0 +1,26 @@ +pub mod migrate; + +pub type Pool = deadpool_postgres::Pool; + +pub fn create_pool(database_url: &str) -> Pool { + let pool = deadpool_postgres::Pool::new( + deadpool_postgres::Manager::new( + database_url.parse().expect("invalid database URL"), + tokio_postgres::NoTls, + ), + // https://wiki.postgresql.org/wiki/Number_Of_Database_Connections + num_cpus::get() * 2, + ); + pool +} + +use crate::errors::DatabaseError; + +pub async fn get_database_client(pool: &Pool) + -> Result +{ + // Returns wrapped client + // https://github.com/bikeshedder/deadpool/issues/56 + let client = pool.get().await?; + Ok(client) +} diff --git a/src/errors.rs b/src/errors.rs new file mode 100644 index 0000000..583e3ab --- /dev/null +++ b/src/errors.rs @@ -0,0 +1,100 @@ +use actix_web::{ + dev::HttpResponseBuilder, + http::StatusCode, + HttpResponse, + error::ResponseError, +}; +use serde::Serialize; + +#[derive(thiserror::Error, Debug)] +#[error("conversion error")] +pub struct ConversionError; + +#[derive(thiserror::Error, Debug)] +#[error("{0}")] +pub struct ValidationError(pub &'static str); + +#[derive(thiserror::Error, Debug)] +pub enum DatabaseError { + #[error("database pool error")] + DatabasePoolError(#[from] deadpool_postgres::PoolError), + + #[error("database client error")] + DatabaseClientError(#[from] tokio_postgres::Error), + + #[error("database type error")] + DatabaseTypeError(#[from] ConversionError), + + #[error("{0}")] + NotFound(&'static str), // object type + + #[error("{0}")] + AlreadyExists(&'static str), // object type +} + +#[derive(thiserror::Error, Debug)] +pub enum HttpError { + #[error(transparent)] + ActixError(#[from] actix_web::Error), + + #[error("database error")] + DatabaseError(#[source] DatabaseError), + + #[error("{0}")] + ValidationError(String), + + #[error("{0}")] + ValidationErrorAuto(#[from] ValidationError), + + #[error("{0}")] + SessionError(&'static str), + + #[error("permission error")] + PermissionError, + + #[error("{0} not found")] + NotFoundError(&'static str), + + #[error("operation not supported")] + NotSupported, + + #[error("internal error")] + InternalError, +} + +impl From for HttpError { + fn from(err: DatabaseError) -> Self { + match err { + DatabaseError::NotFound(name) => HttpError::NotFoundError(name), + DatabaseError::AlreadyExists(name) => HttpError::ValidationError( + format!("{} already exists", name), + ), + _ => HttpError::DatabaseError(err), + } + } +} + +#[derive(Serialize)] +struct ErrorInfo { + message: String, +} + +impl ResponseError for HttpError { + fn error_response(&self) -> HttpResponse { + let err = ErrorInfo { message: self.to_string() }; + HttpResponseBuilder::new(self.status_code()).json(err) + } + + fn status_code(&self) -> StatusCode { + match self { + HttpError::ActixError(err) => err.as_response_error().status_code(), + HttpError::ValidationError(_) => StatusCode::BAD_REQUEST, + HttpError::ValidationErrorAuto(_) => StatusCode::BAD_REQUEST, + HttpError::SessionError(_) => StatusCode::UNAUTHORIZED, + HttpError::PermissionError => StatusCode::FORBIDDEN, + HttpError::NotFoundError(_) => StatusCode::NOT_FOUND, + HttpError::NotSupported => StatusCode::IM_A_TEAPOT, + _ => StatusCode::INTERNAL_SERVER_ERROR, + } + } +} diff --git a/src/ethereum/api.rs b/src/ethereum/api.rs new file mode 100644 index 0000000..e4ef4a4 --- /dev/null +++ b/src/ethereum/api.rs @@ -0,0 +1,10 @@ +use web3::{ + api::Web3, + transports::Http, +}; + +pub fn connect(json_rpc_url: &str) -> Result, web3::Error> { + let transport = web3::transports::Http::new(json_rpc_url)?; + let connection = web3::Web3::new(transport); + Ok(connection) +} diff --git a/src/ethereum/mod.rs b/src/ethereum/mod.rs new file mode 100644 index 0000000..90ba97a --- /dev/null +++ b/src/ethereum/mod.rs @@ -0,0 +1,3 @@ +mod api; +pub mod nft; +pub mod utils; diff --git a/src/ethereum/nft.rs b/src/ethereum/nft.rs new file mode 100644 index 0000000..d66a7b5 --- /dev/null +++ b/src/ethereum/nft.rs @@ -0,0 +1,232 @@ +use std::convert::TryInto; +use std::fs; +use std::path::PathBuf; + +use web3::{ + api::Web3, + contract::{Contract, Options}, + ethabi::{Event, EventParam, ParamType, RawLog, token::Token, encode}, + transports::Http, + types::{BlockNumber, FilterBuilder, H256, U256}, +}; + +use crate::config::{Config, EthereumContract}; +use crate::database::{Pool, get_database_client}; +use crate::errors::DatabaseError; +use crate::ipfs::utils::parse_ipfs_url; +use crate::models::posts::queries::{ + get_post_by_ipfs_cid, + update_post, + is_waiting_for_token, +}; +use super::api::connect; +use super::utils::{ + parse_address, sign_message, + AddressError, SignatureData, SignatureError, +}; + +pub const COLLECTIBLE: &str = "Collectible"; +pub const MINTER: &str = "Minter"; + +#[derive(thiserror::Error, Debug)] +pub enum EthereumError { + #[error("io error")] + IoError(#[from] std::io::Error), + + #[error("json error")] + JsonError(#[from] serde_json::Error), + + #[error("invalid address")] + InvalidAddress(#[from] AddressError), + + #[error(transparent)] + Web3Error(#[from] web3::Error), + + #[error("artifact error")] + ArtifactError, + + #[error("abi error")] + AbiError(#[from] web3::ethabi::Error), + + #[error("contract error")] + ContractError(#[from] web3::contract::Error), + + #[error("improprely configured")] + ImproperlyConfigured, + + #[error("data conversion error")] + ConversionError, + + #[error("token uri parsing error")] + TokenUriParsingError, + + #[error(transparent)] + DatabaseError(#[from] DatabaseError), + + #[error("signature error")] + SigError(#[from] SignatureError), +} + +fn load_abi( + contract_dir: &PathBuf, + contract_name: &str, +) -> Result, EthereumError> { + let contract_artifact_path = contract_dir.join(format!("{}.json", contract_name)); + let contract_artifact = fs::read_to_string(contract_artifact_path)?; + let contract_artifact_value: serde_json::Value = serde_json::from_str(&contract_artifact)?; + let contract_abi = contract_artifact_value.get("abi") + .ok_or(EthereumError::ArtifactError)? + .to_string().as_bytes().to_vec(); + Ok(contract_abi) +} + +pub async fn get_nft_contract( + config: &Config, +) -> Result<(Web3, Contract), EthereumError> { + let json_rpc_url = config.ethereum_json_rpc_url.as_ref() + .ok_or(EthereumError::ImproperlyConfigured)?; + let web3 = connect(json_rpc_url)?; + let ethereum_config = config.ethereum_contract.as_ref() + .ok_or(EthereumError::ImproperlyConfigured)?; + + let minter_abi = load_abi(&config.contract_dir, MINTER)?; + let minter_address = parse_address(ðereum_config.address)?; + let minter = Contract::from_json( + web3.eth(), + minter_address, + &minter_abi, + )?; + + let token_address = minter.query("token", (), None, Options::default(), None).await?; + let token_abi = load_abi(&config.contract_dir, COLLECTIBLE)?; + let token = Contract::from_json( + web3.eth(), + token_address, + &token_abi, + )?; + log::info!("NFT contract address is {:?}", token.address()); + Ok((web3, token)) +} + +#[derive(Debug)] +struct TokenTransfer { + tx_id: Option, + from: Token, + to: Token, + token_id: Token, +} + +pub async fn process_events( + web3: &Web3, + contract: &Contract, + db_pool: &Pool, +) -> Result<(), EthereumError> { + let db_client = &**get_database_client(&db_pool).await?; + if !is_waiting_for_token(db_client).await? { + return Ok(()); + } + + // Search for Transfer events + let event_abi_params = vec![ + EventParam { + name: "from".to_string(), + kind: ParamType::Address, + indexed: true, + }, + EventParam { + name: "to".to_string(), + kind: ParamType::Address, + indexed: true, + }, + EventParam { + name: "tokenId".to_string(), + kind: ParamType::Uint(256), + indexed: true, + }, + ]; + let event_abi = Event { + name: "Transfer".to_string(), + inputs: event_abi_params, + anonymous: false, + }; + let filter = FilterBuilder::default() + .address(vec![contract.address()]) + .topics(Some(vec![event_abi.signature()]), None, None, None) + .from_block(BlockNumber::Earliest) + .build(); + let logs = web3.eth().logs(filter).await?; + + // Convert web3 logs into ethabi logs + let transfers: Vec = logs.iter().map(|log| { + let raw_log = RawLog { + topics: log.topics.clone(), + data: log.data.clone().0, + }; + match event_abi.parse_log(raw_log) { + Ok(event) => { + let params = event.params; + let transfer = TokenTransfer { + tx_id: log.transaction_hash, + from: params[0].value.clone(), + to: params[1].value.clone(), + token_id: params[2].value.clone(), + }; + Ok(transfer) + }, + Err(err) => Err(err), + } + }).collect::>()?; + for transfer in transfers { + let from_address = transfer.from.into_address() + .ok_or(EthereumError::ConversionError)?; + if from_address.is_zero() { + // Mint event found + let token_id_u256 = transfer.token_id.into_uint() + .ok_or(EthereumError::ConversionError)?; + let token_uri_result = contract.query("tokenURI", (token_id_u256,), None, Options::default(), None); + let token_uri: String = token_uri_result.await?; + let tx_id_h256 = transfer.tx_id.ok_or(EthereumError::ConversionError)?; + let tx_id = hex::encode(tx_id_h256.as_bytes()); + let ipfs_cid = parse_ipfs_url(&token_uri) + .map_err(|_| EthereumError::TokenUriParsingError)?; + let mut post = match get_post_by_ipfs_cid(db_client, &ipfs_cid).await { + Ok(post) => post, + Err(err) => { + // Post not found or some other error + log::error!("{}", err); + continue; + }, + }; + if post.token_id.is_none() { + log::info!("post {} was tokenized via {}", post.id, tx_id); + let token_id: i32 = token_id_u256.try_into() + .map_err(|_| EthereumError::ConversionError)?; + post.token_id = Some(token_id); + post.token_tx_id = Some(tx_id); + update_post(db_client, &post).await?; + }; + }; + }; + Ok(()) +} + +pub fn create_mint_signature( + contract_config: &EthereumContract, + user_address: &str, + token_uri: &str, +) -> Result { + let contract_address = parse_address(&contract_config.address)?; + let user_address = parse_address(user_address)?; + let chain_id: U256 = contract_config.chain_id.into(); + let chain_id_token = Token::Uint(chain_id.into()); + let chain_id_bin = encode(&[chain_id_token]); + let message = [ + &chain_id_bin, + contract_address.as_bytes(), + "mint".as_bytes(), + user_address.as_bytes(), + token_uri.as_bytes(), + ].concat(); + let signature = sign_message(&contract_config.signing_key, &message)?; + Ok(signature) +} diff --git a/src/ethereum/utils.rs b/src/ethereum/utils.rs new file mode 100644 index 0000000..b07ad95 --- /dev/null +++ b/src/ethereum/utils.rs @@ -0,0 +1,59 @@ +use std::str::FromStr; + +use secp256k1::{Error as KeyError, SecretKey, rand::rngs::OsRng}; +use serde::Serialize; +use web3::{ + signing::{keccak256, Key, SigningError}, + types::Address, +}; + +pub fn generate_ethereum_address() -> (SecretKey, Address) { + let mut rng = OsRng::new().expect("failed to initialize RNG"); + let secret_key = SecretKey::new(&mut rng); + let address = Box::new(secret_key).address(); + (secret_key, address) +} + +#[derive(thiserror::Error, Debug)] +#[error("address error")] +pub struct AddressError; + +pub fn parse_address(address: &str) -> Result { + Address::from_str(address).map_err(|_| AddressError) +} + +#[derive(Serialize)] +pub struct SignatureData { + pub v: u64, + pub r: String, + pub s: String, +} + +#[derive(thiserror::Error, Debug)] +pub enum SignatureError { + #[error("invalid key")] + InvalidKey(#[from] KeyError), + + #[error("signing error")] + SigningError(#[from] SigningError), +} + +pub fn sign_message( + signing_key: &str, + message: &[u8], +) -> Result { + let key = SecretKey::from_str(&signing_key)?; + let message_hash = keccak256(message); + let eip_191_message = [ + "\x19Ethereum Signed Message:\n32".as_bytes(), + &message_hash, + ].concat(); + let eip_191_message_hash = keccak256(&eip_191_message); + let signature = Box::new(key).sign(&eip_191_message_hash, None)?; + let signature_data = SignatureData { + v: signature.v, + r: hex::encode(signature.r.as_bytes()), + s: hex::encode(signature.s.as_bytes()), + }; + Ok(signature_data) +} diff --git a/src/http_signatures/create.rs b/src/http_signatures/create.rs new file mode 100644 index 0000000..0c731f5 --- /dev/null +++ b/src/http_signatures/create.rs @@ -0,0 +1,96 @@ +use chrono::Utc; +use rsa::RsaPrivateKey; + +use crate::utils::crypto::{sign_message, get_message_digest}; + +pub struct SignatureHeaders { + pub host: String, + pub date: String, + pub digest: String, + pub signature: String, +} + +#[derive(thiserror::Error, Debug)] +pub enum SignatureError { + #[error("invalid request url")] + UrlError, + + #[error("signature error")] + SignatureError(#[from] rsa::errors::Error), +} + +/// Creates HTTP signature according to the old HTTP Signatures Spec: +/// https://datatracker.ietf.org/doc/html/draft-cavage-http-signatures. +pub fn create_http_signature( + request_url: &str, + request_body: &str, + actor_key: RsaPrivateKey, + actor_key_id: String, +) -> Result { + let request_url_object = url::Url::parse(request_url) + .map_err(|_| SignatureError::UrlError)?; + let host = request_url_object.host_str() + .ok_or(SignatureError::UrlError)?; + let date = Utc::now().to_rfc2822(); + let digest = get_message_digest(request_body); + let message = format!( + "(request-target): post {}\nhost: {}\ndate: {}\ndigest: {}", + request_url_object.path(), + host, + date, + digest, + ); + let headers_parameter = &["(request-target)", "host", "date", "digest"]; + let signature_parameter = sign_message(&actor_key, &message)?; + let signature_header = format!( + r#"keyId="{}",headers="{}",signature="{}""#, + actor_key_id, + headers_parameter.join(" "), + signature_parameter, + ); + let headers = SignatureHeaders { + host: host.to_string(), + date, + digest, + signature: signature_header, + }; + Ok(headers) +} + +#[cfg(test)] +mod tests { + use rand::rngs::OsRng; + use super::*; + + #[test] + fn test_create_signature() { + let request_url = "https://example.org/inbox"; + let request_body = "{}"; + let actor_key = RsaPrivateKey::new(&mut OsRng, 512).unwrap(); + let actor_key_id = "https://myserver.org/actor#main-key"; + + let result = create_http_signature( + request_url, + request_body, + actor_key, + actor_key_id.to_string(), + ); + assert_eq!(result.is_ok(), true); + + let headers = result.unwrap(); + assert_eq!(headers.host, "example.org"); + assert_eq!( + headers.digest, + "SHA-256=RBNvo1WzZ4oRRq0W9+hknpT7T8If536DEMBg9hyq/4o=", + ); + let expected_signature_header = concat!( + r#"keyId="https://myserver.org/actor#main-key","#, + r#"headers="(request-target) host date digest","#, + r#"signature=""#, + ); + assert_eq!( + headers.signature.starts_with(expected_signature_header), + true, + ); + } +} diff --git a/src/http_signatures/mod.rs b/src/http_signatures/mod.rs new file mode 100644 index 0000000..c414f99 --- /dev/null +++ b/src/http_signatures/mod.rs @@ -0,0 +1,2 @@ +pub mod create; +pub mod verify; diff --git a/src/http_signatures/verify.rs b/src/http_signatures/verify.rs new file mode 100644 index 0000000..87ed864 --- /dev/null +++ b/src/http_signatures/verify.rs @@ -0,0 +1,197 @@ +use actix_web::{ + HttpRequest, + http::{HeaderMap, Method, Uri}, +}; +use regex::Regex; + +use crate::activitypub::actor::Actor; +use crate::activitypub::fetcher::fetch_profile_by_actor_id; +use crate::config::Config; +use crate::database::{Pool, get_database_client}; +use crate::errors::DatabaseError; +use crate::models::profiles::queries::{ + get_profile_by_actor_id, + create_profile, +}; +use crate::utils::crypto::{deserialize_public_key, verify_signature}; + +#[derive(thiserror::Error, Debug)] +pub enum VerificationError { + #[error("{0}")] + HeaderError(&'static str), + + #[error("{0}")] + ParseError(&'static str), + + #[error("invalid key ID")] + UrlError(#[from] url::ParseError), + + #[error("actor error")] + ActorError, + + #[error("invalid key")] + InvalidKey(#[from] rsa::pkcs8::Error), + + #[error("invalid signature")] + InvalidSignature, +} + +pub struct SignatureData { + pub actor_id: String, + pub message: String, // reconstructed message + pub signature: String, // base64-encoded signature +} + +fn parse_http_signature( + request_method: &Method, + request_uri: &Uri, + request_headers: &HeaderMap, +) -> Result { + let signature_header = request_headers.get("signature") + .ok_or(VerificationError::HeaderError("missing signature header"))? + .to_str() + .map_err(|_| VerificationError::HeaderError("invalid signature header"))?; + // TODO: support arbitrary parameter order + let signature_header_regexp_raw = concat!( + r#"keyId="(?P.+)","#, + r#"headers="(?P.+)","#, + r#"signature="(?P.+)""#, + ); + let signature_header_regexp = Regex::new(signature_header_regexp_raw).unwrap(); + let signature_header_caps = signature_header_regexp + .captures(&signature_header) + .ok_or(VerificationError::HeaderError("invalid signature header"))?; + let key_id = signature_header_caps.name("key_id") + .ok_or(VerificationError::ParseError("keyId parameter is missing"))? + .as_str() + .to_owned(); + let headers_parameter = signature_header_caps.name("headers") + .ok_or(VerificationError::ParseError("headers parameter is missing"))? + .as_str() + .to_owned(); + let signature = signature_header_caps.name("signature") + .ok_or(VerificationError::ParseError("signature is missing"))? + .as_str() + .to_owned(); + + let mut message = format!( + "(request-target): {} {}", + request_method.as_str().to_lowercase(), + request_uri, + ); + for header in headers_parameter.split(" ") { + if header == "(request-target)" { + continue; + } + let header_value = request_headers.get(header) + .ok_or(VerificationError::HeaderError("missing header"))? + .to_str() + .map_err(|_| VerificationError::HeaderError("invalid header value"))?; + let message_part = format!( + "\n{}: {}", + header, + header_value, + ); + message.push_str(&message_part); + } + + let key_url = url::Url::parse(&key_id)?; + let actor_id = &key_url[..url::Position::BeforeQuery]; + + let signature_data = SignatureData { + actor_id: actor_id.to_string(), + message, + signature, + }; + Ok(signature_data) +} + +pub async fn verify_http_signature( + config: &Config, + db_pool: &Pool, + request: &HttpRequest, +) -> Result<(), VerificationError> { + let signature_data = parse_http_signature( + request.method(), + request.uri(), + request.headers(), + )?; + + let db_client = &**get_database_client(db_pool).await + .map_err(|_| VerificationError::ActorError)?; + let actor_profile = match get_profile_by_actor_id(db_client, &signature_data.actor_id).await { + Ok(profile) => profile, + Err(err) => match err { + DatabaseError::NotFound(_) => { + let profile_data = fetch_profile_by_actor_id( + &signature_data.actor_id, + &config.media_dir(), + ).await.map_err(|err| { + log::error!("{}", err); + VerificationError::ActorError + })?; + let profile = create_profile( + db_client, + &profile_data, + ).await.map_err(|_| VerificationError::ActorError)?; + profile + }, + _ => { + return Err(VerificationError::ActorError); + }, + }, + }; + let actor_value = actor_profile.actor_json.ok_or(VerificationError::ActorError)?; + let actor: Actor = serde_json::from_value(actor_value) + .map_err(|_| VerificationError::ActorError)?; + + let public_key = deserialize_public_key(&actor.public_key.public_key_pem)?; + let is_valid_signature = verify_signature( + &public_key, + &signature_data.message, + &signature_data.signature, + ).map_err(|_| VerificationError::InvalidSignature)?; + if !is_valid_signature { + return Err(VerificationError::InvalidSignature); + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use std::str::FromStr; + use actix_web::http::{header, HeaderMap, HeaderName, HeaderValue, Uri}; + use super::*; + + #[test] + fn test_parse_signature() { + let request_method = Method::from_str("POST").unwrap(); + let request_uri = "/user/123/inbox".parse::().unwrap(); + let mut request_headers = HeaderMap::new(); + request_headers.insert( + header::HOST, + HeaderValue::from_static("example.com"), + ); + let signature_header = concat!( + r#"keyId="https://myserver.org/actor#main-key","#, + r#"headers="(request-target) host","#, + r#"signature="test""#, + ); + request_headers.insert( + HeaderName::from_static("signature"), + HeaderValue::from_static(signature_header), + ); + + let signature_data = parse_http_signature( + &request_method, + &request_uri, + &request_headers, + ).unwrap(); + assert_eq!(signature_data.actor_id, "https://myserver.org/actor"); + assert_eq!( + signature_data.message, + "(request-target): post /user/123/inbox\nhost: example.com", + ); + assert_eq!(signature_data.signature, "test"); + } +} diff --git a/src/ipfs/mod.rs b/src/ipfs/mod.rs new file mode 100644 index 0000000..ca7140d --- /dev/null +++ b/src/ipfs/mod.rs @@ -0,0 +1,2 @@ +pub mod store; +pub mod utils; diff --git a/src/ipfs/store.rs b/src/ipfs/store.rs new file mode 100644 index 0000000..8a6c351 --- /dev/null +++ b/src/ipfs/store.rs @@ -0,0 +1,26 @@ +/// https://docs.ipfs.io/reference/http/api/ + +use reqwest::{multipart, Client}; +use serde::Deserialize; + +#[derive(Deserialize)] +#[serde(rename_all="PascalCase")] +struct ObjectAdded { + hash: String, +} + +/// Add file to IPFS. +/// Returns CID v1 of the object. +pub async fn add(ipfs_api_url: &str, data: Vec) -> Result { + let client = Client::new(); + let file_part = multipart::Part::bytes(data); + let form = multipart::Form::new().part("file", file_part); + let url = format!("{}/api/v0/add", ipfs_api_url); + let response = client.post(&url) + .query(&[("cid-version", 1)]) + .multipart(form) + .send() + .await?; + let info: ObjectAdded = response.json().await?; + Ok(info.hash) +} diff --git a/src/ipfs/utils.rs b/src/ipfs/utils.rs new file mode 100644 index 0000000..e46edac --- /dev/null +++ b/src/ipfs/utils.rs @@ -0,0 +1,20 @@ +use regex::Regex; + +pub const IPFS_LOGO: &str = "bafybeihc4hti5ix4ds2tefhy35qd4c7n5as5cazdmksrxj7ipvcxm64h54"; + +pub fn get_ipfs_url(cid: &str) -> String { + format!("ipfs://{}", cid) +} + +#[derive(thiserror::Error, Debug)] +#[error("parse error")] +pub struct ParseError; + +pub fn parse_ipfs_url(url: &str) -> Result { + let regexp = Regex::new(r"ipfs://(?P\w+)").unwrap(); + let caps = regexp.captures(&url).ok_or(ParseError)?; + let cid = caps.name("cid") + .ok_or(ParseError)? + .as_str().to_string(); + Ok(cid) +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..19f3791 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,14 @@ +pub mod activitypub; +pub mod config; +pub mod database; +mod errors; +pub mod ethereum; +mod http_signatures; +mod ipfs; +pub mod logger; +pub mod mastodon_api; +pub mod models; +pub mod nodeinfo; +pub mod scheduler; +mod utils; +pub mod webfinger; diff --git a/src/logger.rs b/src/logger.rs new file mode 100644 index 0000000..2b80a82 --- /dev/null +++ b/src/logger.rs @@ -0,0 +1,17 @@ +use chrono::Local; +use std::io::Write; + +pub fn configure_logger() -> () { + env_logger::Builder::new() + .format(|buf, record| { + writeln!(buf, + "{} {} [{}] {}", + Local::now().format("%Y-%m-%dT%H:%M:%S"), + record.target(), + record.level(), + record.args(), + ) + }) + .filter(None, log::LevelFilter::Info) + .init(); +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..c081101 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,101 @@ +use actix_cors::Cors; +use actix_session::CookieSession; +use actix_web::{ + web, + App, HttpServer, + middleware::Logger as ActixLogger, +}; + +use mitra::activitypub::views::{activitypub_scope, get_object}; +use mitra::config::{Environment, parse_config}; +use mitra::database::create_pool; +use mitra::database::migrate::apply_migrations; +use mitra::logger::configure_logger; +use mitra::mastodon_api::accounts::views::account_api_scope; +use mitra::mastodon_api::directory::views::profile_directory; +use mitra::mastodon_api::instance::views as instance_api; +use mitra::mastodon_api::media::views::media_api_scope; +use mitra::mastodon_api::search::views::search; +use mitra::mastodon_api::statuses::views::status_api_scope; +use mitra::mastodon_api::timelines::views as timeline_api; +use mitra::mastodon_api::users::views as user_api; +use mitra::nodeinfo::views as nodeinfo; +use mitra::scheduler; +use mitra::webfinger::views as webfinger; + +const MAX_UPLOAD_SIZE: usize = 1024 * 1024 * 10; + +#[actix_web::main] +async fn main() -> std::io::Result<()> { + let config = parse_config(); + configure_logger(); + let db_pool = create_pool(&config.database_url); + apply_migrations(&db_pool).await; + log::info!( + "app initialized; environment = '{:?}'", + config.environment, + ); + + scheduler::run(config.clone(), db_pool.clone()); + log::info!("scheduler started"); + + let http_socket_addr = format!( + "{}:{}", + config.http_host, + config.http_port, + ); + let num_workers = std::cmp::max(num_cpus::get(), 4); + HttpServer::new(move || { + let cors_config = match config.environment { + Environment::Development => { + Cors::permissive() + }, + Environment::Production => { + let allowed_origin = config.instance_url(); + Cors::default().allowed_origin(&allowed_origin) + .allow_any_method() + .allow_any_header() + }, + }; + let cookie_config = CookieSession::signed(config.cookie_secret_key.as_bytes()) + .name(config.cookie_name.clone()) + .max_age(86400 * 30) + .secure(true); + App::new() + .wrap(ActixLogger::new("%r : %s : %{r}a")) + .wrap(cors_config) + .wrap(cookie_config) + .data(web::PayloadConfig::default().limit(MAX_UPLOAD_SIZE)) + .data(web::JsonConfig::default().limit(MAX_UPLOAD_SIZE)) + .data(config.clone()) + .data(db_pool.clone()) + .service(actix_files::Files::new( + "/media", + config.media_dir(), + )) + .service(actix_files::Files::new( + "/contracts", + config.contract_dir.clone(), + )) + .service(user_api::create_user_view) + .service(user_api::login_view) + .service(user_api::current_user_view) + .service(user_api::logout_view) + .service(profile_directory) + .service(account_api_scope()) + .service(media_api_scope()) + .service(status_api_scope()) + .service(instance_api::instance) + .service(search) + .service(timeline_api::home_timeline) + .service(webfinger::get_descriptor) + .service(activitypub_scope()) + .service(get_object) + .service(nodeinfo::get_nodeinfo) + .service(nodeinfo::get_nodeinfo_2_0) + }) + .workers(num_workers) + .bind(http_socket_addr)? + .run() + .await +} diff --git a/src/mastodon_api/accounts/mod.rs b/src/mastodon_api/accounts/mod.rs new file mode 100644 index 0000000..a964b01 --- /dev/null +++ b/src/mastodon_api/accounts/mod.rs @@ -0,0 +1,2 @@ +pub mod types; +pub mod views; diff --git a/src/mastodon_api/accounts/types.rs b/src/mastodon_api/accounts/types.rs new file mode 100644 index 0000000..f96ee36 --- /dev/null +++ b/src/mastodon_api/accounts/types.rs @@ -0,0 +1,118 @@ +use std::path::PathBuf; + +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +use crate::models::profiles::types::{DbActorProfile, ProfileUpdateData}; +use crate::utils::files::{FileError, save_validated_b64_file, get_file_url}; + +/// https://docs.joinmastodon.org/entities/source/ +#[derive(Serialize)] +pub struct Source { + pub note: Option, +} + +/// https://docs.joinmastodon.org/entities/account/ +#[derive(Serialize)] +pub struct Account { + pub id: Uuid, + pub username: String, + pub acct: String, + pub display_name: Option, + pub created_at: DateTime, + pub note: Option, + pub avatar: Option, + pub header: Option, + pub followers_count: i32, + pub following_count: i32, + pub statuses_count: i32, + + pub source: Option, +} + +impl Account { + pub fn from_profile(profile: DbActorProfile, instance_url: &str) -> Self { + let avatar_url = profile.avatar_file_name.map(|name| get_file_url(instance_url, &name)); + let header_url = profile.banner_file_name.map(|name| get_file_url(instance_url, &name)); + let source = if profile.actor_json.is_some() { + // Remote actor + None + } else { + let source = Source { note: profile.bio_source }; + Some(source) + }; + Self { + id: profile.id, + username: profile.username, + acct: profile.acct, + display_name: profile.display_name, + created_at: profile.created_at, + note: profile.bio, + avatar: avatar_url, + header: header_url, + followers_count: profile.follower_count, + following_count: profile.following_count, + statuses_count: profile.post_count, + source, + } + } +} + +/// https://docs.joinmastodon.org/methods/accounts/ +#[derive(Deserialize)] +pub struct AccountUpdateData { + pub display_name: Option, + pub note: Option, + pub note_source: Option, + pub avatar: Option, + pub header: Option, +} + +fn process_b64_image_field_value( + form_value: Option, + db_value: Option, + output_dir: &PathBuf, +) -> Result, FileError> { + let maybe_file_name = match form_value { + Some(b64_data) => { + if b64_data == "" { + // Remove file + None + } else { + // Decode and save file + let (file_name, _) = save_validated_b64_file( + &b64_data, &output_dir, "image/", + )?; + Some(file_name) + } + }, + // Keep current value + None => db_value, + }; + Ok(maybe_file_name) +} + +impl AccountUpdateData { + pub fn into_profile_data( + self, + current_avatar: &Option, + current_banner: &Option, + media_dir: &PathBuf, + ) -> Result { + let avatar = process_b64_image_field_value( + self.avatar, current_avatar.clone(), media_dir, + )?; + let banner = process_b64_image_field_value( + self.header, current_banner.clone(), media_dir, + )?; + let profile_data = ProfileUpdateData { + display_name: self.display_name, + bio: self.note, + bio_source: self.note_source, + avatar, + banner, + }; + Ok(profile_data) + } +} diff --git a/src/mastodon_api/accounts/views.rs b/src/mastodon_api/accounts/views.rs new file mode 100644 index 0000000..a5a0ac1 --- /dev/null +++ b/src/mastodon_api/accounts/views.rs @@ -0,0 +1,207 @@ +use actix_session::Session; +use actix_web::{get, post, patch, web, HttpResponse, Scope}; +use serde::Deserialize; +use uuid::Uuid; + +use crate::activitypub::activity::{ + create_activity_follow, + create_activity_undo_follow, +}; +use crate::activitypub::actor::Actor; +use crate::activitypub::deliverer::deliver_activity; +use crate::config::Config; +use crate::database::{Pool, get_database_client}; +use crate::errors::HttpError; +use crate::mastodon_api::statuses::types::Status; +use crate::mastodon_api::users::auth::get_current_user; +use crate::models::posts::queries::get_posts_by_author; +use crate::models::profiles::queries::{ + get_profile_by_id, + update_profile, +}; +use crate::models::relationships::queries as follows; +use crate::utils::files::FileError; +use super::types::{Account, AccountUpdateData}; + +#[get("/{account_id}")] +async fn get_account( + config: web::Data, + db_pool: web::Data, + web::Path(account_id): web::Path, +) -> Result { + let db_client = &**get_database_client(&db_pool).await?; + let profile = get_profile_by_id(db_client, &account_id).await?; + let account = Account::from_profile(profile, &config.instance_url()); + Ok(HttpResponse::Ok().json(account)) +} + +#[patch("/update_credentials")] +async fn update_credentials( + config: web::Data, + db_pool: web::Data, + session: Session, + data: web::Json, +) -> Result { + let db_client = &**get_database_client(&db_pool).await?; + let current_user = get_current_user(db_client, session).await?; + let profile = get_profile_by_id(db_client, ¤t_user.id).await?; + let mut profile_data = data.into_inner() + .into_profile_data( + &profile.avatar_file_name, + &profile.banner_file_name, + &config.media_dir(), + ) + .map_err(|err| { + match err { + FileError::Base64DecodingError(_) => { + HttpError::ValidationError("base64 decoding error".into()) + }, + FileError::InvalidMediaType => { + HttpError::ValidationError("invalid media type".into()) + }, + _ => HttpError::InternalError, + } + })?; + profile_data.clean()?; + let updated_profile = update_profile( + db_client, + &profile.id, + profile_data, + ).await?; + let account = Account::from_profile(updated_profile, &config.instance_url()); + Ok(HttpResponse::Ok().json(account)) +} + +// TODO: actix currently doesn't support parameter arrays +// https://github.com/actix/actix-web/issues/2044 +#[derive(Deserialize)] +pub struct RelationshipQueryParams { + #[serde(rename(deserialize = "id[]"))] + id: Uuid, +} + +#[get("/relationships")] +async fn get_relationships( + db_pool: web::Data, + session: Session, + query_params: web::Query, +) -> Result { + let db_client = &**get_database_client(&db_pool).await?; + let current_user = get_current_user(db_client, session).await?; + let relationships = follows::get_relationships( + db_client, + current_user.id, + vec![query_params.into_inner().id], + ).await?; + Ok(HttpResponse::Ok().json(relationships)) +} + +#[post("/{account_id}/follow")] +async fn follow( + config: web::Data, + db_pool: web::Data, + session: Session, + web::Path(account_id): web::Path, +) -> Result { + let db_client = &mut **get_database_client(&db_pool).await?; + let current_user = get_current_user(db_client, session).await?; + let profile = get_profile_by_id(db_client, &account_id).await?; + let relationship = if let Some(actor_value) = profile.actor_json { + // Remote follow + let request = follows::create_follow_request(db_client, ¤t_user.id, &profile.id).await?; + let actor: Actor = serde_json::from_value(actor_value) + .map_err(|_| HttpError::InternalError)?; + let activity = create_activity_follow( + &config, + ¤t_user.profile, + &request.id, + &actor.id, + ); + let activity_sender = current_user.clone(); + actix_rt::spawn(async move { + deliver_activity( + &config, + &activity_sender, + activity, + vec![actor], + ).await; + }); + follows::get_relationship(db_client, ¤t_user.id, &profile.id).await? + } else { + follows::follow(db_client, ¤t_user.id, &profile.id).await? + }; + Ok(HttpResponse::Ok().json(relationship)) +} + +#[post("/{account_id}/unfollow")] +async fn unfollow( + config: web::Data, + db_pool: web::Data, + session: Session, + web::Path(account_id): web::Path, +) -> Result { + let db_client = &mut **get_database_client(&db_pool).await?; + let current_user = get_current_user(db_client, session).await?; + let target_profile = get_profile_by_id(db_client, &account_id).await?; + let relationship = if let Some(actor_value) = target_profile.actor_json { + // Remote follow + let follow_request = follows::get_follow_request_by_path( + db_client, + ¤t_user.id, + &target_profile.id, + ).await?; + let relationship = follows::unfollow( + db_client, + ¤t_user.id, + &target_profile.id, + ).await?; + // Federate + let actor: Actor = serde_json::from_value(actor_value) + .map_err(|_| HttpError::InternalError)?; + let activity = create_activity_undo_follow( + &config, + ¤t_user.profile, + &follow_request.id, + &actor.id, + ); + actix_rt::spawn(async move { + deliver_activity( + &config, + ¤t_user, + activity, + vec![actor], + ).await; + }); + // TODO: uncouple unfollow and get_relationship + relationship + } else { + follows::unfollow(db_client, ¤t_user.id, &target_profile.id).await? + }; + Ok(HttpResponse::Ok().json(relationship)) +} + +#[get("/{account_id}/statuses")] +async fn get_account_statuses( + config: web::Data, + db_pool: web::Data, + web::Path(account_id): web::Path, +) -> Result { + let db_client = &**get_database_client(&db_pool).await?; + let posts = get_posts_by_author(db_client, &account_id).await?; + let statuses: Vec = posts.into_iter() + .map(|post| Status::from_post(post, &config.instance_url())) + .collect(); + Ok(HttpResponse::Ok().json(statuses)) +} + +pub fn account_api_scope() -> Scope { + web::scope("/api/v1/accounts") + // Routes without account ID + .service(get_relationships) + .service(update_credentials) + // Routes with account ID + .service(get_account) + .service(follow) + .service(unfollow) + .service(get_account_statuses) +} diff --git a/src/mastodon_api/directory/mod.rs b/src/mastodon_api/directory/mod.rs new file mode 100644 index 0000000..38b4403 --- /dev/null +++ b/src/mastodon_api/directory/mod.rs @@ -0,0 +1 @@ +pub mod views; diff --git a/src/mastodon_api/directory/views.rs b/src/mastodon_api/directory/views.rs new file mode 100644 index 0000000..7878ce0 --- /dev/null +++ b/src/mastodon_api/directory/views.rs @@ -0,0 +1,24 @@ +use actix_session::Session; +use actix_web::{get, web, HttpResponse}; + +use crate::config::Config; +use crate::database::{Pool, get_database_client}; +use crate::errors::HttpError; +use crate::mastodon_api::accounts::types::Account; +use crate::mastodon_api::users::auth::get_current_user; +use crate::models::profiles::queries::get_profiles; + +#[get("/api/v1/directory")] +pub async fn profile_directory( + config: web::Data, + db_pool: web::Data, + session: Session, +) -> Result { + let db_client = &**get_database_client(&db_pool).await?; + get_current_user(db_client, session).await?; + let accounts: Vec = get_profiles(db_client).await? + .into_iter() + .map(|profile| Account::from_profile(profile, &config.instance_url())) + .collect(); + Ok(HttpResponse::Ok().json(accounts)) +} diff --git a/src/mastodon_api/instance/mod.rs b/src/mastodon_api/instance/mod.rs new file mode 100644 index 0000000..a964b01 --- /dev/null +++ b/src/mastodon_api/instance/mod.rs @@ -0,0 +1,2 @@ +pub mod types; +pub mod views; diff --git a/src/mastodon_api/instance/types.rs b/src/mastodon_api/instance/types.rs new file mode 100644 index 0000000..a6269d4 --- /dev/null +++ b/src/mastodon_api/instance/types.rs @@ -0,0 +1,40 @@ +use serde::Serialize; + +use crate::config::Config; +use crate::ethereum::nft::MINTER; + +#[derive(Serialize)] +pub struct Instance { + uri: String, + title: String, + short_description: String, + description: String, + version: String, + registrations: bool, + + login_message: String, + ethereum_explorer_url: Option, + nft_contract_name: Option, + nft_contract_address: Option, + ipfs_gateway_url: Option, +} + +impl From<&Config> for Instance { + fn from(config: &Config) -> Self { + Self { + uri: config.instance_uri.clone(), + title: config.instance_title.clone(), + short_description: config.instance_short_description.clone(), + description: config.instance_description.clone(), + version: config.version.clone(), + registrations: config.registrations_open.clone(), + login_message: config.login_message.clone(), + ethereum_explorer_url: config.ethereum_explorer_url.clone(), + nft_contract_name: config.ethereum_contract.as_ref() + .and(Some(MINTER.into())), + nft_contract_address: config.ethereum_contract.as_ref() + .map(|val| val.address.clone()), + ipfs_gateway_url: config.ipfs_gateway_url.clone(), + } + } +} diff --git a/src/mastodon_api/instance/views.rs b/src/mastodon_api/instance/views.rs new file mode 100644 index 0000000..43cce0c --- /dev/null +++ b/src/mastodon_api/instance/views.rs @@ -0,0 +1,13 @@ +use actix_web::{get, web, HttpResponse}; + +use crate::config::Config; +use crate::errors::HttpError; +use super::types::Instance; + +#[get("/api/v1/instance")] +pub async fn instance( + instance_config: web::Data, +) -> Result { + let instance = Instance::from(instance_config.as_ref()); + Ok(HttpResponse::Ok().json(instance)) +} diff --git a/src/mastodon_api/media/mod.rs b/src/mastodon_api/media/mod.rs new file mode 100644 index 0000000..a964b01 --- /dev/null +++ b/src/mastodon_api/media/mod.rs @@ -0,0 +1,2 @@ +pub mod types; +pub mod views; diff --git a/src/mastodon_api/media/types.rs b/src/mastodon_api/media/types.rs new file mode 100644 index 0000000..5d320c6 --- /dev/null +++ b/src/mastodon_api/media/types.rs @@ -0,0 +1,42 @@ +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +use crate::models::attachments::types::{ + DbMediaAttachment, + AttachmentType, +}; +use crate::utils::files::get_file_url; + +/// https://docs.joinmastodon.org/methods/statuses/media/ +#[derive(Deserialize)] +pub struct AttachmentCreateData { + // base64-encoded file + pub file: String, +} + +/// https://docs.joinmastodon.org/entities/attachment/ +#[derive(Serialize)] +pub struct Attachment { + pub id: Uuid, + + #[serde(rename = "type")] + pub attachment_type: String, + + pub url: String, +} + +impl Attachment { + pub fn from_db(db_object: DbMediaAttachment, instance_url: &str) -> Self { + let attachment_type = AttachmentType::from_media_type(db_object.media_type); + let attachment_type_mastodon = match attachment_type { + AttachmentType::Unknown => "unknown", + AttachmentType::Image => "image", + }; + let attachment_url = get_file_url(instance_url, &db_object.file_name); + Self { + id: db_object.id, + attachment_type: attachment_type_mastodon.to_string(), + url: attachment_url, + } + } +} diff --git a/src/mastodon_api/media/views.rs b/src/mastodon_api/media/views.rs new file mode 100644 index 0000000..d84ec08 --- /dev/null +++ b/src/mastodon_api/media/views.rs @@ -0,0 +1,44 @@ +use actix_session::Session; +use actix_web::{post, web, HttpResponse, Scope}; + +use crate::config::Config; +use crate::database::{Pool, get_database_client}; +use crate::errors::HttpError; +use crate::mastodon_api::users::auth::get_current_user; +use crate::models::attachments::queries::create_attachment; +use crate::utils::files::{FileError, save_b64_file}; +use super::types::{AttachmentCreateData, Attachment}; + +#[post("")] +async fn create_attachment_view( + config: web::Data, + db_pool: web::Data, + session: Session, + data: web::Json, +) -> Result { + let db_client = get_database_client(&db_pool).await?; + let current_user = get_current_user(&**db_client, session).await?; + let (file_name, media_type) = save_b64_file( + &data.file, + &config.media_dir(), + ).map_err(|err| match err { + FileError::Base64DecodingError(err) => HttpError::ValidationError(err.to_string()), + _ => HttpError::InternalError, + })?; + let db_attachment = create_attachment( + &**db_client, + ¤t_user.id, + media_type, + file_name, + ).await?; + let attachment = Attachment::from_db( + db_attachment, + &config.instance_url(), + ); + Ok(HttpResponse::Ok().json(attachment)) +} + +pub fn media_api_scope() -> Scope { + web::scope("/api/v1/media") + .service(create_attachment_view) +} diff --git a/src/mastodon_api/mod.rs b/src/mastodon_api/mod.rs new file mode 100644 index 0000000..9265da8 --- /dev/null +++ b/src/mastodon_api/mod.rs @@ -0,0 +1,8 @@ +pub mod accounts; +pub mod directory; +pub mod instance; +pub mod media; +pub mod search; +pub mod statuses; +pub mod timelines; +pub mod users; diff --git a/src/mastodon_api/search/mod.rs b/src/mastodon_api/search/mod.rs new file mode 100644 index 0000000..6cd31d7 --- /dev/null +++ b/src/mastodon_api/search/mod.rs @@ -0,0 +1,3 @@ +pub mod queries; +pub mod types; +pub mod views; diff --git a/src/mastodon_api/search/queries.rs b/src/mastodon_api/search/queries.rs new file mode 100644 index 0000000..b9c333a --- /dev/null +++ b/src/mastodon_api/search/queries.rs @@ -0,0 +1,47 @@ +use regex::Regex; +use tokio_postgres::GenericClient; + +use crate::activitypub::fetcher::fetch_profile; +use crate::config::Config; +use crate::errors::{ValidationError, HttpError}; +use crate::models::profiles::queries::{create_profile, search_profile}; +use crate::models::profiles::types::DbActorProfile; + +fn parse_search_query(query: &str) -> + Result<(String, Option), ValidationError> +{ + let acct_regexp = Regex::new(r"^@?(?P\w+)(@(?P[\w\.-]+))?").unwrap(); + let acct_caps = acct_regexp.captures(query) + .ok_or(ValidationError("invalid search query"))?; + let username = acct_caps.name("user") + .ok_or(ValidationError("invalid search query"))? + .as_str().to_string(); + let instance = acct_caps.name("instance") + .and_then(|val| Some(val.as_str().to_string())); + Ok((username, instance)) +} + +pub async fn search( + config: &Config, + db_client: &impl GenericClient, + search_query: &str, +) -> Result, HttpError> { + let (username, instance) = parse_search_query(search_query)?; + let mut profiles = search_profile(db_client, &username, &instance).await?; + if profiles.len() == 0 && instance.is_some() { + let instance_uri = instance.unwrap(); + let media_dir = config.media_dir(); + let profile_data = fetch_profile(&username, &instance_uri, &media_dir).await + .map_err(|err| { + log::warn!("{}", err); + HttpError::NotFoundError("remote profile") + })?; + let profile = create_profile(db_client, &profile_data).await?; + log::info!( + "imported profile '{}'", + profile.acct, + ); + profiles.push(profile); + } + Ok(profiles) +} diff --git a/src/mastodon_api/search/types.rs b/src/mastodon_api/search/types.rs new file mode 100644 index 0000000..5751fad --- /dev/null +++ b/src/mastodon_api/search/types.rs @@ -0,0 +1,9 @@ +use serde::Serialize; + +use crate::mastodon_api::accounts::types::Account; + +/// https://docs.joinmastodon.org/methods/search/ +#[derive(Serialize)] +pub struct SearchResults { + pub accounts: Vec, +} diff --git a/src/mastodon_api/search/views.rs b/src/mastodon_api/search/views.rs new file mode 100644 index 0000000..81646af --- /dev/null +++ b/src/mastodon_api/search/views.rs @@ -0,0 +1,33 @@ +use actix_session::Session; +use actix_web::{get, web, HttpResponse}; +use serde::Deserialize; + +use crate::config::Config; +use crate::database::{Pool, get_database_client}; +use crate::errors::HttpError; +use crate::mastodon_api::accounts::types::Account; +use crate::mastodon_api::users::auth::get_current_user; +use super::queries; +use super::types::SearchResults; + +#[derive(Deserialize)] +struct SearchQueryParams { + q: String, +} + +#[get("/api/v2/search")] +async fn search( + config: web::Data, + db_pool: web::Data, + session: Session, + query_params: web::Query, +) -> Result { + let db_client = get_database_client(&db_pool).await?; + get_current_user(&**db_client, session).await?; + let profiles = queries::search(&config, &**db_client, &query_params.q).await?; + let accounts: Vec = profiles.into_iter() + .map(|profile| Account::from_profile(profile, &config.instance_url())) + .collect(); + let results = SearchResults { accounts }; + Ok(HttpResponse::Ok().json(results)) +} diff --git a/src/mastodon_api/statuses/mod.rs b/src/mastodon_api/statuses/mod.rs new file mode 100644 index 0000000..a964b01 --- /dev/null +++ b/src/mastodon_api/statuses/mod.rs @@ -0,0 +1,2 @@ +pub mod types; +pub mod views; diff --git a/src/mastodon_api/statuses/types.rs b/src/mastodon_api/statuses/types.rs new file mode 100644 index 0000000..5665593 --- /dev/null +++ b/src/mastodon_api/statuses/types.rs @@ -0,0 +1,61 @@ +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +use crate::mastodon_api::accounts::types::Account; +use crate::mastodon_api::media::types::Attachment; +use crate::models::posts::types::{Post, PostCreateData}; + +/// https://docs.joinmastodon.org/entities/status/ +#[derive(Serialize)] +pub struct Status { + pub id: Uuid, + pub created_at: DateTime, + pub account: Account, + pub content: String, + pub media_attachments: Vec, + + // Extra fields + pub ipfs_cid: Option, + pub token_id: Option, + pub token_tx_id: Option, +} + +impl Status { + pub fn from_post(post: Post, instance_url: &str) -> Self { + let attachments: Vec = post.attachments.into_iter() + .map(|item| Attachment::from_db(item, instance_url)) + .collect(); + let account = Account::from_profile(post.author, instance_url); + Self { + id: post.id, + created_at: post.created_at, + account: account, + content: post.content, + media_attachments: attachments, + ipfs_cid: post.ipfs_cid, + token_id: post.token_id, + token_tx_id: post.token_tx_id, + } + } +} + +/// https://docs.joinmastodon.org/methods/statuses/ +#[derive(Deserialize)] +pub struct StatusData { + pub status: String, + + #[serde(rename = "media_ids[]")] + pub media_ids: Option>, +} + +impl From for PostCreateData { + + fn from(value: StatusData) -> Self { + Self { + content: value.status, + attachments: value.media_ids.unwrap_or(vec![]), + created_at: None, + } + } +} diff --git a/src/mastodon_api/statuses/views.rs b/src/mastodon_api/statuses/views.rs new file mode 100644 index 0000000..16ce9f4 --- /dev/null +++ b/src/mastodon_api/statuses/views.rs @@ -0,0 +1,159 @@ +use actix_session::Session; +use actix_web::{get, post, web, HttpResponse, Scope}; +use serde::Serialize; +use uuid::Uuid; + +use crate::activitypub::activity::create_activity_note; +use crate::activitypub::actor::Actor; +use crate::activitypub::deliverer::deliver_activity; +use crate::config::Config; +use crate::database::{Pool, get_database_client}; +use crate::errors::HttpError; +use crate::ethereum::nft::create_mint_signature; +use crate::ipfs::store as ipfs_store; +use crate::ipfs::utils::{IPFS_LOGO, get_ipfs_url}; +use crate::mastodon_api::users::auth::get_current_user; +use crate::models::profiles::queries::get_followers; +use crate::models::posts::queries::{create_post, get_post_by_id, update_post}; +use crate::models::posts::types::PostCreateData; +use super::types::{Status, StatusData}; + +#[post("")] +async fn create_status( + config: web::Data, + db_pool: web::Data, + session: Session, + data: web::Json, +) -> Result { + let db_client = &mut **get_database_client(&db_pool).await?; + let current_user = get_current_user(db_client, session).await?; + let mut post_data = PostCreateData::from(data.into_inner()); + post_data.validate()?; + let post = create_post(db_client, ¤t_user.id, post_data).await?; + let status = Status::from_post(post.clone(), &config.instance_url()); + // Federate + let activity = create_activity_note(&config, &post); + let followers = get_followers(db_client, ¤t_user.id).await?; + let mut recipients: Vec = Vec::new(); + for follower in followers { + if let Some(actor_value) = follower.actor_json { + // Remote + let actor: Actor = serde_json::from_value(actor_value) + .map_err(|_| HttpError::InternalError)?; + recipients.push(actor); + }; + }; + actix_rt::spawn(async move { + deliver_activity( + &config, + ¤t_user, + activity, + recipients, + ).await; + }); + Ok(HttpResponse::Created().json(status)) +} + +#[get("/{status_id}")] +async fn get_status( + config: web::Data, + db_pool: web::Data, + web::Path(status_id): web::Path, +) -> Result { + let db_client = &**get_database_client(&db_pool).await?; + let post = get_post_by_id(db_client, &status_id).await?; + let status = Status::from_post(post, &config.instance_url()); + Ok(HttpResponse::Ok().json(status)) +} + +// https://docs.opensea.io/docs/metadata-standards +#[derive(Serialize)] +struct PostMetadata { + name: String, + description: String, + image: String, + external_url: String, +} + +#[post("/{status_id}/make_permanent")] +async fn make_permanent( + config: web::Data, + db_pool: web::Data, + session: Session, + web::Path(status_id): web::Path, +) -> Result { + let db_client = &**get_database_client(&db_pool).await?; + get_current_user(db_client, session).await?; + let mut post = get_post_by_id(db_client, &status_id).await?; + let ipfs_api_url = config.ipfs_api_url.as_ref() + .ok_or(HttpError::NotSupported)?; + + let post_image_cid = if let Some(attachment) = post.attachments.first() { + // Add attachment to IPFS + let image_path = config.media_dir().join(&attachment.file_name); + let image_data = std::fs::read(image_path) + .map_err(|_| HttpError::InternalError)?; + let image_cid = ipfs_store::add(&ipfs_api_url, image_data).await + .map_err(|_| HttpError::InternalError)?; + image_cid + } else { + // Use IPFS logo if there's no image + IPFS_LOGO.to_string() + }; + let post_metadata = PostMetadata { + name: format!("Post {}", post.id), + description: post.content.clone(), + image: get_ipfs_url(&post_image_cid), + // TODO: use absolute URL + external_url: format!("/post/{}", post.id), + }; + let post_metadata_json = serde_json::to_string(&post_metadata) + .map_err(|_| HttpError::InternalError)? + .as_bytes().to_vec(); + let post_metadata_cid = ipfs_store::add(&ipfs_api_url, post_metadata_json).await + .map_err(|_| HttpError::InternalError)?; + + // Update post + post.ipfs_cid = Some(post_metadata_cid); + update_post(db_client, &post).await?; + let status = Status::from_post(post, &config.instance_url()); + Ok(HttpResponse::Ok().json(status)) +} + +#[get("/{status_id}/signature")] +async fn get_signature( + config: web::Data, + db_pool: web::Data, + session: Session, + web::Path(status_id): web::Path, +) -> Result { + let db_client = &**get_database_client(&db_pool).await?; + let current_user = get_current_user(db_client, session).await?; + let contract_config = config.ethereum_contract.as_ref() + .ok_or(HttpError::NotSupported)?; + let post = get_post_by_id(db_client, &status_id).await?; + if post.author.id != current_user.id { + // Users can only tokenize their own posts + Err(HttpError::NotFoundError("post"))?; + } + let ipfs_cid = post.ipfs_cid + // Post metadata is not immutable + .ok_or(HttpError::ValidationError("post is not immutable".into()))?; + let token_uri = get_ipfs_url(&ipfs_cid); + let signature = create_mint_signature( + &contract_config, + ¤t_user.wallet_address, + &token_uri, + ).map_err(|_| HttpError::InternalError)?; + Ok(HttpResponse::Ok().json(signature)) +} + +pub fn status_api_scope() -> Scope { + web::scope("/api/v1/statuses") + // Routes without status ID + .service(create_status) + // Routes with status ID + .service(get_status) + .service(make_permanent) + .service(get_signature) +} diff --git a/src/mastodon_api/timelines/mod.rs b/src/mastodon_api/timelines/mod.rs new file mode 100644 index 0000000..38b4403 --- /dev/null +++ b/src/mastodon_api/timelines/mod.rs @@ -0,0 +1 @@ +pub mod views; diff --git a/src/mastodon_api/timelines/views.rs b/src/mastodon_api/timelines/views.rs new file mode 100644 index 0000000..a71baa6 --- /dev/null +++ b/src/mastodon_api/timelines/views.rs @@ -0,0 +1,24 @@ +use actix_session::Session; +use actix_web::{get, web, HttpResponse}; + +use crate::config::Config; +use crate::database::{Pool, get_database_client}; +use crate::errors::HttpError; +use crate::mastodon_api::statuses::types::Status; +use crate::mastodon_api::users::auth::get_current_user; +use crate::models::posts::queries::get_posts; + +#[get("/api/v1/timelines/home")] +pub async fn home_timeline( + config: web::Data, + db_pool: web::Data, + session: Session, +) -> Result { + let db_client = &**get_database_client(&db_pool).await?; + let current_user = get_current_user(db_client, session).await?; + let statuses: Vec = get_posts(db_client, ¤t_user.id).await? + .into_iter() + .map(|post| Status::from_post(post, &config.instance_url())) + .collect(); + Ok(HttpResponse::Ok().json(statuses)) +} diff --git a/src/mastodon_api/users/auth.rs b/src/mastodon_api/users/auth.rs new file mode 100644 index 0000000..d8e2f04 --- /dev/null +++ b/src/mastodon_api/users/auth.rs @@ -0,0 +1,25 @@ +use actix_session::Session; +use tokio_postgres::GenericClient; +use uuid::Uuid; + +use crate::errors::HttpError; +use crate::models::users::queries::get_user_by_id; +use crate::models::users::types::User; + +pub async fn get_current_user( + db_client: &impl GenericClient, + session: Session, +) -> Result { + let maybe_user_id = session.get::("id") + .map_err(|_| HttpError::SessionError("failed to read cookie"))?; + if let Some(user_id) = maybe_user_id { + let user_uuid = Uuid::parse_str(&user_id) + .map_err(|_| HttpError::SessionError("invalid uuid"))?; + let user = get_user_by_id(db_client, &user_uuid) + .await + .map_err(|_| HttpError::SessionError("user not found"))?; + Ok(user) + } else { + return Err(HttpError::SessionError("session not found")); + } +} diff --git a/src/mastodon_api/users/mod.rs b/src/mastodon_api/users/mod.rs new file mode 100644 index 0000000..70b7922 --- /dev/null +++ b/src/mastodon_api/users/mod.rs @@ -0,0 +1,3 @@ +pub mod auth; +mod types; +pub mod views; diff --git a/src/mastodon_api/users/types.rs b/src/mastodon_api/users/types.rs new file mode 100644 index 0000000..df5732d --- /dev/null +++ b/src/mastodon_api/users/types.rs @@ -0,0 +1,24 @@ +use serde::Serialize; +use uuid::Uuid; + +use crate::mastodon_api::accounts::types::Account; +use crate::models::users::types::User; + +// TODO: use Account instead +#[derive(Serialize)] +pub struct ApiUser { + pub id: Uuid, + pub profile: Account, + pub wallet_address: String, +} + +impl ApiUser { + pub fn from_user(user: User, instance_url: &str) -> Self { + let account = Account::from_profile(user.profile, instance_url); + Self { + id: user.id, + profile: account, + wallet_address: user.wallet_address, + } + } +} diff --git a/src/mastodon_api/users/views.rs b/src/mastodon_api/users/views.rs new file mode 100644 index 0000000..abba684 --- /dev/null +++ b/src/mastodon_api/users/views.rs @@ -0,0 +1,105 @@ +use actix_session::Session; +use actix_web::{ + get, post, web, + HttpResponse, +}; + +use crate::config::Config; +use crate::database::{Pool, get_database_client}; +use crate::errors::{HttpError, ValidationError}; +use crate::models::users::queries::{ + is_valid_invite_code, + create_user, + get_user_by_wallet_address, +}; +use crate::models::users::types::{ + UserRegistrationData, + UserLoginData, +}; +use crate::utils::crypto::{ + hash_password, + verify_password, + generate_private_key, + serialize_private_key, +}; +use super::auth::get_current_user; +use super::types::ApiUser; + +// /api/v1/accounts +#[post("/api/v0/create")] +async fn create_user_view( + config: web::Data, + db_pool: web::Data, + form: web::Json, + session: Session, +) -> Result { + let db_client = &mut **get_database_client(&db_pool).await?; + // Validate + form.clean()?; + if !config.registrations_open { + let invite_code = form.invite_code.as_ref() + .ok_or(ValidationError("invite code is required"))?; + if !is_valid_invite_code(db_client, &invite_code).await? { + Err(ValidationError("invalid invite code"))?; + } + } + // Hash password and generate private key + let password_hash = hash_password(&form.signature) + .map_err(|_| HttpError::InternalError)?; + let private_key = match web::block(move || generate_private_key()).await { + Ok(private_key) => private_key, + Err(_) => return Err(HttpError::InternalError), + }; + let private_key_pem = serialize_private_key(private_key) + .map_err(|_| HttpError::InternalError)?; + + let user = create_user( + db_client, + form.into_inner(), + password_hash, + private_key_pem, + ).await?; + session.set("id", user.id)?; + let api_user = ApiUser::from_user(user, &config.instance_url()); + Ok(HttpResponse::Created().json(api_user)) +} + +#[post("/api/v0/login")] +async fn login_view( + config: web::Data, + db_pool: web::Data, + form: web::Json, + session: Session, +) -> Result { + let db_client = &**get_database_client(&db_pool).await?; + let user = get_user_by_wallet_address(db_client, &form.wallet_address).await?; + let result = verify_password(&user.password_hash, &form.signature) + .map_err(|_| ValidationError("incorrect password"))?; + if !result { + // Invalid signature/password + Err(ValidationError("incorrect password"))?; + } + session.set("id", &user.id)?; + let api_user = ApiUser::from_user(user, &config.instance_url()); + Ok(HttpResponse::Ok().json(api_user)) +} + +#[get("/api/v0/current-user")] +async fn current_user_view( + config: web::Data, + db_pool: web::Data, + session: Session, +) -> Result { + let db_client = &**get_database_client(&db_pool).await?; + let user = get_current_user(db_client, session).await?; + let api_user = ApiUser::from_user(user, &config.instance_url()); + Ok(HttpResponse::Ok().json(api_user)) +} + +#[post("/api/v0/logout")] +async fn logout_view( + session: Session, +) -> Result { + session.clear(); + Ok(HttpResponse::Ok().body("logged out")) +} diff --git a/src/models/attachments/mod.rs b/src/models/attachments/mod.rs new file mode 100644 index 0000000..0333ab5 --- /dev/null +++ b/src/models/attachments/mod.rs @@ -0,0 +1,2 @@ +pub mod queries; +pub mod types; diff --git a/src/models/attachments/queries.rs b/src/models/attachments/queries.rs new file mode 100644 index 0000000..dc57659 --- /dev/null +++ b/src/models/attachments/queries.rs @@ -0,0 +1,24 @@ +use tokio_postgres::GenericClient; +use uuid::Uuid; + +use crate::errors::DatabaseError; +use super::types::DbMediaAttachment; + +pub async fn create_attachment( + db_client: &impl GenericClient, + owner_id: &Uuid, + media_type: Option, + file_name: String, +) -> Result { + let attachment_id = Uuid::new_v4(); + let inserted_row = db_client.query_one( + " + INSERT INTO media_attachment (id, owner_id, media_type, file_name) + VALUES ($1, $2, $3, $4) + RETURNING media_attachment + ", + &[&attachment_id, &owner_id, &media_type, &file_name], + ).await?; + let db_attachment: DbMediaAttachment = inserted_row.try_get("media_attachment")?; + Ok(db_attachment) +} diff --git a/src/models/attachments/types.rs b/src/models/attachments/types.rs new file mode 100644 index 0000000..89aa871 --- /dev/null +++ b/src/models/attachments/types.rs @@ -0,0 +1,34 @@ +use chrono::{DateTime, Utc}; +use postgres_types::FromSql; +use uuid::Uuid; + +#[derive(Clone, FromSql)] +#[postgres(name = "media_attachment")] +pub struct DbMediaAttachment { + pub id: Uuid, + pub owner_id: Uuid, + pub media_type: Option, + pub file_name: String, + pub post_id: Option, + pub created_at: DateTime, +} + +pub enum AttachmentType { + Unknown, + Image, +} + +impl AttachmentType { + pub fn from_media_type(value: Option) -> Self { + match value { + Some(media_type) => { + if media_type.starts_with("image/") { + Self::Image + } else { + Self::Unknown + } + }, + None => Self::Unknown, + } + } +} diff --git a/src/models/mod.rs b/src/models/mod.rs new file mode 100644 index 0000000..72f013f --- /dev/null +++ b/src/models/mod.rs @@ -0,0 +1,5 @@ +pub mod attachments; +pub mod posts; +pub mod profiles; +pub mod relationships; +pub mod users; diff --git a/src/models/posts/mod.rs b/src/models/posts/mod.rs new file mode 100644 index 0000000..0333ab5 --- /dev/null +++ b/src/models/posts/mod.rs @@ -0,0 +1,2 @@ +pub mod queries; +pub mod types; diff --git a/src/models/posts/queries.rs b/src/models/posts/queries.rs new file mode 100644 index 0000000..4b86406 --- /dev/null +++ b/src/models/posts/queries.rs @@ -0,0 +1,207 @@ +use std::convert::TryFrom; + +use chrono::Utc; +use tokio_postgres::GenericClient; +use uuid::Uuid; + +use crate::errors::DatabaseError; +use crate::models::attachments::types::DbMediaAttachment; +use crate::models::profiles::queries::update_post_count; +use super::types::{DbPost, Post, PostCreateData}; + +pub async fn get_posts( + db_client: &impl GenericClient, + current_user_id: &Uuid, +) -> Result, DatabaseError> { + // Select posts from follows + own posts + let rows = db_client.query( + " + SELECT + post, actor_profile, + ARRAY( + SELECT media_attachment + FROM media_attachment WHERE post_id = post.id + ) AS attachments + FROM post + JOIN actor_profile ON post.author_id = actor_profile.id + WHERE + post.author_id = $1 + OR EXISTS ( + SELECT 1 FROM relationship + WHERE source_id = $1 AND target_id = post.author_id + ) + ORDER BY post.created_at DESC + ", + &[¤t_user_id], + ).await?; + let posts: Vec = rows.iter() + .map(|row| Post::try_from(row)) + .collect::>()?; + Ok(posts) +} + +pub async fn get_posts_by_author( + db_client: &impl GenericClient, + account_id: &Uuid, +) -> Result, DatabaseError> { + let rows = db_client.query( + " + SELECT + post, actor_profile, + ARRAY( + SELECT media_attachment + FROM media_attachment WHERE post_id = post.id + ) AS attachments + FROM post + JOIN actor_profile ON post.author_id = actor_profile.id + WHERE + post.author_id = $1 + ORDER BY post.created_at DESC + ", + &[&account_id], + ).await?; + let posts: Vec = rows.iter() + .map(|row| Post::try_from(row)) + .collect::>()?; + Ok(posts) +} + +pub async fn create_post( + db_client: &mut impl GenericClient, + author_id: &Uuid, + data: PostCreateData, +) -> Result { + let transaction = db_client.transaction().await?; + let post_id = uuid::Uuid::new_v4(); + let created_at = data.created_at.unwrap_or(Utc::now()); + let post_row = transaction.query_one( + " + INSERT INTO post (id, author_id, content, created_at) + VALUES ($1, $2, $3, $4) + RETURNING post + ", + &[&post_id, &author_id, &data.content, &created_at], + ).await?; + let attachment_rows = transaction.query( + " + UPDATE media_attachment + SET post_id = $1 + WHERE id = ANY($2) + RETURNING media_attachment + ", + &[&post_id, &data.attachments], + ).await?; + let db_attachments: Vec = attachment_rows.iter() + .map(|row| -> Result { + row.try_get("media_attachment") + }) + .collect::>()?; + let db_post: DbPost = post_row.try_get("post")?; + let author = update_post_count(&transaction, &db_post.author_id, 1).await?; + transaction.commit().await?; + let post = Post { + id: db_post.id, + author: author, + content: db_post.content, + attachments: db_attachments, + ipfs_cid: db_post.ipfs_cid, + token_id: db_post.token_id, + token_tx_id: db_post.token_tx_id, + created_at: db_post.created_at, + }; + Ok(post) +} + +pub async fn get_post_by_id( + db_client: &impl GenericClient, + post_id: &Uuid, +) -> Result { + let maybe_row = db_client.query_opt( + " + SELECT + post, actor_profile, + ARRAY( + SELECT media_attachment + FROM media_attachment WHERE post_id = post.id + ) AS attachments + FROM post + JOIN actor_profile ON post.author_id = actor_profile.id + WHERE post.id = $1 + ", + &[&post_id], + ).await?; + let post = match maybe_row { + Some(row) => Post::try_from(&row)?, + None => return Err(DatabaseError::NotFound("post")), + }; + Ok(post) +} + +pub async fn get_post_by_ipfs_cid( + db_client: &impl GenericClient, + ipfs_cid: &str, +) -> Result { + let result = db_client.query_opt( + " + SELECT + post, actor_profile, + ARRAY( + SELECT media_attachment + FROM media_attachment WHERE post_id = post.id + ) AS attachments + FROM post + JOIN actor_profile ON post.author_id = actor_profile.id + WHERE post.ipfs_cid = $1 + ", + &[&ipfs_cid], + ).await?; + let post = match result { + Some(row) => Post::try_from(&row)?, + None => return Err(DatabaseError::NotFound("post")), + }; + Ok(post) +} + +pub async fn update_post( + db_client: &impl GenericClient, + post: &Post, +) -> Result<(), DatabaseError> { + // TODO: create PostUpdateData type + let updated_count = db_client.execute( + " + UPDATE post + SET + content = $1, + ipfs_cid = $2, + token_id = $3, + token_tx_id = $4 + WHERE id = $5 + ", + &[ + &post.content, + &post.ipfs_cid, + &post.token_id, + &post.token_tx_id, + &post.id, + ], + ).await?; + if updated_count == 0 { + return Err(DatabaseError::NotFound("post")); + } + Ok(()) +} + +pub async fn is_waiting_for_token( + db_client: &impl GenericClient, +) -> Result { + let row = db_client.query_one( + " + SELECT count(post) > 0 AS is_waiting + FROM post + WHERE ipfs_cid IS NOT NULL AND token_id IS NULL + ", + &[], + ).await?; + let is_waiting: bool = row.try_get("is_waiting")?; + Ok(is_waiting) +} diff --git a/src/models/posts/types.rs b/src/models/posts/types.rs new file mode 100644 index 0000000..8f0ba04 --- /dev/null +++ b/src/models/posts/types.rs @@ -0,0 +1,102 @@ +use std::convert::TryFrom; + +use chrono::{DateTime, Utc}; +use postgres_types::FromSql; +use tokio_postgres::Row; +use uuid::Uuid; + +use crate::errors::ValidationError; +use crate::models::attachments::types::DbMediaAttachment; +use crate::models::profiles::types::DbActorProfile; +use crate::utils::html::clean_html; + +#[derive(FromSql)] +#[postgres(name = "post")] +pub struct DbPost { + pub id: Uuid, + pub author_id: Uuid, + pub content: String, + pub ipfs_cid: Option, + pub token_id: Option, + pub token_tx_id: Option, + pub created_at: DateTime, +} + +#[derive(Clone)] +pub struct Post { + pub id: Uuid, + pub author: DbActorProfile, + pub content: String, + pub attachments: Vec, + pub ipfs_cid: Option, + pub token_id: Option, + pub token_tx_id: Option, + pub created_at: DateTime, +} + +impl TryFrom<&Row> for Post { + + type Error = tokio_postgres::Error; + + fn try_from(row: &Row) -> Result { + let db_post: DbPost = row.try_get("post")?; + let db_profile: DbActorProfile = row.try_get("actor_profile")?; + let db_attachments: Vec = row.try_get("attachments")?; + let post = Post { + id: db_post.id, + author: db_profile, + content: db_post.content, + attachments: db_attachments, + ipfs_cid: db_post.ipfs_cid, + token_id: db_post.token_id, + token_tx_id: db_post.token_tx_id, + created_at: db_post.created_at, + }; + Ok(post) + } +} + +pub struct PostCreateData { + pub content: String, + pub attachments: Vec, + pub created_at: Option>, +} + +impl PostCreateData { + /// Validate and clean post data. + pub fn validate(&mut self) -> Result<(), ValidationError> { + let content_safe = clean_html(&self.content); + let content_trimmed = content_safe.trim(); + if content_trimmed == "" { + return Err(ValidationError("post can not be empty")); + } + self.content = content_trimmed.to_string(); + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_validate_post_data() { + let mut post_data_1 = PostCreateData { + content: " ".to_string(), + attachments: vec![], + created_at: None, + }; + assert_eq!(post_data_1.validate().is_ok(), false); + } + + #[test] + fn test_trimming() { + let mut post_data_2 = PostCreateData { + content: "test ".to_string(), + attachments: vec![], + created_at: None, + }; + assert_eq!(post_data_2.validate().is_ok(), true); + assert_eq!(post_data_2.content.as_str(), "test"); + } +} diff --git a/src/models/profiles/mod.rs b/src/models/profiles/mod.rs new file mode 100644 index 0000000..0333ab5 --- /dev/null +++ b/src/models/profiles/mod.rs @@ -0,0 +1,2 @@ +pub mod queries; +pub mod types; diff --git a/src/models/profiles/queries.rs b/src/models/profiles/queries.rs new file mode 100644 index 0000000..459e9ff --- /dev/null +++ b/src/models/profiles/queries.rs @@ -0,0 +1,272 @@ +use tokio_postgres::GenericClient; +use uuid::Uuid; + +use crate::errors::DatabaseError; +use super::types::{DbActorProfile, ProfileCreateData, ProfileUpdateData}; + +/// Create new profile using given Client or Transaction. +pub async fn create_profile( + db_client: &impl GenericClient, + profile_data: &ProfileCreateData, +) -> Result { + let profile_id = Uuid::new_v4(); + let result = db_client.query_one( + " + INSERT INTO actor_profile ( + id, username, display_name, acct, bio, bio_source, + avatar_file_name, banner_file_name, + actor_json + ) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) + RETURNING actor_profile + ", + &[ + &profile_id, + &profile_data.username, + &profile_data.display_name, + &profile_data.acct, + &profile_data.bio, + &profile_data.bio, + &profile_data.avatar, + &profile_data.banner, + &profile_data.actor, + ], + ).await; + let profile = match result { + Ok(row) => row.try_get("actor_profile")?, + Err(err) => { + // TODO: catch profile already exists error + log::warn!("{}", err); + return Err(DatabaseError::AlreadyExists("profile")); + }, + }; + Ok(profile) +} + +pub async fn update_profile( + db_client: &impl GenericClient, + profile_id: &Uuid, + data: ProfileUpdateData, +) -> Result { + let maybe_row = db_client.query_opt( + " + UPDATE actor_profile + SET + display_name = $1, + bio = $2, + bio_source = $3, + avatar_file_name = $4, + banner_file_name = $5 + WHERE id = $6 + RETURNING actor_profile + ", + &[ + &data.display_name, + &data.bio, + &data.bio_source, + &data.avatar, + &data.banner, + &profile_id, + ], + ).await?; + let profile = match maybe_row { + Some(row) => row.try_get("actor_profile")?, + None => return Err(DatabaseError::NotFound("profile")), + }; + Ok(profile) +} + +pub async fn get_profile_by_id( + db_client: &impl GenericClient, + profile_id: &Uuid, +) -> Result { + let result = db_client.query_opt( + " + SELECT actor_profile + FROM actor_profile + WHERE id = $1 + ", + &[&profile_id], + ).await?; + let profile = match result { + Some(row) => row.try_get("actor_profile")?, + None => return Err(DatabaseError::NotFound("profile")), + }; + Ok(profile) +} + +pub async fn get_profile_by_actor_id( + db_client: &impl GenericClient, + actor_id: &str, +) -> Result { + let result = db_client.query_opt( + " + SELECT actor_profile + FROM actor_profile + WHERE actor_profile.actor_json ->> 'id' = $1 + ", + &[&actor_id], + ).await?; + let profile = match result { + Some(row) => row.try_get("actor_profile")?, + None => return Err(DatabaseError::NotFound("profile")), + }; + Ok(profile) +} + +pub async fn get_profile_by_acct( + db_client: &impl GenericClient, + account_uri: &str, +) -> Result { + let result = db_client.query_opt( + " + SELECT actor_profile + FROM actor_profile + WHERE actor_profile.acct = $1 + ", + &[&account_uri], + ).await?; + let profile = match result { + Some(row) => row.try_get("actor_profile")?, + None => return Err(DatabaseError::NotFound("profile")), + }; + Ok(profile) +} + +pub async fn get_profiles( + db_client: &impl GenericClient, +) -> Result, DatabaseError> { + let rows = db_client.query( + " + SELECT actor_profile + FROM actor_profile + ORDER BY username + ", + &[], + ).await?; + let profiles = rows.iter() + .map(|row| row.try_get("actor_profile")) + .collect::, _>>()?; + Ok(profiles) +} + +pub async fn get_followers( + db_client: &impl GenericClient, + profile_id: &Uuid, +) -> Result, DatabaseError> { + let rows = db_client.query( + " + SELECT actor_profile + FROM actor_profile + JOIN relationship + ON (actor_profile.id = relationship.source_id) + WHERE relationship.target_id = $1 + ", + &[&profile_id], + ).await?; + let profiles = rows.iter() + .map(|row| row.try_get("actor_profile")) + .collect::, _>>()?; + Ok(profiles) +} + +pub async fn delete_profile( + db_client: &impl GenericClient, + profile_id: &Uuid, +) -> Result<(), DatabaseError> { + let deleted_count = db_client.execute( + "DELETE FROM actor_profile WHERE id = $1", + &[&profile_id], + ).await?; + if deleted_count == 0 { + return Err(DatabaseError::NotFound("profile")); + } + Ok(()) +} + +pub async fn search_profile( + db_client: &impl GenericClient, + username: &String, + instance: &Option, +) -> Result, DatabaseError> { + let db_search_query = match &instance { + Some(instance) => { + // Search for exact profile name. + // Fetch from remote server if not found + format!("{}@{}", username, instance) + }, + None => { + // Search for username + format!("%{}%", username) + }, + }; + let rows = db_client.query( + " + SELECT actor_profile + FROM actor_profile + WHERE acct LIKE $1 + ", + &[&db_search_query], + ).await?; + let profiles: Vec = rows.iter() + .map(|row| row.try_get("actor_profile")) + .collect::>()?; + Ok(profiles) +} + +pub async fn update_follower_count( + db_client: &impl GenericClient, + profile_id: &Uuid, + change: i32, +) -> Result { + let maybe_row = db_client.query_opt( + " + UPDATE actor_profile + SET follower_count = follower_count + $1 + WHERE id = $2 + RETURNING actor_profile + ", + &[&change, &profile_id], + ).await?; + let row = maybe_row.ok_or(DatabaseError::NotFound("profile"))?; + let profile = row.try_get("actor_profile")?; + Ok(profile) +} + +pub async fn update_following_count( + db_client: &impl GenericClient, + profile_id: &Uuid, + change: i32, +) -> Result { + let maybe_row = db_client.query_opt( + " + UPDATE actor_profile + SET following_count = following_count + $1 + WHERE id = $2 + RETURNING actor_profile + ", + &[&change, &profile_id], + ).await?; + let row = maybe_row.ok_or(DatabaseError::NotFound("profile"))?; + let profile = row.try_get("actor_profile")?; + Ok(profile) +} + +pub async fn update_post_count( + db_client: &impl GenericClient, + profile_id: &Uuid, + change: i32, +) -> Result { + let maybe_row = db_client.query_opt( + " + UPDATE actor_profile + SET post_count = post_count + $1 + WHERE id = $2 + RETURNING actor_profile + ", + &[&change, &profile_id], + ).await?; + let row = maybe_row.ok_or(DatabaseError::NotFound("profile"))?; + let profile = row.try_get("actor_profile")?; + Ok(profile) +} diff --git a/src/models/profiles/types.rs b/src/models/profiles/types.rs new file mode 100644 index 0000000..c6565f7 --- /dev/null +++ b/src/models/profiles/types.rs @@ -0,0 +1,51 @@ +use chrono::{DateTime, Utc}; +use postgres_types::FromSql; +use serde_json::Value; +use uuid::Uuid; + +use crate::errors::ValidationError; +use crate::utils::html::clean_html; + +#[derive(Clone, FromSql)] +#[postgres(name = "actor_profile")] +pub struct DbActorProfile { + pub id: Uuid, + pub username: String, + pub acct: String, + pub display_name: Option, + pub bio: Option, // html + pub bio_source: Option, // plaintext or markdown + pub avatar_file_name: Option, + pub banner_file_name: Option, + pub follower_count: i32, + pub following_count: i32, + pub post_count: i32, + pub created_at: DateTime, + pub actor_json: Option, +} + +pub struct ProfileCreateData { + pub username: String, + pub display_name: Option, + pub acct: String, + pub bio: Option, + pub avatar: Option, + pub banner: Option, + pub actor: Option, +} + +pub struct ProfileUpdateData { + pub display_name: Option, + pub bio: Option, + pub bio_source: Option, + pub avatar: Option, + pub banner: Option, +} + +impl ProfileUpdateData { + /// Validate and clean bio. + pub fn clean(&mut self) -> Result<(), ValidationError> { + self.bio = self.bio.as_ref().map(|val| clean_html(val)); + Ok(()) + } +} diff --git a/src/models/relationships/mod.rs b/src/models/relationships/mod.rs new file mode 100644 index 0000000..0333ab5 --- /dev/null +++ b/src/models/relationships/mod.rs @@ -0,0 +1,2 @@ +pub mod queries; +pub mod types; diff --git a/src/models/relationships/queries.rs b/src/models/relationships/queries.rs new file mode 100644 index 0000000..4138839 --- /dev/null +++ b/src/models/relationships/queries.rs @@ -0,0 +1,230 @@ +use std::convert::TryFrom; + +use tokio_postgres::GenericClient; +use uuid::Uuid; + +use crate::errors::DatabaseError; +use crate::models::profiles::queries::{ + update_follower_count, + update_following_count, +}; +use super::types::{ + DbFollowRequest, + FollowRequest, + FollowRequestStatus, + Relationship, +}; + +pub async fn get_relationships( + db_client: &impl GenericClient, + source_id: Uuid, + target_ids: Vec, +) -> Result, DatabaseError> { + let rows = db_client.query( + " + SELECT + actor_profile.id, + EXISTS ( + SELECT 1 FROM relationship + WHERE source_id = $1 AND target_id = actor_profile.id + ) AS following, + EXISTS ( + SELECT 1 FROM relationship + WHERE source_id = actor_profile.id AND target_id = $1 + ) AS followed_by, + EXISTS ( + SELECT 1 FROM follow_request + WHERE source_id = $1 AND target_id = actor_profile.id + AND request_status = 1 + ) AS requested + FROM actor_profile + WHERE actor_profile.id = ANY($2) + ", + &[&source_id, &target_ids], + ).await?; + let relationships = rows.iter() + .map(|row| Relationship::try_from(row)) + .collect::>()?; + Ok(relationships) +} + +pub async fn get_relationship( + db_client: &impl GenericClient, + source_id: &Uuid, + target_id: &Uuid, +) -> Result { + let maybe_row = db_client.query_opt( + " + SELECT + actor_profile.id, + EXISTS ( + SELECT 1 FROM relationship + WHERE source_id = $1 AND target_id = actor_profile.id + ) AS following, + EXISTS ( + SELECT 1 FROM relationship + WHERE source_id = actor_profile.id AND target_id = $1 + ) AS followed_by, + EXISTS ( + SELECT 1 FROM follow_request + WHERE source_id = $1 AND target_id = actor_profile.id + AND request_status = 1 + ) AS requested + FROM actor_profile + WHERE actor_profile.id = $2 + ", + &[&source_id, &target_id], + ).await?; + let row = maybe_row.ok_or(DatabaseError::NotFound("profile"))?; + let relationship = Relationship::try_from(&row)?; + Ok(relationship) +} + +pub async fn follow( + db_client: &mut impl GenericClient, + source_id: &Uuid, + target_id: &Uuid, +) -> Result { + let transaction = db_client.transaction().await?; + let result = transaction.execute( + " + INSERT INTO relationship (source_id, target_id) + VALUES ($1, $2) + ", + &[&source_id, &target_id], + ).await; + if let Err(err) = result { + log::info!("{}", err); + return Err(DatabaseError::AlreadyExists("relationship")); + }; + update_follower_count(&transaction, target_id, 1).await?; + update_following_count(&transaction, source_id, 1).await?; + let relationship = get_relationship(&transaction, source_id, target_id).await?; + transaction.commit().await?; + Ok(relationship) +} + +pub async fn unfollow( + db_client: &mut impl GenericClient, + source_id: &Uuid, + target_id: &Uuid, +) -> Result { + let transaction = db_client.transaction().await?; + let deleted_count = transaction.execute( + " + DELETE FROM relationship + WHERE source_id = $1 AND target_id = $2 + ", + &[&source_id, &target_id], + ).await?; + if deleted_count == 0 { + // Relationship not found, try to delete follow request + let follow_request_deleted = delete_follow_request( + &transaction, + source_id, + target_id, + ).await?; + if !follow_request_deleted { + return Err(DatabaseError::NotFound("relationship")); + } + } else { + // Update counters only if relationship exists + update_follower_count(&transaction, target_id, -1).await?; + update_following_count(&transaction, source_id, -1).await?; + } + let relationship = get_relationship(&transaction, source_id, target_id).await?; + transaction.commit().await?; + Ok(relationship) +} + +pub async fn create_follow_request( + db_client: &impl GenericClient, + source_id: &Uuid, + target_id: &Uuid, +) -> Result { + let request = FollowRequest { + id: Uuid::new_v4(), + source_id: source_id.to_owned(), + target_id: target_id.to_owned(), + status: FollowRequestStatus::Pending, + }; + db_client.execute( + " + INSERT INTO follow_request ( + id, source_id, target_id, request_status + ) + VALUES ($1, $2, $3, $4) + ", + &[ + &request.id, + &request.source_id, + &request.target_id, + &i16::from(request.status.clone()), + ], + ).await?; + Ok(request) +} + +pub async fn accept_follow_request( + db_client: &mut impl GenericClient, + request_id: &Uuid, +) -> Result<(), DatabaseError> { + let mut transaction = db_client.transaction().await?; + let status_sql = i16::from(FollowRequestStatus::Accepted); + let maybe_row = transaction.query_opt( + " + UPDATE follow_request + SET request_status = $1 + WHERE id = $2 + RETURNING source_id, target_id + ", + &[&status_sql, &request_id], + ).await?; + let row = maybe_row.ok_or(DatabaseError::NotFound("follow request"))?; + let source_id: Uuid = row.try_get("source_id")?; + let target_id: Uuid = row.try_get("target_id")?; + follow(&mut transaction, &source_id, &target_id).await?; + transaction.commit().await?; + Ok(()) +} + +pub async fn delete_follow_request( + db_client: &impl GenericClient, + source_id: &Uuid, + target_id: &Uuid, +) -> Result { + let deleted_count = db_client.execute( + " + DELETE FROM follow_request + WHERE source_id = $1 AND target_id = $2 + ", + &[&source_id, &target_id], + ).await?; + let is_success = deleted_count > 0; + Ok(is_success) +} + +pub async fn get_follow_request_by_path( + db_client: &impl GenericClient, + source_id: &Uuid, + target_id: &Uuid, +) -> Result { + let maybe_row = db_client.query_opt( + " + SELECT follow_request + FROM follow_request + WHERE source_id = $1 AND target_id = $2 + ", + &[&source_id, &target_id], + ).await?; + let row = maybe_row.ok_or(DatabaseError::NotFound("follow request"))?; + let db_request: DbFollowRequest = row.try_get("follow_request")?; + let request_status = FollowRequestStatus::try_from(db_request.request_status)?; + let request = FollowRequest { + id: db_request.id, + source_id: db_request.source_id, + target_id: db_request.target_id, + status: request_status, + }; + Ok(request) +} diff --git a/src/models/relationships/types.rs b/src/models/relationships/types.rs new file mode 100644 index 0000000..65b7fd1 --- /dev/null +++ b/src/models/relationships/types.rs @@ -0,0 +1,78 @@ +use std::convert::TryFrom; + +use postgres_types::FromSql; +use serde::Serialize; +use tokio_postgres::Row; +use uuid::Uuid; + +use crate::errors::ConversionError; + +#[derive(Serialize)] +pub struct Relationship { + pub id: Uuid, + pub following: bool, + pub followed_by: bool, + pub requested: bool, +} + +impl TryFrom<&Row> for Relationship { + + type Error = tokio_postgres::Error; + + fn try_from(row: &Row) -> Result { + let relationship = Relationship { + id: row.try_get("id")?, + following: row.try_get("following")?, + followed_by: row.try_get("followed_by")?, + requested: row.try_get("requested")?, + }; + Ok(relationship) + } +} + +#[derive(Clone, PartialEq)] +pub enum FollowRequestStatus { + Pending, + Accepted, + Rejected, +} + +impl From for i16 { + fn from(value: FollowRequestStatus) -> i16 { + match value { + FollowRequestStatus::Pending => 1, + FollowRequestStatus::Accepted => 2, + FollowRequestStatus::Rejected => 3, + } + } +} + +impl TryFrom for FollowRequestStatus { + type Error = ConversionError; + + fn try_from(value: i16) -> Result { + let status = match value { + 1 => Self::Pending, + 2 => Self::Accepted, + 3 => Self::Rejected, + _ => return Err(ConversionError), + }; + Ok(status) + } +} + +#[derive(FromSql)] +#[postgres(name = "follow_request")] +pub struct DbFollowRequest { + pub id: Uuid, + pub source_id: Uuid, + pub target_id: Uuid, + pub request_status: i16, +} + +pub struct FollowRequest { + pub id: Uuid, + pub source_id: Uuid, + pub target_id: Uuid, + pub status: FollowRequestStatus, +} diff --git a/src/models/users/mod.rs b/src/models/users/mod.rs new file mode 100644 index 0000000..0333ab5 --- /dev/null +++ b/src/models/users/mod.rs @@ -0,0 +1,2 @@ +pub mod queries; +pub mod types; diff --git a/src/models/users/queries.rs b/src/models/users/queries.rs new file mode 100644 index 0000000..2a20169 --- /dev/null +++ b/src/models/users/queries.rs @@ -0,0 +1,212 @@ +use tokio_postgres::GenericClient; +use uuid::Uuid; + +use crate::errors::DatabaseError; +use crate::models::profiles::queries::create_profile; +use crate::models::profiles::types::{DbActorProfile, ProfileCreateData}; +use crate::utils::crypto::generate_random_string; +use super::types::{DbUser, User, UserRegistrationData}; + +pub async fn generate_invite_code( + db_client: &impl GenericClient, +) -> Result { + let invite_code = generate_random_string(); + db_client.execute( + " + INSERT INTO user_invite_code (code) + VALUES ($1) + ", + &[&invite_code], + ).await?; + Ok(invite_code) +} + +pub async fn get_invite_codes( + db_client: &impl GenericClient, +) -> Result, DatabaseError> { + let rows = db_client.query( + " + SELECT code + FROM user_invite_code + WHERE used = FALSE + ", + &[], + ).await?; + let codes: Vec = rows.iter() + .map(|row| row.try_get("code")) + .collect::>()?; + Ok(codes) +} + +pub async fn is_valid_invite_code( + db_client: &impl GenericClient, + invite_code: &str, +) -> Result { + let maybe_row = db_client.query_opt( + " + SELECT 1 FROM user_invite_code + WHERE code = $1 AND used = FALSE + ", + &[&invite_code], + ).await?; + Ok(maybe_row.is_some()) +} + +pub async fn get_user_by_id( + db_client: &impl GenericClient, + user_id: &Uuid, +) -> Result { + let maybe_row = db_client.query_opt( + " + SELECT user_account, actor_profile + FROM user_account JOIN actor_profile USING (id) + WHERE id = $1 + ", + &[&user_id], + ).await?; + let row = maybe_row.ok_or(DatabaseError::NotFound("user"))?; + let db_user: DbUser = row.try_get("user_account")?; + let db_profile: DbActorProfile = row.try_get("actor_profile")?; + let user = User { + id: db_user.id, + wallet_address: db_user.wallet_address, + password_hash: db_user.password_hash, + private_key: db_user.private_key, + profile: db_profile, + }; + Ok(user) +} + +pub async fn get_user_by_name( + db_client: &impl GenericClient, + username: &str, +) -> Result { + let maybe_row = db_client.query_opt( + " + SELECT user_account, actor_profile + FROM user_account JOIN actor_profile USING (id) + WHERE actor_profile.username = $1 + ", + &[&username], + ).await?; + let row = maybe_row.ok_or(DatabaseError::NotFound("user"))?; + let db_user: DbUser = row.try_get("user_account")?; + let db_profile: DbActorProfile = row.try_get("actor_profile")?; + let user = User { + id: db_user.id, + wallet_address: db_user.wallet_address, + password_hash: db_user.password_hash, + private_key: db_user.private_key, + profile: db_profile, + }; + Ok(user) +} + +pub async fn is_registered_user( + db_client: &impl GenericClient, + username: &str, +) -> Result { + let maybe_row = db_client.query_opt( + " + SELECT 1 FROM user_account JOIN actor_profile USING (id) + WHERE actor_profile.username = $1 + ", + &[&username], + ).await?; + Ok(maybe_row.is_some()) +} + +pub async fn create_user( + db_client: &mut impl GenericClient, + form: UserRegistrationData, + password_hash: String, + private_key_pem: String, +) -> Result { + let transaction = db_client.transaction().await?; + // Use invite code + if let Some(ref invite_code) = form.invite_code { + let updated_count = transaction.execute( + " + UPDATE user_invite_code + SET used = TRUE + WHERE code = $1 AND used = FALSE + ", + &[&invite_code], + ).await?; + if updated_count == 0 { + Err(DatabaseError::NotFound("invite code"))?; + } + } + // Create profile + let profile_data = ProfileCreateData { + username: form.username.clone(), + display_name: None, + acct: form.username.clone(), + bio: None, + avatar: None, + banner: None, + actor: None, + }; + let profile = create_profile(&transaction, &profile_data).await?; + // Create user + let result = transaction.query_one( + " + INSERT INTO user_account ( + id, wallet_address, password_hash, private_key, invite_code + ) + VALUES ($1, $2, $3, $4, $5) + RETURNING user_account + ", + &[ + &profile.id, + &form.wallet_address, + &password_hash, + &private_key_pem, + &form.invite_code, + ], + ).await; + match result { + Ok(row) => { + transaction.commit().await?; + let db_user: DbUser = row.try_get("user_account")?; + let user = User { + id: db_user.id, + wallet_address: db_user.wallet_address, + password_hash: db_user.password_hash, + private_key: db_user.private_key, + profile, + }; + Ok(user) + }, + Err(err) => { + // TODO: catch user already exists error + log::info!("{}", err); + Err(DatabaseError::AlreadyExists("user"))? + }, + } +} + +pub async fn get_user_by_wallet_address( + db_client: &impl GenericClient, + wallet_address: &str, +) -> Result { + let maybe_row = db_client.query_opt( + " + SELECT user_account, actor_profile + FROM user_account JOIN actor_profile USING (id) + WHERE wallet_address = $1 + ", + &[&wallet_address], + ).await?; + let row = maybe_row.ok_or(DatabaseError::NotFound("user"))?; + let db_user: DbUser = row.try_get("user_account")?; + let db_profile: DbActorProfile = row.try_get("actor_profile")?; + let user = User { + id: db_user.id, + wallet_address: db_user.wallet_address, + password_hash: db_user.password_hash, + private_key: db_user.private_key, + profile: db_profile, + }; + Ok(user) +} diff --git a/src/models/users/types.rs b/src/models/users/types.rs new file mode 100644 index 0000000..ae65e24 --- /dev/null +++ b/src/models/users/types.rs @@ -0,0 +1,72 @@ +use chrono::{DateTime, Utc}; +use postgres_types::FromSql; +use regex::Regex; +use serde::Deserialize; +use uuid::Uuid; + +use crate::errors::ValidationError; +use crate::models::profiles::types::DbActorProfile; + +#[derive(FromSql)] +#[postgres(name = "user_account")] +pub struct DbUser { + pub id: Uuid, + pub wallet_address: String, + pub password_hash: String, + pub private_key: String, + pub invite_code: Option, + pub created_at: DateTime, +} + +// Represents local user +#[derive(Clone)] +pub struct User { + pub id: Uuid, + pub wallet_address: String, + pub password_hash: String, + pub private_key: String, + pub profile: DbActorProfile, +} + +#[derive(Deserialize)] +pub struct UserRegistrationData { + pub username: String, + pub signature: String, + pub wallet_address: String, + pub invite_code: Option, +} + +fn validate_username(username: &str) -> Result<(), ValidationError> { + let username_regexp = Regex::new(r"^[a-z0-9_]+$").unwrap(); + if !username_regexp.is_match(username) { + return Err(ValidationError("invalid username")); + } + Ok(()) +} + +impl UserRegistrationData { + /// Validate and clean. + pub fn clean(&self) -> Result<(), ValidationError> { + validate_username(&self.username)?; + Ok(()) + } +} + +#[derive(Deserialize)] +pub struct UserLoginData { + pub signature: String, + pub wallet_address: String, +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_validate_username() { + let result_1 = validate_username("name_1"); + assert_eq!(result_1.is_ok(), true); + let result_2 = validate_username("name&"); + assert_eq!(result_2.is_ok(), false); + } +} diff --git a/src/nodeinfo/mod.rs b/src/nodeinfo/mod.rs new file mode 100644 index 0000000..718ba5f --- /dev/null +++ b/src/nodeinfo/mod.rs @@ -0,0 +1,2 @@ +mod types; +pub mod views; diff --git a/src/nodeinfo/types.rs b/src/nodeinfo/types.rs new file mode 100644 index 0000000..a687908 --- /dev/null +++ b/src/nodeinfo/types.rs @@ -0,0 +1,68 @@ +/// http://nodeinfo.diaspora.software/schema.html + +use serde::Serialize; + +use crate::config::Config; + +#[derive(Serialize)] +struct Software { + name: String, + version: String, +} + +#[derive(Serialize)] +struct Services { + inbound: Vec, + outbound: Vec, +} + +#[derive(Serialize)] +struct Users { +} + +#[derive(Serialize)] +struct Usage { + users: Users, +} + +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +struct Metadata { + node_name: String, + node_description: String, +} + +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +pub struct NodeInfo20 { + version: String, + software: Software, + protocols: Vec, + services: Services, + open_registrations: bool, + usage: Usage, + metadata: Metadata, +} + +impl NodeInfo20 { + pub fn new(config: &Config) -> Self { + let software = Software { + name: "mitra".to_string(), + version: config.version.clone(), + }; + let services = Services { inbound: vec![], outbound: vec![] }; + let metadata = Metadata { + node_name: config.instance_title.clone(), + node_description: config.instance_short_description.clone(), + }; + Self { + version: "2.0".to_string(), + software, + protocols: vec!["activitypub".to_string()], + services, + open_registrations: config.registrations_open, + usage: Usage { users: Users { } }, + metadata, + } + } +} diff --git a/src/nodeinfo/views.rs b/src/nodeinfo/views.rs new file mode 100644 index 0000000..82e795a --- /dev/null +++ b/src/nodeinfo/views.rs @@ -0,0 +1,39 @@ +/// http://nodeinfo.diaspora.software/protocol.html + +use actix_web::{get, web, HttpResponse}; + +use crate::config::Config; +use crate::errors::HttpError; +use crate::webfinger::types::{ + Link, + JsonResourceDescriptor, +}; +use super::types::NodeInfo20; + +#[get("/.well-known/nodeinfo")] +pub async fn get_nodeinfo( + config: web::Data, +) -> Result { + let nodeinfo_2_0_url = format!("{}/nodeinfo/2.0", config.instance_url()); + let link = Link { + rel: "http://nodeinfo.diaspora.software/ns/schema/2.0".to_string(), + link_type: None, + href: Some(nodeinfo_2_0_url), + }; + let jrd = JsonResourceDescriptor { + subject: config.instance_url(), + links: vec![link], + }; + let response = HttpResponse::Ok().json(jrd); + Ok(response) +} + +#[get("/nodeinfo/2.0")] +pub async fn get_nodeinfo_2_0( + config: web::Data, +) -> Result { + let nodeinfo = NodeInfo20::new(&config); + let response = HttpResponse::Ok().json(nodeinfo); + Ok(response) +} + diff --git a/src/scheduler.rs b/src/scheduler.rs new file mode 100644 index 0000000..f45b9dd --- /dev/null +++ b/src/scheduler.rs @@ -0,0 +1,24 @@ +use std::time::Duration; + +use crate::config::Config; +use crate::database::Pool; +use crate::ethereum::nft::{get_nft_contract, process_events}; + +pub fn run(config: Config, db_pool: Pool) -> () { + actix_rt::spawn(async move { + let mut interval = actix_rt::time::interval(Duration::from_secs(30)); + // Verify config and create contract interface + let web3_contract = get_nft_contract(&config).await + .map_err(|err| log::error!("{}", err)) + .ok(); + loop { + interval.tick().await; + // Process events only if contract is properly configured + if let Some((web3, contract)) = web3_contract.as_ref() { + process_events(web3, contract, &db_pool).await.unwrap_or_else(|err| { + log::error!("{}", err); + }); + } + } + }); +} diff --git a/src/utils/crypto.rs b/src/utils/crypto.rs new file mode 100644 index 0000000..1c9c7ba --- /dev/null +++ b/src/utils/crypto.rs @@ -0,0 +1,115 @@ +use rand; +use rand::prelude::*; +use rsa::{Hash, PaddingScheme, PublicKey, RsaPrivateKey, RsaPublicKey}; +use rsa::pkcs8::{FromPrivateKey, FromPublicKey, ToPrivateKey, ToPublicKey}; +use sha2::{Digest, Sha256}; + +pub fn generate_random_string() -> String { + let mut rng = rand::thread_rng(); + let value: [u8; 16] = rng.gen(); + hex::encode(value) +} + +pub fn hash_password(password: &str) -> Result { + let mut rng = rand::thread_rng(); + let salt: [u8; 32] = rng.gen(); + let config = argon2::Config::default(); + + argon2::hash_encoded(password.as_bytes(), &salt, &config) +} + +pub fn verify_password( + password_hash: &str, + password: &str, +) -> Result { + argon2::verify_encoded(password_hash, password.as_bytes()) +} + +pub fn generate_private_key() -> Result { + let mut rng = rand::rngs::OsRng; + let bits = 2048; + RsaPrivateKey::new(&mut rng, bits) +} + +pub fn serialize_private_key( + private_key: RsaPrivateKey, +) -> Result { + private_key.to_pkcs8_pem().map(|val| val.to_string()) +} + +pub fn deserialize_private_key( + private_key_pem: &str, +) -> Result { + RsaPrivateKey::from_pkcs8_pem(&private_key_pem) +} + +pub fn get_public_key_pem( + private_key: &RsaPrivateKey, +) -> Result { + let public_key = RsaPublicKey::from(private_key); + public_key.to_public_key_pem() +} + +pub fn deserialize_public_key( + public_key_pem: &str, +) -> Result { + RsaPublicKey::from_public_key_pem(&public_key_pem.trim()) +} + +pub fn sign_message( + private_key: &RsaPrivateKey, + message: &str, +) -> Result { + let digest = Sha256::digest(message.as_bytes()); + let padding = PaddingScheme::new_pkcs1v15_sign(Some(Hash::SHA2_256)); + let signature = private_key.sign(padding, &digest)?; + let signature_b64 = base64::encode(&signature); + Ok(signature_b64) +} + +pub fn get_message_digest(message: &str) -> String { + let digest = Sha256::digest(message.as_bytes()); + let digest_b64 = base64::encode(digest); + format!("SHA-256={}", digest_b64) +} + +pub fn verify_signature( + public_key: &RsaPublicKey, + message: &str, + signature_b64: &str, +) -> Result { + let digest = Sha256::digest(message.as_bytes()); + let padding = PaddingScheme::new_pkcs1v15_sign(Some(Hash::SHA2_256)); + let signature = base64::decode(signature_b64)?; + let is_valid = public_key.verify( + padding, + &digest, + &signature, + ).is_ok(); + Ok(is_valid) +} + +#[cfg(test)] +mod tests { + use rand::rngs::OsRng; + use super::*; + + #[test] + fn test_public_key_serialization_deserialization() { + let private_key = RsaPrivateKey::new(&mut OsRng, 512).unwrap(); + let public_key_pem = get_public_key_pem(&private_key).unwrap(); + let public_key = deserialize_public_key(&public_key_pem).unwrap(); + assert_eq!(public_key, RsaPublicKey::from(&private_key)); + } + + #[test] + fn test_verify_signature() { + let private_key = RsaPrivateKey::new(&mut OsRng, 512).unwrap(); + let message = "test".to_string(); + let signature = sign_message(&private_key, &message).unwrap(); + let public_key = RsaPublicKey::from(&private_key); + + let is_valid = verify_signature(&public_key, &message, &signature).unwrap(); + assert_eq!(is_valid, true); + } +} diff --git a/src/utils/files.rs b/src/utils/files.rs new file mode 100644 index 0000000..7701cac --- /dev/null +++ b/src/utils/files.rs @@ -0,0 +1,68 @@ +use std::fs::File; +use std::io::prelude::*; +use std::path::PathBuf; + +use mime_guess::get_mime_extensions_str; +use mime_sniffer::MimeTypeSniffer; +use sha2::{Digest, Sha256}; + +#[derive(thiserror::Error, Debug)] +pub enum FileError { + #[error(transparent)] + WriteError(#[from] std::io::Error), + + #[error("base64 decoding error")] + Base64DecodingError(#[from] base64::DecodeError), + + #[error("invalid media type")] + InvalidMediaType, +} + +pub fn save_file(data: Vec, output_dir: &PathBuf) -> Result { + let digest = Sha256::digest(&data); + let mut file_name = hex::encode(digest); + let maybe_extension = data.sniff_mime_type() + .and_then(|media_type| get_mime_extensions_str(media_type)) + .and_then(|extensions| extensions.first()); + if let Some(extension) = maybe_extension { + // Append extension for known media types + file_name = format!("{}.{}", file_name, extension); + } + let file_path = output_dir.join(&file_name); + let mut file = File::create(&file_path)?; + file.write_all(&data)?; + Ok(file_name) +} + +fn sniff_media_type(data: &Vec) -> Option { + data.sniff_mime_type().map(|val| val.to_string()) +} + +pub fn save_b64_file( + b64data: &str, + output_dir: &PathBuf, +) -> Result<(String, Option), FileError> { + let data = base64::decode(b64data)?; + let media_type = sniff_media_type(&data); + let file_name = save_file(data, output_dir)?; + Ok((file_name, media_type)) +} + +pub fn save_validated_b64_file( + b64data: &str, + output_dir: &PathBuf, + media_type_prefix: &str, +) -> Result<(String, String), FileError> { + let data = base64::decode(b64data)?; + let media_type = sniff_media_type(&data) + .ok_or(FileError::InvalidMediaType)?; + if !media_type.starts_with(media_type_prefix) { + return Err(FileError::InvalidMediaType); + } + let file_name = save_file(data, output_dir)?; + Ok((file_name, media_type)) +} + +pub fn get_file_url(instance_url: &str, file_name: &str) -> String { + format!("{}/media/{}", instance_url, file_name) +} diff --git a/src/utils/html.rs b/src/utils/html.rs new file mode 100644 index 0000000..1652cc8 --- /dev/null +++ b/src/utils/html.rs @@ -0,0 +1,27 @@ +use std::collections::HashSet; + +use ammonia::Builder; + +pub fn clean_html(unsafe_html: &str) -> String { + let mut allowed_tags = HashSet::new(); + allowed_tags.insert("a"); + allowed_tags.insert("br"); + + let safe_html = Builder::default() + .tags(allowed_tags) + .clean(unsafe_html) + .to_string(); + safe_html +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_clean_html() { + let unsafe_html = r#"

test bold with link

"#; + let safe_html = clean_html(unsafe_html); + assert_eq!(safe_html, r#"test bold with link"#); + } +} diff --git a/src/utils/mod.rs b/src/utils/mod.rs new file mode 100644 index 0000000..960d114 --- /dev/null +++ b/src/utils/mod.rs @@ -0,0 +1,3 @@ +pub mod crypto; +pub mod files; +pub mod html; diff --git a/src/webfinger/mod.rs b/src/webfinger/mod.rs new file mode 100644 index 0000000..a964b01 --- /dev/null +++ b/src/webfinger/mod.rs @@ -0,0 +1,2 @@ +pub mod types; +pub mod views; diff --git a/src/webfinger/types.rs b/src/webfinger/types.rs new file mode 100644 index 0000000..af00a67 --- /dev/null +++ b/src/webfinger/types.rs @@ -0,0 +1,27 @@ +/// https://webfinger.net/ + +use serde::{Serialize, Deserialize}; + +pub const JRD_CONTENT_TYPE: &str = "application/jrd+json"; + +#[derive(Deserialize)] +pub struct WebfingerQueryParams { + pub resource: String, +} + +#[derive(Serialize, Deserialize)] +pub struct Link { + pub rel: String, + + #[serde(rename = "type", skip_serializing_if = "Option::is_none")] + pub link_type: Option, + + pub href: Option, +} + +// https://datatracker.ietf.org/doc/html/rfc7033#section-4.4 +#[derive(Serialize, Deserialize)] +pub struct JsonResourceDescriptor { + pub subject: String, + pub links: Vec, +} diff --git a/src/webfinger/views.rs b/src/webfinger/views.rs new file mode 100644 index 0000000..af8e7e7 --- /dev/null +++ b/src/webfinger/views.rs @@ -0,0 +1,66 @@ +use actix_web::{get, web, HttpResponse}; +use regex::Regex; + +use crate::activitypub::views::get_actor_url; +use crate::activitypub::constants::ACTIVITY_CONTENT_TYPE; +use crate::config::Config; +use crate::database::{Pool, get_database_client}; +use crate::errors::HttpError; +use crate::models::users::queries::is_registered_user; +use super::types::{ + JRD_CONTENT_TYPE, + WebfingerQueryParams, + Link, + JsonResourceDescriptor, +}; + +pub async fn get_user_info( + db_pool: &Pool, + config: &Config, + query_params: WebfingerQueryParams, +) -> Result { + // Parse 'acct' URI + // https://datatracker.ietf.org/doc/html/rfc7565#section-7 + let uri_regexp = Regex::new(r"acct:(?P\w+)@(?P.+)").unwrap(); + let uri_caps = uri_regexp.captures(&query_params.resource) + .ok_or(HttpError::ValidationError("invalid query target".into()))?; + let username = uri_caps.name("user") + .ok_or(HttpError::ValidationError("invalid query target".into()))? + .as_str(); + let instance_uri = uri_caps.name("instance") + .ok_or(HttpError::ValidationError("invalid query target".into()))? + .as_str(); + + if instance_uri != config.instance_uri { + // Wrong instance URI + return Err(HttpError::NotFoundError("user")); + } + let db_client = &**get_database_client(db_pool).await?; + if !is_registered_user(db_client, &username).await? { + return Err(HttpError::NotFoundError("user")); + } + let actor_url = get_actor_url(&config.instance_url(), &username); + let link = Link { + rel: "self".to_string(), + link_type: Some(ACTIVITY_CONTENT_TYPE.to_string()), + href: Some(actor_url), + }; + let jrd = JsonResourceDescriptor { + subject: query_params.resource, + links: vec![link], + }; + Ok(jrd) +} + +#[get("/.well-known/webfinger")] +async fn get_descriptor( + config: web::Data, + db_bool: web::Data, + query_params: web::Query, +) -> Result { + let jrd = get_user_info(&db_bool, &config, query_params.into_inner()).await?; + let response = HttpResponse::Ok() + .content_type(JRD_CONTENT_TYPE) + .json(jrd); + Ok(response) +}