diff --git a/.env.example b/.env.example index 3c74235..91ba6cc 100644 --- a/.env.example +++ b/.env.example @@ -3,3 +3,4 @@ CONTRACT= IS_MAINNET=false SECRET_KEY= MESSAGE_FILE=./Messages.toml +ROCKET_DATABASES={race-of-sloths={url="postgres:://user:password@127.0.0.1:5432/db?sslmode=disable"}} diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index ef216b2..9600115 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -41,7 +41,7 @@ jobs: cargo clippy --release -- -D warnings -A clippy::too_many_arguments - name: build bot if: steps.git-diff.outputs.diff - run: cargo build --release --package race-of-sloths-bot + run: cargo build --release - name: build contract if: steps.git-diff.outputs.diff run: cd contract && cargo near build diff --git a/Cargo.lock b/Cargo.lock index f3e266c..267700d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -101,6 +101,19 @@ dependencies = [ "version_check", ] +[[package]] +name = "ahash" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" +dependencies = [ + "cfg-if 1.0.0", + "getrandom 0.2.15", + "once_cell", + "version_check", + "zerocopy", +] + [[package]] name = "aho-corasick" version = "1.1.3" @@ -110,6 +123,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "allocator-api2" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f" + [[package]] name = "android-tzdata" version = "0.1.1" @@ -240,6 +259,30 @@ dependencies = [ "syn 2.0.63", ] +[[package]] +name = "atoi" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528" +dependencies = [ + "num-traits", +] + +[[package]] +name = "atomic" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c59bdb34bc650a32731b31bd8f0829cc15d24a708ee31559e0bb34f2bc320cba" + +[[package]] +name = "atomic" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d818003e740b63afc82337e3160717f4f63078720a810b7b903e70a5d1d2994" +dependencies = [ + "bytemuck", +] + [[package]] name = "atty" version = "0.2.14" @@ -314,6 +357,12 @@ dependencies = [ "zip 0.6.6", ] +[[package]] +name = "binascii" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "383d29d513d8764dcdc42ea295d979eb99c3c9f00607b3692cf68a431f7dca72" + [[package]] name = "bitflags" version = "1.3.2" @@ -325,6 +374,9 @@ name = "bitflags" version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" +dependencies = [ + "serde", +] [[package]] name = "bitvec" @@ -457,6 +509,12 @@ version = "3.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" +[[package]] +name = "bytemuck" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78834c15cb5d5efe3452d58b1e8ba890dd62d21907f867f383358198e56ebca5" + [[package]] name = "byteorder" version = "1.5.0" @@ -511,9 +569,9 @@ dependencies = [ [[package]] name = "camino" -version = "1.1.6" +version = "1.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c59e92b5a388f549b863a7bea62612c09f24c8393560709a54558a9abdfb3b9c" +checksum = "e0ec6b951b160caa93cc0c7b209e5a3bff7aae9062213451ac99493cd844c239" dependencies = [ "serde", ] @@ -723,6 +781,12 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + [[package]] name = "constant_time_eq" version = "0.1.5" @@ -735,6 +799,17 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" +[[package]] +name = "cookie" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747" +dependencies = [ + "percent-encoding", + "time", + "version_check", +] + [[package]] name = "core-foundation" version = "0.9.4" @@ -760,11 +835,26 @@ dependencies = [ "libc", ] +[[package]] +name = "crc" +version = "3.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69e6e4d7b33a94f0991c26729976b10ebde1d34c3ee82408fb536164fa10d636" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" + [[package]] name = "crc32fast" -version = "1.4.0" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3855a8a784b474f333699ef2bbca9db2c4a1f6d9088a90a2d25b1eb53111eaa" +checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" dependencies = [ "cfg-if 1.0.0", ] @@ -778,6 +868,15 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "crossbeam-queue" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df0346b5d5e76ac2fe4e327c5fd1118d6be7c51dfb18f9b7922923f287471e35" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "crossbeam-utils" version = "0.8.19" @@ -883,6 +982,17 @@ dependencies = [ "uuid", ] +[[package]] +name = "der" +version = "0.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f55bf8e7b65898637379c1b74eb1551107c8294ed26d855ceb9fd1a09cfc9bc0" +dependencies = [ + "const-oid", + "pem-rfc7468", + "zeroize", +] + [[package]] name = "deranged" version = "0.3.11" @@ -917,6 +1027,39 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "devise" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6eacefd3f541c66fc61433d65e54e0e46e0a029a819a7dbbc7a7b489e8a85f8" +dependencies = [ + "devise_codegen", + "devise_core", +] + +[[package]] +name = "devise_codegen" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8cf4b8dd484ede80fd5c547592c46c3745a617c8af278e2b72bea86b2dfed6" +dependencies = [ + "devise_core", + "quote", +] + +[[package]] +name = "devise_core" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35b50dba0afdca80b187392b24f2499a88c336d5a8493e4b4ccfb608708be56a" +dependencies = [ + "bitflags 2.5.0", + "proc-macro2", + "proc-macro2-diagnostics", + "quote", + "syn 2.0.63", +] + [[package]] name = "digest" version = "0.9.0" @@ -933,6 +1076,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer", + "const-oid", "crypto-common", "subtle", ] @@ -970,6 +1114,12 @@ version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f" +[[package]] +name = "dotenvy" +version = "0.15.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" + [[package]] name = "dyn-clone" version = "1.0.17" @@ -1009,6 +1159,9 @@ name = "either" version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a47c1c47d2f5964e29c61246e81db715514cd532db6b5116a25ea3c03d6780a2" +dependencies = [ + "serde", +] [[package]] name = "elementtree" @@ -1087,6 +1240,23 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "etcetera" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "136d1b5283a1ab77bd9257427ffd09d8667ced0570b6f938942bc7568ed5b943" +dependencies = [ + "cfg-if 1.0.0", + "home", + "windows-sys 0.48.0", +] + +[[package]] +name = "event-listener" +version = "2.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" + [[package]] name = "fallible-iterator" version = "0.2.0" @@ -1105,6 +1275,20 @@ version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" +[[package]] +name = "figment" +version = "0.10.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cb01cd46b0cf372153850f4c6c272d9cbea2da513e07538405148f95bd789f3" +dependencies = [ + "atomic 0.6.0", + "pear", + "serde", + "toml 0.8.12", + "uncased", + "version_check", +] + [[package]] name = "filetime" version = "0.2.23" @@ -1142,6 +1326,17 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "flume" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55ac459de2512911e4b674ce33cf20befaba382d05b62b008afc1c8b57cbf181" +dependencies = [ + "futures-core", + "futures-sink", + "spin 0.9.8", +] + [[package]] name = "fnv" version = "1.0.7" @@ -1230,6 +1425,17 @@ dependencies = [ "futures-util", ] +[[package]] +name = "futures-intrusive" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d930c203dd0b6ff06e0201a4a2fe9149b43c684fd4420555b26d21b1a02956f" +dependencies = [ + "futures-core", + "lock_api", + "parking_lot", +] + [[package]] name = "futures-io" version = "0.3.30" @@ -1277,6 +1483,19 @@ dependencies = [ "slab", ] +[[package]] +name = "generator" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cc16584ff22b460a382b7feec54b23d2908d858152e5739a120b949293bd74e" +dependencies = [ + "cc", + "libc", + "log", + "rustversion", + "windows", +] + [[package]] name = "generic-array" version = "0.14.7" @@ -1327,6 +1546,12 @@ version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" +[[package]] +name = "glob" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" + [[package]] name = "goblin" version = "0.5.4" @@ -1363,7 +1588,7 @@ version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" dependencies = [ - "ahash", + "ahash 0.7.8", ] [[package]] @@ -1377,6 +1602,19 @@ name = "hashbrown" version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +dependencies = [ + "ahash 0.8.11", + "allocator-api2", +] + +[[package]] +name = "hashlink" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8094feaf31ff591f651a2664fb9cfd92bba7a60ce3197265e9482ebe753c8f7" +dependencies = [ + "hashbrown 0.14.5", +] [[package]] name = "heck" @@ -1392,6 +1630,9 @@ name = "heck" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" +dependencies = [ + "unicode-segmentation", +] [[package]] name = "heck" @@ -1423,6 +1664,15 @@ dependencies = [ "serde", ] +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac", +] + [[package]] name = "hmac" version = "0.12.1" @@ -1569,7 +1819,7 @@ dependencies = [ "hyper 1.3.1", "hyper-util", "log", - "rustls", + "rustls 0.22.4", "rustls-native-certs", "rustls-pki-types", "tokio", @@ -1702,6 +1952,12 @@ dependencies = [ "serde", ] +[[package]] +name = "inlinable_string" +version = "0.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8fae54786f62fb2918dcfae3d568594e50eb9b5c25bf04371af6fe7516452fb" + [[package]] name = "inout" version = "0.1.3" @@ -1727,6 +1983,17 @@ dependencies = [ "serde", ] +[[package]] +name = "is-terminal" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f23ff5ef2b80d608d61efee834934d862cd92461afc0560dedf493e4c033738b" +dependencies = [ + "hermit-abi 0.3.9", + "libc", + "windows-sys 0.52.0", +] + [[package]] name = "is_executable" version = "0.1.2" @@ -1751,6 +2018,15 @@ dependencies = [ "either", ] +[[package]] +name = "itertools" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.11" @@ -1853,6 +2129,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "libm" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" + [[package]] name = "libredox" version = "0.1.3" @@ -1863,6 +2145,17 @@ dependencies = [ "libc", ] +[[package]] +name = "libsqlite3-sys" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf4e226dcd58b4be396f7bd3c20da8fdee2911400705297ba7d2d7cc2c30f716" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] + [[package]] name = "linux-raw-sys" version = "0.4.13" @@ -1885,6 +2178,21 @@ version = "0.4.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" +[[package]] +name = "loom" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff50ecb28bb86013e935fb6683ab1f6d3a20016f123c76fd4c27470076ac30f5" +dependencies = [ + "cfg-if 1.0.0", + "generator", + "scoped-tls", + "serde", + "serde_json", + "tracing", + "tracing-subscriber", +] + [[package]] name = "matchers" version = "0.1.0" @@ -1894,6 +2202,16 @@ dependencies = [ "regex-automata 0.1.10", ] +[[package]] +name = "md-5" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" +dependencies = [ + "cfg-if 1.0.0", + "digest 0.10.7", +] + [[package]] name = "memchr" version = "2.7.2" @@ -1956,6 +2274,25 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "multer" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83e87776546dc87511aa5ee218730c92b666d7264ab6ed41f9d215af9cd5224b" +dependencies = [ + "bytes", + "encoding_rs", + "futures-util", + "http 1.1.0", + "httparse", + "memchr", + "mime", + "spin 0.9.8", + "tokio", + "tokio-util 0.7.11", + "version_check", +] + [[package]] name = "multimap" version = "0.8.3" @@ -2456,6 +2793,23 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-bigint-dig" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc84195820f291c7697304f3cbdadd1cb7199c0efc917ff5eafd71225c136151" +dependencies = [ + "byteorder", + "lazy_static", + "libm", + "num-integer", + "num-iter", + "num-traits", + "rand 0.8.5", + "smallvec", + "zeroize", +] + [[package]] name = "num-conv" version = "0.1.0" @@ -2472,7 +2826,18 @@ dependencies = [ ] [[package]] -name = "num-rational" +name = "num-iter" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-rational" version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "12ac428b1cb17fce6f731001d307d351ec70a6d202fc2e60f7d4c5e42d8f4f07" @@ -2491,6 +2856,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", + "libm", ] [[package]] @@ -2700,6 +3066,12 @@ dependencies = [ "subtle", ] +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + [[package]] name = "pbkdf2" version = "0.11.0" @@ -2723,6 +3095,29 @@ dependencies = [ "uuid", ] +[[package]] +name = "pear" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdeeaa00ce488657faba8ebf44ab9361f9365a97bd39ffb8a60663f57ff4b467" +dependencies = [ + "inlinable_string", + "pear_codegen", + "yansi", +] + +[[package]] +name = "pear_codegen" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bab5b985dc082b345f812b7df84e1bef27e7207b39e448439ba8bd69c93f147" +dependencies = [ + "proc-macro2", + "proc-macro2-diagnostics", + "quote", + "syn 2.0.63", +] + [[package]] name = "pem" version = "3.0.4" @@ -2733,6 +3128,15 @@ dependencies = [ "serde", ] +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + [[package]] name = "percent-encoding" version = "2.3.1" @@ -2790,6 +3194,27 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "pkcs1" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" +dependencies = [ + "der", + "pkcs8", + "spki", +] + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + [[package]] name = "pkg-config" version = "0.3.30" @@ -2893,6 +3318,19 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "proc-macro2-diagnostics" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.63", + "version_check", + "yansi", +] + [[package]] name = "prometheus" version = "0.13.4" @@ -2926,7 +3364,7 @@ checksum = "62941722fb675d463659e49c4f3fe1fe792ff24fe5bbaa9c08cd3b98a1c354f5" dependencies = [ "bytes", "heck 0.3.3", - "itertools", + "itertools 0.10.5", "lazy_static", "log", "multimap", @@ -2945,7 +3383,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f9cc1a3263e07e0bf68e96268f37665207b49560d98739662cdfaae215c720fe" dependencies = [ "anyhow", - "itertools", + "itertools 0.10.5", "proc-macro2", "quote", "syn 1.0.109", @@ -2981,24 +3419,38 @@ name = "race-of-sloths-bot" version = "0.1.0" dependencies = [ "anyhow", - "async-trait", "chrono", "dotenv", "envy", "futures", - "hex", - "near-workspaces", "octocrab", "rand 0.8.5", "serde", "serde_json", - "shared-types", + "shared", "tokio", "toml 0.8.12", "tracing", "tracing-subscriber", ] +[[package]] +name = "race-of-sloths-server" +version = "0.1.0" +dependencies = [ + "anyhow", + "dotenv", + "envy", + "rocket", + "rocket_db_pools", + "serde", + "shared", + "sqlx", + "svg", + "tracing", + "tracing-subscriber", +] + [[package]] name = "radium" version = "0.7.0" @@ -3114,6 +3566,26 @@ dependencies = [ "smallvec", ] +[[package]] +name = "ref-cast" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf0a6f84d5f1d581da8b41b47ec8600871962f2a528115b542b362d4b744931" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bcc303e793d3734489387d205e9b186fac9c6cfacedd98cbb2e8a5943595f3e6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.63", +] + [[package]] name = "regex" version = "1.10.4" @@ -3222,6 +3694,130 @@ dependencies = [ "digest 0.10.7", ] +[[package]] +name = "rocket" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a516907296a31df7dc04310e7043b61d71954d703b603cc6867a026d7e72d73f" +dependencies = [ + "async-stream", + "async-trait", + "atomic 0.5.3", + "binascii", + "bytes", + "either", + "figment", + "futures", + "indexmap 2.2.6", + "log", + "memchr", + "multer", + "num_cpus", + "parking_lot", + "pin-project-lite", + "rand 0.8.5", + "ref-cast", + "rocket_codegen", + "rocket_http", + "serde", + "serde_json", + "state", + "tempfile", + "time", + "tokio", + "tokio-stream", + "tokio-util 0.7.11", + "ubyte", + "version_check", + "yansi", +] + +[[package]] +name = "rocket_codegen" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "575d32d7ec1a9770108c879fc7c47815a80073f96ca07ff9525a94fcede1dd46" +dependencies = [ + "devise", + "glob", + "indexmap 2.2.6", + "proc-macro2", + "quote", + "rocket_http", + "syn 2.0.63", + "unicode-xid", + "version_check", +] + +[[package]] +name = "rocket_db_pools" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6578b2740ceee3e78bff63fe9299d964b7e68318446cdcb9af3b9cab46e1e9d" +dependencies = [ + "rocket", + "rocket_db_pools_codegen", + "sqlx", + "version_check", +] + +[[package]] +name = "rocket_db_pools_codegen" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "842e859f2e87a23efc0f81e25756c0fb43f18726e62daf99da7ea19fbc56cebd" +dependencies = [ + "devise", + "quote", +] + +[[package]] +name = "rocket_http" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e274915a20ee3065f611c044bd63c40757396b6dbc057d6046aec27f14f882b9" +dependencies = [ + "cookie", + "either", + "futures", + "http 0.2.12", + "hyper 0.14.28", + "indexmap 2.2.6", + "log", + "memchr", + "pear", + "percent-encoding", + "pin-project-lite", + "ref-cast", + "serde", + "smallvec", + "stable-pattern", + "state", + "time", + "tokio", + "uncased", +] + +[[package]] +name = "rsa" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0e5124fcb30e76a7e79bfee683a2746db83784b86289f6251b54b7950a0dfc" +dependencies = [ + "const-oid", + "digest 0.10.7", + "num-bigint-dig", + "num-integer", + "num-traits", + "pkcs1", + "pkcs8", + "rand_core 0.6.4", + "signature", + "spki", + "subtle", + "zeroize", +] + [[package]] name = "rustc-demangle" version = "0.1.24" @@ -3256,6 +3852,17 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "rustls" +version = "0.21.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e" +dependencies = [ + "ring", + "rustls-webpki 0.101.7", + "sct", +] + [[package]] name = "rustls" version = "0.22.4" @@ -3265,7 +3872,7 @@ dependencies = [ "log", "ring", "rustls-pki-types", - "rustls-webpki", + "rustls-webpki 0.102.3", "subtle", "zeroize", ] @@ -3308,6 +3915,16 @@ version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "976295e77ce332211c0d24d92c0e83e50f5c5f046d11082cea19f3df13a3562d" +[[package]] +name = "rustls-webpki" +version = "0.101.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" +dependencies = [ + "ring", + "untrusted", +] + [[package]] name = "rustls-webpki" version = "0.102.3" @@ -3342,9 +3959,9 @@ dependencies = [ [[package]] name = "schemars" -version = "0.8.19" +version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc6e7ed6919cb46507fb01ff1654309219f62b4d603822501b0b80d42f6f21ef" +checksum = "09c024468a378b7e36765cd36702b7a90cc3cba11654f6685c8f233408e89e92" dependencies = [ "dyn-clone", "schemars_derive", @@ -3354,9 +3971,9 @@ dependencies = [ [[package]] name = "schemars_derive" -version = "0.8.19" +version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "185f2b7aa7e02d418e453790dde16890256bbd2bcd04b7dc5348811052b53f49" +checksum = "b1eee588578aff73f856ab961cd2f79e36bc45d7ded33a7562adba4667aecc0e" dependencies = [ "proc-macro2", "quote", @@ -3364,6 +3981,12 @@ dependencies = [ "syn 2.0.63", ] +[[package]] +name = "scoped-tls" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" + [[package]] name = "scopeguard" version = "1.2.0" @@ -3396,6 +4019,16 @@ dependencies = [ "syn 2.0.63", ] +[[package]] +name = "sct" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" +dependencies = [ + "ring", + "untrusted", +] + [[package]] name = "secp256k1" version = "0.27.0" @@ -3478,9 +4111,9 @@ dependencies = [ [[package]] name = "serde_derive_internals" -version = "0.29.0" +version = "0.29.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "330f01ce65a3a5fe59a60c82f3c9a024b573b8a6e875bd233fe5f934e71d54e3" +checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" dependencies = [ "proc-macro2", "quote", @@ -3625,12 +4258,17 @@ dependencies = [ ] [[package]] -name = "shared-types" +name = "shared" version = "0.1.0" dependencies = [ + "anyhow", "chrono", "near-sdk", + "near-workspaces", + "octocrab", + "serde_json", "strum 0.26.2", + "tracing", ] [[package]] @@ -3647,6 +4285,10 @@ name = "signature" version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "digest 0.10.7", + "rand_core 0.6.4", +] [[package]] name = "simple_asn1" @@ -3680,7 +4322,7 @@ name = "slothrace-storage-contract" version = "0.1.0" dependencies = [ "near-sdk", - "shared-types", + "shared", "tokio", ] @@ -3743,6 +4385,236 @@ name = "spin" version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +dependencies = [ + "lock_api", +] + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "sqlformat" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce81b7bd7c4493975347ef60d8c7e8b742d4694f4c49f93e0a12ea263938176c" +dependencies = [ + "itertools 0.12.1", + "nom", + "unicode_categories", +] + +[[package]] +name = "sqlx" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9a2ccff1a000a5a59cd33da541d9f2fdcd9e6e8229cc200565942bff36d0aaa" +dependencies = [ + "sqlx-core", + "sqlx-macros", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", +] + +[[package]] +name = "sqlx-core" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24ba59a9342a3d9bab6c56c118be528b27c9b60e490080e9711a04dccac83ef6" +dependencies = [ + "ahash 0.8.11", + "atoi", + "byteorder", + "bytes", + "crc", + "crossbeam-queue", + "either", + "event-listener", + "futures-channel", + "futures-core", + "futures-intrusive", + "futures-io", + "futures-util", + "hashlink", + "hex", + "indexmap 2.2.6", + "log", + "memchr", + "once_cell", + "paste", + "percent-encoding", + "rustls 0.21.12", + "rustls-pemfile 1.0.4", + "serde", + "serde_json", + "sha2", + "smallvec", + "sqlformat", + "thiserror", + "tokio", + "tokio-stream", + "tracing", + "url", + "webpki-roots 0.25.4", +] + +[[package]] +name = "sqlx-macros" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ea40e2345eb2faa9e1e5e326db8c34711317d2b5e08d0d5741619048a803127" +dependencies = [ + "proc-macro2", + "quote", + "sqlx-core", + "sqlx-macros-core", + "syn 1.0.109", +] + +[[package]] +name = "sqlx-macros-core" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5833ef53aaa16d860e92123292f1f6a3d53c34ba8b1969f152ef1a7bb803f3c8" +dependencies = [ + "dotenvy", + "either", + "heck 0.4.1", + "hex", + "once_cell", + "proc-macro2", + "quote", + "serde", + "serde_json", + "sha2", + "sqlx-core", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", + "syn 1.0.109", + "tempfile", + "tokio", + "url", +] + +[[package]] +name = "sqlx-mysql" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ed31390216d20e538e447a7a9b959e06ed9fc51c37b514b46eb758016ecd418" +dependencies = [ + "atoi", + "base64 0.21.7", + "bitflags 2.5.0", + "byteorder", + "bytes", + "crc", + "digest 0.10.7", + "dotenvy", + "either", + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "generic-array", + "hex", + "hkdf", + "hmac", + "itoa", + "log", + "md-5", + "memchr", + "once_cell", + "percent-encoding", + "rand 0.8.5", + "rsa", + "serde", + "sha1", + "sha2", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror", + "tracing", + "whoami", +] + +[[package]] +name = "sqlx-postgres" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c824eb80b894f926f89a0b9da0c7f435d27cdd35b8c655b114e58223918577e" +dependencies = [ + "atoi", + "base64 0.21.7", + "bitflags 2.5.0", + "byteorder", + "crc", + "dotenvy", + "etcetera", + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "hex", + "hkdf", + "hmac", + "home", + "itoa", + "log", + "md-5", + "memchr", + "once_cell", + "rand 0.8.5", + "serde", + "serde_json", + "sha2", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror", + "tracing", + "whoami", +] + +[[package]] +name = "sqlx-sqlite" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b244ef0a8414da0bed4bb1910426e890b19e5e9bccc27ada6b797d05c55ae0aa" +dependencies = [ + "atoi", + "flume", + "futures-channel", + "futures-core", + "futures-executor", + "futures-intrusive", + "futures-util", + "libsqlite3-sys", + "log", + "percent-encoding", + "serde", + "sqlx-core", + "tracing", + "url", + "urlencoding", +] + +[[package]] +name = "stable-pattern" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4564168c00635f88eaed410d5efa8131afa8d8699a612c80c455a0ba05c21045" +dependencies = [ + "memchr", +] [[package]] name = "stable_deref_trait" @@ -3750,6 +4622,15 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" +[[package]] +name = "state" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b8c4a4445d81357df8b1a650d0d0d6fbbbfe99d064aa5e02f3e4022061476d8" +dependencies = [ + "loom", +] + [[package]] name = "static_assertions" version = "1.1.0" @@ -3770,6 +4651,17 @@ dependencies = [ "serde", ] +[[package]] +name = "stringprep" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1" +dependencies = [ + "unicode-bidi", + "unicode-normalization", + "unicode-properties", +] + [[package]] name = "strsim" version = "0.10.0" @@ -3832,6 +4724,12 @@ version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" +[[package]] +name = "svg" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "700efb40f3f559c23c18b446e8ed62b08b56b2bb3197b36d57e0470b4102779e" + [[package]] name = "symbolic-common" version = "8.8.0" @@ -4122,7 +5020,7 @@ version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "775e0c0f0adb3a2f22a00c4745d728b479985fc15ee7ca6a2608388c5569860f" dependencies = [ - "rustls", + "rustls 0.22.4", "rustls-pki-types", "tokio", ] @@ -4435,6 +5333,15 @@ version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" +[[package]] +name = "ubyte" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f720def6ce1ee2fc44d40ac9ed6d3a59c361c80a75a7aa8e75bb9baed31cf2ea" +dependencies = [ + "serde", +] + [[package]] name = "uint" version = "0.9.5" @@ -4447,6 +5354,16 @@ dependencies = [ "static_assertions", ] +[[package]] +name = "uncased" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1b88fcfe09e89d3866a5c11019378088af2d24c3fbd4f0543f96b479ec90697" +dependencies = [ + "serde", + "version_check", +] + [[package]] name = "unicode-bidi" version = "0.3.15" @@ -4468,12 +5385,30 @@ dependencies = [ "tinyvec", ] +[[package]] +name = "unicode-properties" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4259d9d4425d9f0661581b804cb85fe66a4c631cadd8f490d1c13a35d5d9291" + [[package]] name = "unicode-segmentation" version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202" +[[package]] +name = "unicode-xid" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c" + +[[package]] +name = "unicode_categories" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e" + [[package]] name = "unsafe-libyaml" version = "0.2.11" @@ -4496,11 +5431,11 @@ dependencies = [ "flate2", "log", "once_cell", - "rustls", + "rustls 0.22.4", "rustls-pki-types", - "rustls-webpki", + "rustls-webpki 0.102.3", "url", - "webpki-roots", + "webpki-roots 0.26.1", ] [[package]] @@ -4515,6 +5450,12 @@ dependencies = [ "serde", ] +[[package]] +name = "urlencoding" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" + [[package]] name = "utf8parse" version = "0.2.1" @@ -4566,6 +5507,12 @@ version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +[[package]] +name = "wasite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" + [[package]] name = "wasm-bindgen" version = "0.2.92" @@ -4648,6 +5595,12 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "webpki-roots" +version = "0.25.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1" + [[package]] name = "webpki-roots" version = "0.26.1" @@ -4681,6 +5634,16 @@ dependencies = [ "rustix", ] +[[package]] +name = "whoami" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a44ab49fad634e88f55bf8f9bb3abd2f27d7204172a112c7c9987e01c1c94ea9" +dependencies = [ + "redox_syscall 0.4.1", + "wasite", +] + [[package]] name = "winapi" version = "0.3.9" @@ -4712,6 +5675,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" +dependencies = [ + "windows-targets 0.48.5", +] + [[package]] name = "windows-core" version = "0.52.0" @@ -4914,6 +5886,35 @@ version = "0.8.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "791978798f0597cfc70478424c2b4fdc2b7a8024aaff78497ef00f24ef674193" +[[package]] +name = "yansi" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" +dependencies = [ + "is-terminal", +] + +[[package]] +name = "zerocopy" +version = "0.7.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae87e3fcd617500e5d106f0380cf7b77f3c6092aae37191433159dda23cfb087" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15e934569e47891f7d9411f1a451d947a60e000ab3bd24fbb970f000387d1b3b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.63", +] + [[package]] name = "zeroize" version = "1.7.0" diff --git a/Cargo.toml b/Cargo.toml index 8788a01..4ddc6c6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [workspace] -members = ["server", "contract", "shared-types"] +members = ["bot", "contract", "server", "shared"] resolver = "2" [workspace.package] @@ -32,7 +32,11 @@ toml = "0.8" strum = { version = "0.26", no-default-features = true } tracing-subscriber = "0.3" -shared-types = { path = "shared-types" } +rocket = "0.5" +rocket_db_pools = "0.2" +svg = "0.17" +sqlx = "0.7" +shared = { path = "shared" } [profile.release] codegen-units = 1 diff --git a/Dockerfile b/Dockerfile index 62779bf..899bfbc 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,7 +4,7 @@ WORKDIR /usr/src/app COPY . . RUN --mount=type=cache,target=/usr/local/cargo,from=rust:latest,source=/usr/local/cargo \ --mount=type=cache,target=target \ - cargo build --release -p race-of-sloths-bot && mv ./target/release/race-of-sloths-bot ./race-of-sloths-bot + cargo build --release -p race-of-sloths-bot -p race-of-sloths-server && mv ./target/release/race-of-sloths-* ./ FROM debian:bookworm-slim @@ -15,6 +15,6 @@ USER app WORKDIR /app COPY --from=builder /usr/src/app/race-of-sloths-bot /app/race-of-sloths-bot +COPY --from=builder /usr/src/app/race-of-sloths-server /app/race-of-sloths-server COPY ./Messages.toml /app/Messages.toml - -CMD ./race-of-sloths-bot +COPY ./Rocket.toml /app/Rocket.toml diff --git a/Rocket.toml b/Rocket.toml new file mode 100644 index 0000000..a2d629e --- /dev/null +++ b/Rocket.toml @@ -0,0 +1,4 @@ +[default] +address = "0.0.0.0" +port = 8080 +workers = 2 diff --git a/bot/Cargo.toml b/bot/Cargo.toml new file mode 100644 index 0000000..70b30bb --- /dev/null +++ b/bot/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "race-of-sloths-bot" +version.workspace = true +edition.workspace = true +license.workspace = true +authors.workspace = true + +[dependencies] +anyhow.workspace = true +chrono.workspace = true +envy.workspace = true +octocrab.workspace = true +dotenv.workspace = true +serde = { workspace = true, features = ["derive"] } +tokio = { workspace = true, features = ["full"] } +tracing-subscriber.workspace = true +tracing.workspace = true +serde_json.workspace = true +futures.workspace = true +rand.workspace = true +toml.workspace = true + +shared = { workspace = true, features = ["client"] } diff --git a/server/src/api/github/mod.rs b/bot/src/api/mod.rs similarity index 98% rename from server/src/api/github/mod.rs rename to bot/src/api/mod.rs index 2919bd8..8c3502b 100644 --- a/server/src/api/github/mod.rs +++ b/bot/src/api/mod.rs @@ -6,8 +6,7 @@ use tracing::{error, info, instrument}; use crate::events::{commands::Command, Event, EventType}; -mod types; -pub use types::*; +pub use shared::github::*; #[derive(Clone, Debug)] pub struct GithubClient { @@ -69,7 +68,7 @@ impl GithubClient { } }; - let pr_metadata = match types::PrMetadata::try_from(pr) { + let pr_metadata = match PrMetadata::try_from(pr) { Ok(pr) => pr, Err(e) => { error!("Failed to convert PR: {:?}", e); diff --git a/server/src/events/actions/finalize.rs b/bot/src/events/actions/finalize.rs similarity index 88% rename from server/src/events/actions/finalize.rs rename to bot/src/events/actions/finalize.rs index 348c7cc..572f7b7 100644 --- a/server/src/events/actions/finalize.rs +++ b/bot/src/events/actions/finalize.rs @@ -1,10 +1,8 @@ use tracing::{instrument, warn}; -use crate::{ - api::{github::PrMetadata, near::PRInfo}, - events::Context, - messages::MsgCategory, -}; +use shared::{github::PrMetadata, PRInfo}; + +use crate::{events::Context, messages::MsgCategory}; #[derive(Debug, Clone)] pub struct PullRequestFinalize { diff --git a/server/src/events/actions/merge.rs b/bot/src/events/actions/merge.rs similarity index 92% rename from server/src/events/actions/merge.rs rename to bot/src/events/actions/merge.rs index 775e4c1..83bd042 100644 --- a/server/src/events/actions/merge.rs +++ b/bot/src/events/actions/merge.rs @@ -1,10 +1,8 @@ use tracing::instrument; -use crate::{ - api::{github::PrMetadata, near::PRInfo}, - events::Context, - messages::MsgCategory, -}; +use shared::{github::PrMetadata, PRInfo}; + +use crate::{events::Context, messages::MsgCategory}; #[derive(Debug, Clone)] pub struct PullRequestMerge { diff --git a/server/src/events/actions/mod.rs b/bot/src/events/actions/mod.rs similarity index 100% rename from server/src/events/actions/mod.rs rename to bot/src/events/actions/mod.rs diff --git a/server/src/events/actions/stale.rs b/bot/src/events/actions/stale.rs similarity index 89% rename from server/src/events/actions/stale.rs rename to bot/src/events/actions/stale.rs index 4209af7..74fa20b 100644 --- a/server/src/events/actions/stale.rs +++ b/bot/src/events/actions/stale.rs @@ -1,10 +1,8 @@ use tracing::{instrument, warn}; -use crate::{ - api::{github::PrMetadata, near::PRInfo}, - events::Context, - messages::MsgCategory, -}; +use shared::{github::PrMetadata, PRInfo}; + +use crate::{events::Context, messages::MsgCategory}; #[derive(Debug, Clone)] pub struct PullRequestStale { diff --git a/server/src/events/commands/exclude.rs b/bot/src/events/commands/exclude.rs similarity index 98% rename from server/src/events/commands/exclude.rs rename to bot/src/events/commands/exclude.rs index 663347e..a9da870 100644 --- a/server/src/events/commands/exclude.rs +++ b/bot/src/events/commands/exclude.rs @@ -2,7 +2,7 @@ use tracing::{debug, instrument}; use crate::messages::MsgCategory; -use self::api::github::User; +use shared::github::User; use super::*; diff --git a/server/src/events/commands/mod.rs b/bot/src/events/commands/mod.rs similarity index 100% rename from server/src/events/commands/mod.rs rename to bot/src/events/commands/mod.rs diff --git a/server/src/events/commands/pause.rs b/bot/src/events/commands/pause.rs similarity index 99% rename from server/src/events/commands/pause.rs rename to bot/src/events/commands/pause.rs index 1ceb6d4..02d2ae7 100644 --- a/server/src/events/commands/pause.rs +++ b/bot/src/events/commands/pause.rs @@ -2,7 +2,7 @@ use tracing::{debug, info, instrument}; use crate::messages::MsgCategory; -use self::api::github::User; +use shared::github::User; use super::*; diff --git a/server/src/events/commands/score.rs b/bot/src/events/commands/score.rs similarity index 98% rename from server/src/events/commands/score.rs rename to bot/src/events/commands/score.rs index 77e4d88..c9d43bf 100644 --- a/server/src/events/commands/score.rs +++ b/bot/src/events/commands/score.rs @@ -2,7 +2,7 @@ use tracing::{debug, instrument}; use crate::messages::MsgCategory; -use self::api::{github::User, near::PRInfo}; +use shared::{github::User, PRInfo}; use super::*; diff --git a/server/src/events/commands/start.rs b/bot/src/events/commands/start.rs similarity index 99% rename from server/src/events/commands/start.rs rename to bot/src/events/commands/start.rs index 80efb55..67036bc 100644 --- a/server/src/events/commands/start.rs +++ b/bot/src/events/commands/start.rs @@ -2,7 +2,7 @@ use tracing::{debug, instrument}; use crate::messages::MsgCategory; -use self::api::github::User; +use shared::github::User; use super::*; diff --git a/server/src/events/commands/unknown.rs b/bot/src/events/commands/unknown.rs similarity index 98% rename from server/src/events/commands/unknown.rs rename to bot/src/events/commands/unknown.rs index 8b10d83..e72d149 100644 --- a/server/src/events/commands/unknown.rs +++ b/bot/src/events/commands/unknown.rs @@ -2,7 +2,7 @@ use tracing::debug; use crate::messages::MsgCategory; -use self::api::github::User; +use shared::github::User; use super::*; diff --git a/server/src/events/common.rs b/bot/src/events/common.rs similarity index 98% rename from server/src/events/common.rs rename to bot/src/events/common.rs index 35970be..6d19bdf 100644 --- a/server/src/events/common.rs +++ b/bot/src/events/common.rs @@ -1,11 +1,9 @@ +use shared::PRInfo; use std::collections::HashMap; - use tracing::trace; use crate::messages::MsgCategory; -use self::api::near::PRInfo; - use super::*; impl Context { diff --git a/server/src/events/mod.rs b/bot/src/events/mod.rs similarity index 89% rename from server/src/events/mod.rs rename to bot/src/events/mod.rs index 435da5f..3f8e842 100644 --- a/server/src/events/mod.rs +++ b/bot/src/events/mod.rs @@ -3,10 +3,9 @@ use std::sync::Arc; use octocrab::models::{issues::Comment, CommentId, NotificationId}; use tracing::{info, instrument}; -use crate::{ - api::{self, github::PrMetadata, near::PRInfo}, - messages::MessageLoader, -}; +use crate::{api, messages::MessageLoader}; + +use shared::{github::PrMetadata, near::NearClient, PRInfo}; use self::{actions::Action, commands::Command}; @@ -16,8 +15,8 @@ pub(crate) mod common; #[derive(Clone, Debug)] pub struct Context { - pub github: Arc, - pub near: Arc, + pub github: Arc, + pub near: Arc, pub messages: Arc, } diff --git a/bot/src/lib.rs b/bot/src/lib.rs new file mode 100644 index 0000000..fbe0169 --- /dev/null +++ b/bot/src/lib.rs @@ -0,0 +1,3 @@ +pub mod api; +pub mod events; +pub mod messages; diff --git a/bot/src/main.rs b/bot/src/main.rs new file mode 100644 index 0000000..53fd334 --- /dev/null +++ b/bot/src/main.rs @@ -0,0 +1,295 @@ +use std::{collections::HashMap, path::PathBuf}; + +use futures::future::join_all; +use race_of_sloths_bot::{ + api::GithubClient, + events::{actions::Action, Context, Event, EventType}, + messages::MessageLoader, +}; +use serde::Deserialize; +use tokio::signal; +use tracing::{debug, error, info, instrument, trace}; +use tracing_subscriber::{layer::SubscriberExt, EnvFilter}; + +use shared::github::PrMetadata; +use shared::near::NearClient; + +#[derive(Deserialize)] +struct Env { + github_token: String, + contract: String, + secret_key: String, + is_mainnet: bool, + message_file: PathBuf, +} + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + dotenv::dotenv().ok(); + let subscriber = tracing_subscriber::registry() + .with(EnvFilter::from_default_env()) + .with(tracing_subscriber::fmt::layer()); + tracing::subscriber::set_global_default(subscriber)?; + + let env = envy::from_env::()?; + + let github_api = GithubClient::new(env.github_token).await?; + let messages = MessageLoader::load_from_file(&env.message_file, &github_api.user_handle)?; + let near_api = NearClient::new(env.contract, env.secret_key, env.is_mainnet).await?; + let context = Context { + github: github_api.into(), + near: near_api.into(), + messages: messages.into(), + }; + + tokio::select! { + _ = run(context) => { + error!("Main loop exited unexpectedly.") + } + _ = signal::ctrl_c() => { + info!("Received SIGINT. Exiting."); + } + } + + Ok(()) +} + +async fn run(context: Context) { + let minute = tokio::time::Duration::from_secs(60); + let mut interval: tokio::time::Interval = tokio::time::interval(minute); + let mut merge_time = std::time::SystemTime::now(); + let merge_interval = 60 * minute; + + loop { + let current_time = std::time::SystemTime::now(); + (_, _, merge_time) = tokio::join!( + interval.tick(), + event_task(context.clone()), + merge_and_execute_task(context.clone(), current_time, merge_time, merge_interval) + ) + } +} + +async fn event_task(context: Context) { + let events = match context.github.get_events().await { + Ok(events) => events, + Err(e) => { + error!("Failed to get events: {}", e); + return; + } + }; + + info!("Received {} events.", events.len()); + + let events_per_pr = events.into_iter().fold( + std::collections::HashMap::new(), + |mut map: HashMap>, event| { + let pr = event.event.pr(); + map.entry(pr.full_id.clone()).or_default().push(event); + map + }, + ); + + let futures = events_per_pr.into_iter().map(|(key, events)| { + debug!("Received {} events for PR {}", events.len(), key); + execute(context.clone(), events) + }); + + join_all(futures).await; +} + +async fn merge_and_execute_task( + context: Context, + current_time: std::time::SystemTime, + merge_time: std::time::SystemTime, + merge_interval: std::time::Duration, +) -> std::time::SystemTime { + if current_time < merge_time { + return merge_time; + } + + let events = match merge_events(&context).await { + Ok(events) => events, + Err(e) => { + error!("Failed to get merge events: {}", e); + return merge_time; + } + }; + + execute(context.clone(), events).await; + + // It matters to first execute the merge events and then finalize + // as the merge event is a requirement for the finalize event + let event = match finalized_events(&context).await { + Ok(events) => events, + Err(e) => { + error!("Failed to get finalize events: {}", e); + return merge_time; + } + }; + + execute(context.clone(), event).await; + + current_time + merge_interval +} + +// Runs events from the same PR +#[instrument(skip(context, events))] +async fn execute(context: Context, events: Vec) { + if events.is_empty() { + return; + } + + debug!("Executing {} events", events.len()); + let mut should_update = false; + for event in &events { + match event.execute(context.clone()).await { + Ok(res) => { + should_update |= res; + } + Err(e) => { + error!("Failed to execute event for {}: {e}", event.pr().full_id); + } + } + } + let event = &events[0]; + let pr = event.pr(); + + if !should_update { + debug!( + "No events that require updating status comment for {}", + pr.full_id + ); + return; + } + + if event.comment_id.is_none() { + debug!( + "No comment id for {}. Skipping status comment update", + pr.full_id + ); + return; + } + + debug!( + "Finished executing events. Updating status comment for {}", + pr.full_id + ); + let info = match context.check_info(pr).await { + Ok(info) => info, + Err(e) => { + error!("Failed to get PR info for {}: {e}", pr.full_id); + return; + } + }; + + if let Err(e) = context + .github + .edit_comment( + &pr.owner, + &pr.repo, + event.comment_id.unwrap().0, + &info.status_message(), + ) + .await + { + error!("Failed to update status comment for {}: {e}", pr.full_id); + } +} + +#[instrument(skip(context))] +async fn merge_events(context: &Context) -> anyhow::Result> { + let prs = context.near.unmerged_prs_all().await?; + info!("Received {} PRs for merge request check", prs.len()); + let mut results = vec![]; + + for pr in prs { + let pr = context + .github + .get_pull_request(&pr.organization, &pr.repo, pr.number) + .await; + let pr = match pr { + Ok(pr) => pr, + Err(e) => { + error!("Failed to get PR: {e}"); + continue; + } + }; + + let pr_metadata = match PrMetadata::try_from(pr) { + Ok(pr) => pr, + Err(e) => { + error!("Failed to convert PR: {e}"); + continue; + } + }; + let comment_id = context + .github + .get_comment_id(&pr_metadata.owner, &pr_metadata.repo, pr_metadata.number) + .await + .ok() + .flatten(); + + if pr_metadata.merged.is_none() { + trace!( + "PR {} is not merged. Checking for stale", + pr_metadata.full_id + ); + if check_for_stale_pr(&pr_metadata) { + info!("PR {} is stale. Creating an event", pr_metadata.full_id); + results.push(Event { + event: EventType::Action(Action::stale(pr_metadata)), + notification_id: None, + comment_id, + }); + } + continue; + } + trace!("PR {} is merged. Creating an event", pr_metadata.full_id); + if let Some(merged) = Action::merge(pr_metadata) { + results.push(Event { + event: EventType::Action(merged), + notification_id: None, + comment_id, + }); + } + } + info!("Finished merge task with {} events", results.len()); + Ok(results) +} + +#[instrument(skip(context))] +async fn finalized_events(context: &Context) -> anyhow::Result> { + let prs = context.near.unfinalized_prs_all().await?; + info!("Received {} PRs for merge request check", prs.len()); + + let comment_id_futures = prs.into_iter().map(|pr| async { + let comment_id = context + .github + .get_comment_id(&pr.organization, &pr.repo, pr.number) + .await + .ok() + .flatten(); + (pr, comment_id) + }); + + Ok(join_all(comment_id_futures) + .await + .into_iter() + .map(|(pr, comment_id)| Event { + event: EventType::Action(Action::finalize(pr.into())), + notification_id: None, + comment_id, + }) + .collect()) +} + +fn check_for_stale_pr(pr: &PrMetadata) -> bool { + if pr.merged.is_some() { + return false; + } + + let now = chrono::Utc::now(); + let stale = now - pr.updated_at; + stale.num_days() > 14 || pr.closed +} diff --git a/server/src/messages.rs b/bot/src/messages.rs similarity index 100% rename from server/src/messages.rs rename to bot/src/messages.rs diff --git a/contract/Cargo.toml b/contract/Cargo.toml index 050fed9..fc2063c 100644 --- a/contract/Cargo.toml +++ b/contract/Cargo.toml @@ -12,7 +12,7 @@ crate-type = ["cdylib"] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] near-sdk.workspace = true -shared-types.workspace = true +shared.workspace = true [dev-dependencies] near-sdk = { version = "5.0.0", features = ["unit-testing"] } diff --git a/contract/src/lib.rs b/contract/src/lib.rs index 12ff4d0..7bfb77b 100644 --- a/contract/src/lib.rs +++ b/contract/src/lib.rs @@ -7,7 +7,7 @@ use near_sdk::{ Timestamp, }; use near_sdk::{env, near_bindgen, AccountId, PanicOnDefault}; -use shared_types::{ +use shared::{ GithubHandle, IntoEnumIterator, PRId, Streak, StreakId, StreakType, StreakUserData, TimePeriod, TimePeriodString, UserPeriodData, VersionedPR, VersionedStreak, VersionedStreakUserData, VersionedUserPeriodData, PR, diff --git a/contract/src/tests.rs b/contract/src/tests.rs index 8c5d52e..0e4bb0e 100644 --- a/contract/src/tests.rs +++ b/contract/src/tests.rs @@ -1,5 +1,5 @@ use near_sdk::{test_utils::VMContextBuilder, testing_env, AccountId, VMContext}; -use shared_types::SCORE_TIMEOUT_IN_NANOSECONDS; +use shared::SCORE_TIMEOUT_IN_NANOSECONDS; use super::*; @@ -89,11 +89,14 @@ fn success_flow() { contract.finalize(0); assert_eq!(contract.contract.unfinalized_prs(0, 50).len(), 0); - let user = contract.contract.user(&github_handle(0), None).unwrap(); - assert_eq!(user.period_data.total_score, 10); - assert_eq!(user.period_data.executed_prs, 1); - assert_eq!(user.period_data.prs_opened, 1); - assert_eq!(user.period_data.prs_merged, 1); + let user = contract + .contract + .user(&github_handle(0), vec!["all-time".to_string()]) + .unwrap(); + assert_eq!(user.period_data[0].1.total_score, 10); + assert_eq!(user.period_data[0].1.executed_prs, 1); + assert_eq!(user.period_data[0].1.prs_opened, 1); + assert_eq!(user.period_data[0].1.prs_merged, 1); assert_eq!(user.streaks[0].1.amount, 1); assert_eq!(user.streaks[1].1.amount, 1); } @@ -115,14 +118,17 @@ fn streak_calculation() { contract.finalize(i); } - let user = contract.contract.user(&github_handle(0), None).unwrap(); + let user = contract + .contract + .user(&github_handle(0), vec!["all-time".to_string()]) + .unwrap(); // 12 weeks streak with opened PR assert_eq!(user.streaks[0].1.amount, 12); // 3 months streak with 8+ scopre assert_eq!(user.streaks[1].1.amount, 3); - assert_eq!(user.period_data.total_score, 12 * 10); - assert_eq!(user.period_data.executed_prs, 12); + assert_eq!(user.period_data[0].1.total_score, 12 * 10); + assert_eq!(user.period_data[0].1.executed_prs, 12); } #[test] @@ -130,12 +136,18 @@ fn streak_in_a_nutshell() { let mut contract = ContractExt::new(); contract.include_sloth_common_repo(0, 0, 0); - let user = contract.contract.user(&github_handle(0), None).unwrap(); + let user = contract + .contract + .user(&github_handle(0), vec!["all-time".to_string()]) + .unwrap(); assert_eq!(user.streaks[0].1.amount, 1); contract.merge(0, 10); - let user = contract.contract.user(&github_handle(0), None).unwrap(); + let user = contract + .contract + .user(&github_handle(0), vec!["all-time".to_string()]) + .unwrap(); assert_eq!(user.streaks[0].1.amount, 1); } @@ -144,20 +156,29 @@ fn user_had_a_streak_then_lost_then_again_get_it() { let mut contract = ContractExt::new(); contract.include_sloth_common_repo(0, 0, 0); - let user = contract.contract.user(&github_handle(0), None).unwrap(); + let user = contract + .contract + .user(&github_handle(0), vec!["all-time".to_string()]) + .unwrap(); assert_eq!(user.streaks[0].1.amount, 1); contract.exclude(0); - let user = contract.contract.user(&github_handle(0), None).unwrap(); + let user = contract + .contract + .user(&github_handle(0), vec!["all-time".to_string()]) + .unwrap(); assert_eq!(user.streaks[0].1.amount, 0); assert_eq!(user.streaks[0].1.best, 1); contract.include_sloth_common_repo(0, 1, 0); - let user = contract.contract.user(&github_handle(0), None).unwrap(); + let user = contract + .contract + .user(&github_handle(0), vec!["all-time".to_string()]) + .unwrap(); assert_eq!(user.streaks[0].1.amount, 1); } @@ -179,11 +200,14 @@ fn streak_crashed_in_middle() { contract.finalize(i); } - let user = contract.contract.user(&github_handle(0), None).unwrap(); + let user = contract + .contract + .user(&github_handle(0), vec!["all-time".to_string()]) + .unwrap(); assert_eq!(user.streaks[0].1.amount, 8); assert_eq!(user.streaks[1].1.amount, 2); - assert_eq!(user.period_data.total_score, 8 * 10); - assert_eq!(user.period_data.executed_prs, 8); + assert_eq!(user.period_data[0].1.total_score, 8 * 10); + assert_eq!(user.period_data[0].1.executed_prs, 8); // 5 weeks skipped to crush both streaks current_time += SCORE_TIMEOUT_IN_NANOSECONDS * 7 * 5 + 1; @@ -203,14 +227,17 @@ fn streak_crashed_in_middle() { contract.finalize(i); } - let user = contract.contract.user(&github_handle(0), None).unwrap(); + let user = contract + .contract + .user(&github_handle(0), vec!["all-time".to_string()]) + .unwrap(); assert_eq!(user.streaks[0].1.amount, 4); assert_eq!(user.streaks[0].1.best, 8); assert_eq!(user.streaks[1].1.amount, 1); assert_eq!(user.streaks[1].1.best, 2); - assert_eq!(user.period_data.total_score, 12 * 10); - assert_eq!(user.period_data.executed_prs, 12); + assert_eq!(user.period_data[0].1.total_score, 12 * 10); + assert_eq!(user.period_data[0].1.executed_prs, 12); } #[test] diff --git a/contract/src/views.rs b/contract/src/views.rs index 2a14500..7642d7c 100644 --- a/contract/src/views.rs +++ b/contract/src/views.rs @@ -1,5 +1,5 @@ use near_sdk::near_bindgen; -use shared_types::{PRInfo, User}; +use shared::{PRInfo, User}; use super::*; @@ -79,23 +79,27 @@ impl Contract { .map(Into::into) } - pub fn user(&self, user: &String, period_string: Option) -> Option { - let period = period_string - .unwrap_or_else(|| TimePeriod::AllTime.time_string(env::block_timestamp())); + pub fn user(&self, user: &String, periods: Vec) -> Option { self.accounts.get(user).map(|_| User { name: user.to_string(), - period_data: self.period_data(user, &period).unwrap_or_default(), + period_data: periods + .iter() + .map(|period| { + ( + period.clone(), + self.period_data(user, period).unwrap_or_default(), + ) + }) + .collect(), streaks: self.user_streaks(user), }) } - pub fn users(&self, limit: u64, page: u64, period_string: Option) -> Vec { - let period = period_string - .unwrap_or_else(|| TimePeriod::AllTime.time_string(env::block_timestamp())); + pub fn users(&self, limit: u64, page: u64, periods: Vec) -> Vec { self.accounts .iter() .skip((page * limit) as usize) - .filter_map(|(user, _data)| self.user(user, Some(period.clone()))) + .filter_map(|(user, _data)| self.user(user, periods.clone())) .collect() } } diff --git a/fly.toml b/fly.toml index 469426a..9b450ab 100644 --- a/fly.toml +++ b/fly.toml @@ -7,15 +7,25 @@ app = 'race-of-sloths' primary_region = 'ams' [env] -RUST_LOG = "race_of_sloths_bot=TRACE" +RUST_LOG = "race_of_sloths_bot=TRACE,race-of-sloths-server=TRACE" -[build] -[http_service] +[processes] +bot = "./race-of-sloths-bot" +server = "./race-of-sloths-server" + +[[services]] internal_port = 8080 +processes = ["server"] protocol = "tcp" -auto_start_machines = true -auto_stop_machines = false -min_machines_running = 1 + +[[services.ports]] +handlers = ["http"] +port = "80" +force_https = true + +[[services.ports]] +handlers = ["tls", "http"] +port = 443 [[vm]] memory = '256mb' diff --git a/server/.sqlx/query-2086820ddd9a4927c28862da5bba1f322f658918f45b019f9db8b31a8bb05294.json b/server/.sqlx/query-2086820ddd9a4927c28862da5bba1f322f658918f45b019f9db8b31a8bb05294.json new file mode 100644 index 0000000..f726b55 --- /dev/null +++ b/server/.sqlx/query-2086820ddd9a4927c28862da5bba1f322f658918f45b019f9db8b31a8bb05294.json @@ -0,0 +1,20 @@ +{ + "db_name": "PostgreSQL", + "query": "\n INSERT INTO user_period_data (user_id, period_type, total_score, executed_prs, largest_score, prs_opened, prs_merged)\n VALUES ($1, $2, $3, $4, $5, $6, $7)\n ON CONFLICT (user_id, period_type) DO UPDATE\n SET total_score = EXCLUDED.total_score,\n executed_prs = EXCLUDED.executed_prs,\n largest_score = EXCLUDED.largest_score,\n prs_opened = EXCLUDED.prs_opened,\n prs_merged = EXCLUDED.prs_merged\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Int4", + "Text", + "Int4", + "Int4", + "Int4", + "Int4", + "Int4" + ] + }, + "nullable": [] + }, + "hash": "2086820ddd9a4927c28862da5bba1f322f658918f45b019f9db8b31a8bb05294" +} diff --git a/server/.sqlx/query-812d0f5f99dacbaf032615bbfd971b39793fa2b27cedc2b4b8c34624515fda01.json b/server/.sqlx/query-812d0f5f99dacbaf032615bbfd971b39793fa2b27cedc2b4b8c34624515fda01.json new file mode 100644 index 0000000..c480e25 --- /dev/null +++ b/server/.sqlx/query-812d0f5f99dacbaf032615bbfd971b39793fa2b27cedc2b4b8c34624515fda01.json @@ -0,0 +1,52 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT period_type, total_score, executed_prs, largest_score, prs_opened, prs_merged\n FROM user_period_data \n WHERE user_id = $1\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "period_type", + "type_info": "Text" + }, + { + "ordinal": 1, + "name": "total_score", + "type_info": "Int4" + }, + { + "ordinal": 2, + "name": "executed_prs", + "type_info": "Int4" + }, + { + "ordinal": 3, + "name": "largest_score", + "type_info": "Int4" + }, + { + "ordinal": 4, + "name": "prs_opened", + "type_info": "Int4" + }, + { + "ordinal": 5, + "name": "prs_merged", + "type_info": "Int4" + } + ], + "parameters": { + "Left": [ + "Int4" + ] + }, + "nullable": [ + false, + false, + false, + false, + false, + false + ] + }, + "hash": "812d0f5f99dacbaf032615bbfd971b39793fa2b27cedc2b4b8c34624515fda01" +} diff --git a/server/.sqlx/query-aa83fc267311ea2cd091a549ddec3798236baf1d40e20d90a6146d24285cfa49.json b/server/.sqlx/query-aa83fc267311ea2cd091a549ddec3798236baf1d40e20d90a6146d24285cfa49.json new file mode 100644 index 0000000..2949c47 --- /dev/null +++ b/server/.sqlx/query-aa83fc267311ea2cd091a549ddec3798236baf1d40e20d90a6146d24285cfa49.json @@ -0,0 +1,22 @@ +{ + "db_name": "PostgreSQL", + "query": "\n INSERT INTO users (name)\n VALUES ($1)\n ON CONFLICT (name) DO UPDATE\n SET name = EXCLUDED.name\n RETURNING id\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Int4" + } + ], + "parameters": { + "Left": [ + "Text" + ] + }, + "nullable": [ + false + ] + }, + "hash": "aa83fc267311ea2cd091a549ddec3798236baf1d40e20d90a6146d24285cfa49" +} diff --git a/server/.sqlx/query-c948b8c82bac8f0787c05d102c5e2119d8ec12683ef55fb70c9e0f1100560b8c.json b/server/.sqlx/query-c948b8c82bac8f0787c05d102c5e2119d8ec12683ef55fb70c9e0f1100560b8c.json new file mode 100644 index 0000000..d4e884d --- /dev/null +++ b/server/.sqlx/query-c948b8c82bac8f0787c05d102c5e2119d8ec12683ef55fb70c9e0f1100560b8c.json @@ -0,0 +1,40 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT streak_id, amount, best, latest_time_string\n FROM streak_user_data\n WHERE user_id = $1\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "streak_id", + "type_info": "Int4" + }, + { + "ordinal": 1, + "name": "amount", + "type_info": "Int4" + }, + { + "ordinal": 2, + "name": "best", + "type_info": "Int4" + }, + { + "ordinal": 3, + "name": "latest_time_string", + "type_info": "Text" + } + ], + "parameters": { + "Left": [ + "Int4" + ] + }, + "nullable": [ + false, + false, + false, + false + ] + }, + "hash": "c948b8c82bac8f0787c05d102c5e2119d8ec12683ef55fb70c9e0f1100560b8c" +} diff --git a/server/.sqlx/query-ce92e1e0312984f8af5eb4d51e1101beb8e53b613de892c45199b3ad2626776a.json b/server/.sqlx/query-ce92e1e0312984f8af5eb4d51e1101beb8e53b613de892c45199b3ad2626776a.json new file mode 100644 index 0000000..682a613 --- /dev/null +++ b/server/.sqlx/query-ce92e1e0312984f8af5eb4d51e1101beb8e53b613de892c45199b3ad2626776a.json @@ -0,0 +1,18 @@ +{ + "db_name": "PostgreSQL", + "query": "\n INSERT INTO streak_user_data (user_id, streak_id, amount, best, latest_time_string)\n VALUES ($1, $2, $3, $4, $5)\n ON CONFLICT (user_id, streak_id) DO UPDATE\n SET amount = EXCLUDED.amount,\n best = EXCLUDED.best,\n latest_time_string = EXCLUDED.latest_time_string\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Int4", + "Int4", + "Int4", + "Int4", + "Text" + ] + }, + "nullable": [] + }, + "hash": "ce92e1e0312984f8af5eb4d51e1101beb8e53b613de892c45199b3ad2626776a" +} diff --git a/server/.sqlx/query-cfc7f3b9f05924401e41cd5356ba2dbcf67fb6e123b7680e0bd3f884bf5615e2.json b/server/.sqlx/query-cfc7f3b9f05924401e41cd5356ba2dbcf67fb6e123b7680e0bd3f884bf5615e2.json new file mode 100644 index 0000000..b783b57 --- /dev/null +++ b/server/.sqlx/query-cfc7f3b9f05924401e41cd5356ba2dbcf67fb6e123b7680e0bd3f884bf5615e2.json @@ -0,0 +1,28 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT id, name FROM users WHERE name = $1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Int4" + }, + { + "ordinal": 1, + "name": "name", + "type_info": "Text" + } + ], + "parameters": { + "Left": [ + "Text" + ] + }, + "nullable": [ + false, + false + ] + }, + "hash": "cfc7f3b9f05924401e41cd5356ba2dbcf67fb6e123b7680e0bd3f884bf5615e2" +} diff --git a/server/.sqlx/query-e73a787ff2949ebe6032c05675c98b4405c18e25eeeb3c0d6236671dbdc060e3.json b/server/.sqlx/query-e73a787ff2949ebe6032c05675c98b4405c18e25eeeb3c0d6236671dbdc060e3.json new file mode 100644 index 0000000..56f12ef --- /dev/null +++ b/server/.sqlx/query-e73a787ff2949ebe6032c05675c98b4405c18e25eeeb3c0d6236671dbdc060e3.json @@ -0,0 +1,60 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT users.name, period_type, total_score, executed_prs, largest_score, prs_opened, prs_merged\n FROM user_period_data \n JOIN users ON users.id = user_period_data.user_id\n WHERE period_type = $1\n ORDER BY total_score DESC\n LIMIT $2 OFFSET $3\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "name", + "type_info": "Text" + }, + { + "ordinal": 1, + "name": "period_type", + "type_info": "Text" + }, + { + "ordinal": 2, + "name": "total_score", + "type_info": "Int4" + }, + { + "ordinal": 3, + "name": "executed_prs", + "type_info": "Int4" + }, + { + "ordinal": 4, + "name": "largest_score", + "type_info": "Int4" + }, + { + "ordinal": 5, + "name": "prs_opened", + "type_info": "Int4" + }, + { + "ordinal": 6, + "name": "prs_merged", + "type_info": "Int4" + } + ], + "parameters": { + "Left": [ + "Text", + "Int8", + "Int8" + ] + }, + "nullable": [ + false, + false, + false, + false, + false, + false, + false + ] + }, + "hash": "e73a787ff2949ebe6032c05675c98b4405c18e25eeeb3c0d6236671dbdc060e3" +} diff --git a/server/Cargo.toml b/server/Cargo.toml index c5ca032..82c795d 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -1,25 +1,20 @@ [package] -name = "race-of-sloths-bot" +name = "race-of-sloths-server" version.workspace = true edition.workspace = true -license.workspace = true authors.workspace = true +license.workspace = true [dependencies] anyhow.workspace = true -chrono.workspace = true -envy.workspace = true -hex.workspace = true -octocrab.workspace = true dotenv.workspace = true -serde = { workspace = true, features = ["derive"] } -tokio = { workspace = true, features = ["full"] } -tracing-subscriber.workspace = true +envy.workspace = true +rocket = { workspace = true, features = ["json"] } +rocket_db_pools = { workspace = true, features = ["sqlx_postgres"] } +svg.workspace = true +sqlx = { workspace = true, features = ["postgres", "macros"] } tracing.workspace = true -async-trait.workspace = true -near-workspaces.workspace = true -serde_json.workspace = true -futures.workspace = true -shared-types.workspace = true -rand.workspace = true -toml.workspace = true +tracing-subscriber.workspace = true +serde.workspace = true + +shared = { workspace = true, features = ["client"] } diff --git a/server/migrations/20240526132957_create-table.sql b/server/migrations/20240526132957_create-table.sql new file mode 100644 index 0000000..96b1823 --- /dev/null +++ b/server/migrations/20240526132957_create-table.sql @@ -0,0 +1,31 @@ +CREATE TABLE IF NOT EXISTS users ( + id SERIAL PRIMARY KEY, + name TEXT UNIQUE NOT NULL +); + +CREATE TABLE IF NOT EXISTS user_period_data ( + user_id INTEGER REFERENCES users(id) ON DELETE CASCADE, + period_type TEXT NOT NULL, + total_score INTEGER NOT NULL, + executed_prs INTEGER NOT NULL, + largest_score INTEGER NOT NULL, + prs_opened INTEGER NOT NULL, + prs_merged INTEGER NOT NULL, + PRIMARY KEY (user_id, period_type) +); + +CREATE TABLE IF NOT EXISTS streak_user_data ( + user_id INTEGER REFERENCES users(id) ON DELETE CASCADE, + streak_id INTEGER NOT NULL, + amount INTEGER NOT NULL, + best INTEGER NOT NULL, + latest_time_string TEXT NOT NULL, + PRIMARY KEY (user_id, streak_id) +); + +-- Indexes for leaderboard queries +CREATE INDEX IF NOT EXISTS idx_user_period_data_total_score ON user_period_data (total_score); + +CREATE INDEX IF NOT EXISTS idx_user_period_data_prs_opened ON user_period_data (prs_opened); + +CREATE INDEX IF NOT EXISTS idx_user_period_data_prs_merged ON user_period_data (prs_merged); diff --git a/server/src/api/mod.rs b/server/src/api/mod.rs deleted file mode 100644 index bdb0a33..0000000 --- a/server/src/api/mod.rs +++ /dev/null @@ -1,2 +0,0 @@ -pub mod github; -pub mod near; diff --git a/server/src/db/mod.rs b/server/src/db/mod.rs new file mode 100644 index 0000000..7d8b3ca --- /dev/null +++ b/server/src/db/mod.rs @@ -0,0 +1,171 @@ +use rocket::{ + fairing::{self, AdHoc}, + Build, Rocket, +}; +use rocket_db_pools::Database; +use shared::{StreakUserData, TimePeriodString, User, UserPeriodData}; +use sqlx::PgPool; +use tracing::instrument; + +#[derive(Database, Clone, Debug)] +#[database("race-of-sloths")] +pub struct DB(PgPool); + +pub mod types; + +use types::LeaderboardRecord; + +use self::types::{StreakRecord, UserPeriodRecord, UserRecord}; + +impl DB { + #[instrument(skip(self))] + pub async fn upsert_user(&self, user: &User) -> anyhow::Result { + let rec = sqlx::query!( + r#" + INSERT INTO users (name) + VALUES ($1) + ON CONFLICT (name) DO UPDATE + SET name = EXCLUDED.name + RETURNING id + "#, + user.name + ) + .fetch_one(&self.0) + .await?; + + Ok(rec.id) + } + + #[instrument(skip(self))] + pub async fn upsert_user_period_data( + &self, + period: TimePeriodString, + data: &UserPeriodData, + user_id: i32, + ) -> anyhow::Result<()> { + sqlx::query!( + r#" + INSERT INTO user_period_data (user_id, period_type, total_score, executed_prs, largest_score, prs_opened, prs_merged) + VALUES ($1, $2, $3, $4, $5, $6, $7) + ON CONFLICT (user_id, period_type) DO UPDATE + SET total_score = EXCLUDED.total_score, + executed_prs = EXCLUDED.executed_prs, + largest_score = EXCLUDED.largest_score, + prs_opened = EXCLUDED.prs_opened, + prs_merged = EXCLUDED.prs_merged + "#, + user_id, period, data.total_score as i32, data.executed_prs as i32, data.largest_score as i32, data.prs_opened as i32, data.prs_merged as i32 + ) + .execute(&self.0) + .await?; + Ok(()) + } + + #[instrument(skip(self))] + pub async fn upsert_streak_user_data( + &self, + data: &StreakUserData, + streak_id: i32, + user_id: i32, + ) -> anyhow::Result<()> { + sqlx::query!( + r#" + INSERT INTO streak_user_data (user_id, streak_id, amount, best, latest_time_string) + VALUES ($1, $2, $3, $4, $5) + ON CONFLICT (user_id, streak_id) DO UPDATE + SET amount = EXCLUDED.amount, + best = EXCLUDED.best, + latest_time_string = EXCLUDED.latest_time_string + "#, + user_id, + streak_id, + data.amount as i32, + data.best as i32, + data.latest_time_string + ) + .execute(&self.0) + .await?; + Ok(()) + } + + #[instrument(skip(self))] + pub async fn get_user(&self, name: &str) -> anyhow::Result> { + let user_rec: i32 = match sqlx::query!("SELECT id, name FROM users WHERE name = $1", name) + .fetch_optional(&self.0) + .await? + { + Some(rec) => rec.id, + None => return Ok(None), + }; + + let period_data_recs: Vec = sqlx::query_as!( + UserPeriodRecord, + r#" + SELECT period_type, total_score, executed_prs, largest_score, prs_opened, prs_merged + FROM user_period_data + WHERE user_id = $1 + "#, + user_rec, + ) + .fetch_all(&self.0) + .await?; + + let streak_recs: Vec = sqlx::query_as!( + StreakRecord, + r#" + SELECT streak_id, amount, best, latest_time_string + FROM streak_user_data + WHERE user_id = $1 + "#, + user_rec + ) + .fetch_all(&self.0) + .await?; + + let user = UserRecord { + name: name.to_string(), + period_data: period_data_recs, + streaks: streak_recs, + }; + + Ok(Some(user)) + } + + #[instrument(skip(self))] + pub async fn get_leaderboard( + &self, + period: &str, + page: i64, + limit: i64, + ) -> anyhow::Result> { + Ok(sqlx::query_as!(LeaderboardRecord,r#" + SELECT users.name, period_type, total_score, executed_prs, largest_score, prs_opened, prs_merged + FROM user_period_data + JOIN users ON users.id = user_period_data.user_id + WHERE period_type = $1 + ORDER BY total_score DESC + LIMIT $2 OFFSET $3 + "#,period,limit,page*limit).fetch_all(&self.0,).await? ) + } +} + +async fn run_migrations(rocket: Rocket) -> fairing::Result { + match DB::fetch(&rocket) { + Some(db) => match sqlx::migrate!("./migrations").run(&**db).await { + Ok(_) => Ok(rocket), + Err(e) => { + tracing::error!("Failed to initialize SQLx database: {}", e); + Err(rocket) + } + }, + None => Err(rocket), + } +} + +pub fn stage() -> AdHoc { + AdHoc::on_ignite("SQLx Stage", |rocket| async { + rocket + .attach(DB::init()) + .attach(AdHoc::try_on_ignite("SQLx Migrations", run_migrations)) + }) +} diff --git a/server/src/db/types.rs b/server/src/db/types.rs new file mode 100644 index 0000000..7f76fe4 --- /dev/null +++ b/server/src/db/types.rs @@ -0,0 +1,38 @@ +use serde::{Deserialize, Serialize}; +use shared::TimePeriodString; + +#[derive(Debug, Clone, sqlx::FromRow, Serialize, Deserialize)] +pub struct LeaderboardRecord { + pub name: String, + pub total_score: i32, + pub period_type: TimePeriodString, + pub executed_prs: i32, + pub largest_score: i32, + pub prs_opened: i32, + pub prs_merged: i32, +} + +#[derive(Debug, Clone, sqlx::FromRow, Serialize, Deserialize)] +pub struct UserPeriodRecord { + pub period_type: TimePeriodString, + pub total_score: i32, + pub executed_prs: i32, + pub largest_score: i32, + pub prs_opened: i32, + pub prs_merged: i32, +} + +#[derive(Debug, Clone, sqlx::FromRow, Serialize, Deserialize)] +pub struct StreakRecord { + pub streak_id: i32, + pub amount: i32, + pub best: i32, + pub latest_time_string: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct UserRecord { + pub name: String, + pub period_data: Vec, + pub streaks: Vec, +} diff --git a/server/src/entrypoints.rs b/server/src/entrypoints.rs new file mode 100644 index 0000000..937e7f3 --- /dev/null +++ b/server/src/entrypoints.rs @@ -0,0 +1,69 @@ +use race_of_sloths_server::{ + db::{ + types::{LeaderboardRecord, UserRecord}, + DB, + }, + generate_svg, +}; +use rocket::{fairing::AdHoc, http::ContentType, response::content::RawHtml, serde::json::Json}; +use tracing::instrument; + +#[get("/badges/")] +#[instrument] +async fn get_svg(username: &str, db: &DB) -> Option<(ContentType, RawHtml)> { + let user = match db.get_user(username).await { + Err(e) => { + error!("Failed to get user: {username}: {e}"); + return None; + } + Ok(value) => value?, + }; + let period_data = user + .period_data + .iter() + .find(|p| p.period_type == "all-time")?; + let streak = user.streaks.iter().max_by(|a, b| a.amount.cmp(&b.amount))?; + let svg_content = generate_svg( + &user.name, + streak.amount as u32, + period_data.total_score as u32, + ); + Some((ContentType::SVG, RawHtml(svg_content))) +} + +#[get("/users/")] +async fn get_user(username: &str, db: &DB) -> Option> { + let user = match db.get_user(username).await { + Err(e) => { + error!("Failed to get user: {username}: {e}"); + return None; + } + Ok(value) => value?, + }; + Some(Json(user)) +} + +#[get("/leaderboard/?&")] +async fn get_leaderboard( + period: &str, + db: &DB, + page: Option, + limit: Option, +) -> Option>> { + let page = page.unwrap_or(0); + let limit = limit.unwrap_or(50); + let users = match db.get_leaderboard(period, page as i64, limit as i64).await { + Err(e) => { + error!("Failed to get leaderboard: {period}: {e}"); + return None; + } + Ok(value) => value, + }; + Some(Json(users)) +} + +pub fn stage() -> AdHoc { + AdHoc::on_ignite("Installing entrypoints", |rocket| async { + rocket.mount("/api", routes![get_svg, get_user, get_leaderboard]) + }) +} diff --git a/server/src/lib.rs b/server/src/lib.rs index fbe0169..ec6c702 100644 --- a/server/src/lib.rs +++ b/server/src/lib.rs @@ -1,3 +1,45 @@ -pub mod api; -pub mod events; -pub mod messages; +use svg::node::element::{Rectangle, Text}; +use svg::Document; + +pub mod db; + +pub fn generate_svg(contributor_name: &str, streak_count: u32, total_points: u32) -> String { + let document = Document::new() + .set("viewBox", (0, 0, 200, 100)) + .add( + Rectangle::new() + .set("x", 0) + .set("y", 0) + .set("width", 200) + .set("height", 100) + .set("fill", "white") + .set("stroke", "black") + .set("stroke-width", 2), + ) + .add( + Text::new(format!("Name: {}", contributor_name)) + .set("x", 10) + .set("y", 30) + .set("font-family", "Arial") + .set("font-size", 20) + .set("fill", "black"), + ) + .add( + Text::new(format!("Streak: {}", streak_count)) + .set("x", 10) + .set("y", 60) + .set("font-family", "Arial") + .set("font-size", 20) + .set("fill", "black"), + ) + .add( + Text::new(format!("Points: {}", total_points)) + .set("x", 10) + .set("y", 90) + .set("font-family", "Arial") + .set("font-size", 20) + .set("fill", "black"), + ); + + document.to_string() +} diff --git a/server/src/main.rs b/server/src/main.rs index 59900b2..60b78f0 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -1,301 +1,109 @@ -use std::{collections::HashMap, path::PathBuf, str::FromStr}; +#[macro_use] +extern crate rocket; -use futures::future::join_all; -use near_workspaces::types::SecretKey; -use race_of_sloths_bot::{ - api::{ - github::{GithubClient, PrMetadata}, - near::NearClient, - }, - events::{actions::Action, Context, Event, EventType}, - messages::MessageLoader, -}; -use serde::Deserialize; -use tokio::signal; -use tracing::{debug, error, info, instrument, trace}; -use tracing_subscriber::{layer::SubscriberExt, EnvFilter}; +mod entrypoints; -#[derive(Deserialize)] -struct Env { - github_token: String, +use std::sync::Arc; +use std::time::Duration; + +use rocket_db_pools::Database; +use shared::near::NearClient; +use shared::TimePeriod; +use tracing::instrument; +use tracing_subscriber::layer::SubscriberExt; +use tracing_subscriber::EnvFilter; + +use race_of_sloths_server::db::{self, DB}; + +#[derive(Debug, serde::Deserialize)] +pub struct Env { contract: String, secret_key: String, is_mainnet: bool, - message_file: PathBuf, + sleep_duration_in_minutes: Option, } -#[tokio::main] -async fn main() -> anyhow::Result<()> { +#[launch] +async fn rocket() -> _ { dotenv::dotenv().ok(); + let subscriber = tracing_subscriber::registry() .with(EnvFilter::from_default_env()) - .with(tracing_subscriber::fmt::layer()); - tracing::subscriber::set_global_default(subscriber)?; - - let env = envy::from_env::()?; - - let github_api = GithubClient::new(env.github_token).await?; - let messages = MessageLoader::load_from_file(&env.message_file, &github_api.user_handle)?; - let near_api = NearClient::new( - env.contract, - SecretKey::from_str(&env.secret_key)?, - env.is_mainnet, - ) - .await?; - let context = Context { - github: github_api.into(), - near: near_api.into(), - messages: messages.into(), - }; - - tokio::select! { - _ = run(context) => { - error!("Main loop exited unexpectedly.") - } - _ = signal::ctrl_c() => { - info!("Received SIGINT. Exiting."); - } - } - - Ok(()) -} - -async fn run(context: Context) { - let minute = tokio::time::Duration::from_secs(60); - let mut interval: tokio::time::Interval = tokio::time::interval(minute); - let mut merge_time = std::time::SystemTime::now(); - let merge_interval = 60 * minute; - - loop { - let current_time = std::time::SystemTime::now(); - (_, _, merge_time) = tokio::join!( - interval.tick(), - event_task(context.clone()), - merge_and_execute_task(context.clone(), current_time, merge_time, merge_interval) - ) - } -} - -async fn event_task(context: Context) { - let events = match context.github.get_events().await { - Ok(events) => events, - Err(e) => { - error!("Failed to get events: {}", e); - return; - } - }; - - info!("Received {} events.", events.len()); - - let events_per_pr = events.into_iter().fold( - std::collections::HashMap::new(), - |mut map: HashMap>, event| { - let pr = event.event.pr(); - map.entry(pr.full_id.clone()).or_default().push(event); - map - }, - ); - - let futures = events_per_pr.into_iter().map(|(key, events)| { - debug!("Received {} events for PR {}", events.len(), key); - execute(context.clone(), events) - }); - - join_all(futures).await; -} - -async fn merge_and_execute_task( - context: Context, - current_time: std::time::SystemTime, - merge_time: std::time::SystemTime, - merge_interval: std::time::Duration, -) -> std::time::SystemTime { - if current_time < merge_time { - return merge_time; - } - - let events = match merge_events(&context).await { - Ok(events) => events, - Err(e) => { - error!("Failed to get merge events: {}", e); - return merge_time; - } - }; - - execute(context.clone(), events).await; - - // It matters to first execute the merge events and then finalize - // as the merge event is a requirement for the finalize event - let event = match finalized_events(&context).await { - Ok(events) => events, - Err(e) => { - error!("Failed to get finalize events: {}", e); - return merge_time; - } - }; - - execute(context.clone(), event).await; - - current_time + merge_interval -} - -// Runs events from the same PR -#[instrument(skip(context, events))] -async fn execute(context: Context, events: Vec) { - if events.is_empty() { - return; - } - - debug!("Executing {} events", events.len()); - let mut should_update = false; - for event in &events { - match event.execute(context.clone()).await { - Ok(res) => { - should_update |= res; - } - Err(e) => { - error!("Failed to execute event for {}: {e}", event.pr().full_id); - } - } - } - let event = &events[0]; - let pr = event.pr(); - - if !should_update { - debug!( - "No events that require updating status comment for {}", - pr.full_id - ); - return; - } - - if event.comment_id.is_none() { - debug!( - "No comment id for {}. Skipping status comment update", - pr.full_id - ); - return; - } - - debug!( - "Finished executing events. Updating status comment for {}", - pr.full_id - ); - let info = match context.check_info(pr).await { - Ok(info) => info, - Err(e) => { - error!("Failed to get PR info for {}: {e}", pr.full_id); - return; - } - }; - - if let Err(e) = context - .github - .edit_comment( - &pr.owner, - &pr.repo, - event.comment_id.unwrap().0, - &info.status_message(), - ) - .await - { - error!("Failed to update status comment for {}: {e}", pr.full_id); - } + .with(tracing_subscriber::fmt::layer().pretty()); + tracing::subscriber::set_global_default(subscriber).expect("Failed to set subscriber"); + + let env = envy::from_env::().expect("Failed to load environment variables"); + let sleep_duration = + Duration::from_secs(env.sleep_duration_in_minutes.unwrap_or(10) as u64 * 60); + let atomic_bool = Arc::new(std::sync::atomic::AtomicBool::new(true)); + let atomic_bool_clone = atomic_bool.clone(); + + let span = tracing::info_span!("Starting Rocket"); + let _enter = span.enter(); + + rocket::build() + .attach(db::stage()) + .attach(rocket::fairing::AdHoc::on_liftoff( + "Load users from Near every X minutes", + move |rocket| { + Box::pin(async move { + // Get an actual DB connection + let db = DB::fetch(rocket) + .expect("Failed to get DB connection") + .clone(); + + rocket::tokio::spawn(async move { + let mut interval = rocket::tokio::time::interval(sleep_duration); + let near_client = NearClient::new( + env.contract.clone(), + env.secret_key.clone(), + env.is_mainnet, + ) + .await + .expect("Failed to create Near client"); + while atomic_bool.load(std::sync::atomic::Ordering::Relaxed) { + interval.tick().await; + + // Execute a query of some kind + if let Err(e) = fetch_and_store_users(&near_client, &db).await { + tracing::error!("Failed to fetch and store users: {:#?}", e); + } + } + }); + }) + }, + )) + .attach(rocket::fairing::AdHoc::on_shutdown( + "Stop loading users from Near", + |_| { + Box::pin(async move { + atomic_bool_clone.store(false, std::sync::atomic::Ordering::Relaxed); + }) + }, + )) + .attach(entrypoints::stage()) } -#[instrument(skip(context))] -async fn merge_events(context: &Context) -> anyhow::Result> { - let prs = context.near.unmerged_prs_all().await?; - info!("Received {} PRs for merge request check", prs.len()); - let mut results = vec![]; - - for pr in prs { - let pr = context - .github - .get_pull_request(&pr.organization, &pr.repo, pr.number) - .await; - let pr = match pr { - Ok(pr) => pr, - Err(e) => { - error!("Failed to get PR: {e}"); - continue; - } - }; - - let pr_metadata = match PrMetadata::try_from(pr) { - Ok(pr) => pr, - Err(e) => { - error!("Failed to convert PR: {e}"); - continue; - } - }; - let comment_id = context - .github - .get_comment_id(&pr_metadata.owner, &pr_metadata.repo, pr_metadata.number) - .await - .ok() - .flatten(); - - if pr_metadata.merged.is_none() { - trace!( - "PR {} is not merged. Checking for stale", - pr_metadata.full_id - ); - if check_for_stale_pr(&pr_metadata) { - info!("PR {} is stale. Creating an event", pr_metadata.full_id); - results.push(Event { - event: EventType::Action(Action::stale(pr_metadata)), - notification_id: None, - comment_id, - }); - } - continue; +#[instrument(skip(near_client, db))] +async fn fetch_and_store_users(near_client: &NearClient, db: &DB) -> anyhow::Result<()> { + let timestamp = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH)? + .as_nanos(); + let periods = [TimePeriod::Month, TimePeriod::Quarter, TimePeriod::AllTime] + .into_iter() + .map(|e| e.time_string(timestamp as u64)) + .collect(); + let users = near_client.users(periods).await?; + for user in users { + let user_id = db.upsert_user(&user).await?; + for (period, data) in user.period_data { + db.upsert_user_period_data(period, &data, user_id).await?; } - trace!("PR {} is merged. Creating an event", pr_metadata.full_id); - if let Some(merged) = Action::merge(pr_metadata) { - results.push(Event { - event: EventType::Action(merged), - notification_id: None, - comment_id, - }); + for (streak_id, streak_data) in user.streaks { + db.upsert_streak_user_data(&streak_data, streak_id as i32, user_id) + .await?; } } - info!("Finished merge task with {} events", results.len()); - Ok(results) -} - -#[instrument(skip(context))] -async fn finalized_events(context: &Context) -> anyhow::Result> { - let prs = context.near.unfinalized_prs_all().await?; - info!("Received {} PRs for merge request check", prs.len()); - let comment_id_futures = prs.into_iter().map(|pr| async { - let comment_id = context - .github - .get_comment_id(&pr.organization, &pr.repo, pr.number) - .await - .ok() - .flatten(); - (pr, comment_id) - }); - - Ok(join_all(comment_id_futures) - .await - .into_iter() - .map(|(pr, comment_id)| Event { - event: EventType::Action(Action::finalize(pr.into())), - notification_id: None, - comment_id, - }) - .collect()) -} - -fn check_for_stale_pr(pr: &PrMetadata) -> bool { - if pr.merged.is_some() { - return false; - } - - let now = chrono::Utc::now(); - let stale = now - pr.updated_at; - stale.num_days() > 14 || pr.closed + Ok(()) } diff --git a/shared-types/Cargo.toml b/shared-types/Cargo.toml deleted file mode 100644 index 1bc1668..0000000 --- a/shared-types/Cargo.toml +++ /dev/null @@ -1,12 +0,0 @@ -[package] -name = "shared-types" -version.workspace = true -edition.workspace = true -authors.workspace = true -license.workspace = true - - -[dependencies] -near-sdk.workspace = true -strum = { workspace = true, features = ["derive"] } -chrono.workspace = true diff --git a/shared/Cargo.toml b/shared/Cargo.toml new file mode 100644 index 0000000..b4ec6de --- /dev/null +++ b/shared/Cargo.toml @@ -0,0 +1,29 @@ +[package] +name = "shared" +version.workspace = true +edition.workspace = true +authors.workspace = true +license.workspace = true + + +[dependencies] +near-sdk.workspace = true +strum = { workspace = true, features = ["derive"] } +chrono.workspace = true + +near-workspaces = { workspace = true, optional = true } +anyhow = { workspace = true, optional = true } +serde_json = { workspace = true, optional = true } +tracing = { workspace = true, optional = true } + +octocrab = { workspace = true, optional = true } + +[features] +github = ["dep:octocrab"] +client = [ + "dep:near-workspaces", + "dep:anyhow", + "dep:serde_json", + "dep:tracing", + "github", +] diff --git a/server/src/api/github/types.rs b/shared/src/github.rs similarity index 99% rename from server/src/api/github/types.rs rename to shared/src/github.rs index 30d0acb..9fd8628 100644 --- a/server/src/api/github/types.rs +++ b/shared/src/github.rs @@ -1,5 +1,5 @@ +use crate::PR; use octocrab::models::AuthorAssociation; -use shared_types::PR; #[derive(Debug, Clone)] pub struct User { diff --git a/shared-types/src/lib.rs b/shared/src/lib.rs similarity index 93% rename from shared-types/src/lib.rs rename to shared/src/lib.rs index bbc5ffc..1480e70 100644 --- a/shared-types/src/lib.rs +++ b/shared/src/lib.rs @@ -8,6 +8,11 @@ mod pr; mod streak; mod timeperiod; +#[cfg(feature = "github")] +pub mod github; +#[cfg(feature = "client")] +pub mod near; + pub use pr::*; pub use streak::*; pub use timeperiod::*; @@ -87,6 +92,6 @@ pub struct UserPeriodData { #[borsh(crate = "near_sdk::borsh")] pub struct User { pub name: GithubHandle, - pub period_data: UserPeriodData, + pub period_data: Vec<(TimePeriodString, UserPeriodData)>, pub streaks: Vec<(StreakId, StreakUserData)>, } diff --git a/server/src/api/near.rs b/shared/src/near.rs similarity index 86% rename from server/src/api/near.rs rename to shared/src/near.rs index 5f348d6..c282815 100644 --- a/server/src/api/near.rs +++ b/shared/src/near.rs @@ -1,3 +1,5 @@ +use std::str::FromStr; + use anyhow::bail; use near_workspaces::{types::SecretKey, Contract}; use serde_json::json; @@ -5,7 +7,7 @@ use tracing::instrument; use super::github::PrMetadata; -pub use shared_types::*; +use crate::*; #[derive(Clone, Debug)] pub struct NearClient { @@ -13,7 +15,8 @@ pub struct NearClient { } impl NearClient { - pub async fn new(contract: String, sk: SecretKey, mainnet: bool) -> anyhow::Result { + pub async fn new(contract: String, sk: String, mainnet: bool) -> anyhow::Result { + let sk = SecretKey::from_str(&sk)?; if mainnet { let mainnet = near_workspaces::mainnet().await?; let contract = Contract::from_secret_key(contract.parse()?, sk, &mainnet); @@ -264,10 +267,47 @@ impl NearClient { .view("user") .args_json(json!({ "user": user, + "periods": vec![TimePeriod::AllTime.time_string(0)] })) .await .map_err(|e| anyhow::anyhow!("Failed to call user_info: {:?}", e))?; let res = res.json()?; Ok(res) } + + pub async fn users_paged( + &self, + page: u64, + limit: u64, + periods: Vec, + ) -> anyhow::Result> { + let res = self + .contract + .view("users") + .args_json(json!({ + "page": page, + "limit": limit, + "periods": periods, + })) + .await + .map_err(|e| anyhow::anyhow!("Failed to call users: {:?}", e))?; + let res = res.json()?; + Ok(res) + } + + #[instrument(skip(self))] + pub async fn users(&self, periods: Vec) -> anyhow::Result> { + let mut page = 0; + const LIMIT: u64 = 100; + let mut res = vec![]; + loop { + let users = self.users_paged(page, LIMIT, periods.clone()).await?; + if users.is_empty() { + break; + } + res.extend(users); + page += 1; + } + Ok(res) + } } diff --git a/shared-types/src/pr.rs b/shared/src/pr.rs similarity index 100% rename from shared-types/src/pr.rs rename to shared/src/pr.rs diff --git a/shared-types/src/streak.rs b/shared/src/streak.rs similarity index 100% rename from shared-types/src/streak.rs rename to shared/src/streak.rs diff --git a/shared-types/src/timeperiod.rs b/shared/src/timeperiod.rs similarity index 100% rename from shared-types/src/timeperiod.rs rename to shared/src/timeperiod.rs